Mercurial > lbo > hg > syslog
view src/config.rs @ 22:226f1c607612 draft
Fix file size parsing and general settings extraction
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 03 Dec 2016 16:27:07 +0100 |
parents | eae478565a9f |
children | 2b5e442c8bbb |
line wrap: on
line source
//! See prototype.toml for a full example config file. //! use std::collections::HashMap; use std::default::Default; use std::fs; use std::io::{self, Read}; use std::net::SocketAddr; use std::path::Path; use std::str::FromStr; use dns_lookup; use toml; use priority; /// Parses strings like 0/1s/2m/3h/4d/5w and returns seconds. If no unit is there, seconds is /// assumed. fn parse_duration(s: &str) -> Option<u64> { if s.len() == 0 { return None; } let s = s.trim(); let suffix = &s[s.len() - 1..]; let num_part = &s[0..s.len() - 1]; if suffix == "s" { if let Ok(s) = u64::from_str(num_part) { Some(s) } else { None } } else if suffix == "m" { if let Ok(m) = u64::from_str(num_part) { Some(m * 60) } else { None } } else if suffix == "h" { if let Ok(h) = u64::from_str(num_part) { Some(h * 60 * 60) } else { None } } else if suffix == "d" { if let Ok(d) = u64::from_str(num_part) { Some(d * 24 * 60 * 60) } else { None } } else if suffix == "w" { if let Ok(w) = u64::from_str(num_part) { Some(w * 7 * 24 * 60 * 60) } else { None } } else if let Ok(_) = u64::from_str(suffix) { // no explicit unit if let Ok(s) = u64::from_str(s) { Some(s) } else { None } } else { None } } /// Parses expressions like 1/2B/3k/4M/5G fn parse_size(s: &str) -> Option<u64> { if s.len() == 0 { return None; } let s = s.trim(); let suffix = &s[s.len() - 1..]; let num_part = &s[0..s.len() - 1]; if suffix == "B" { if let Ok(s) = u64::from_str(num_part) { Some(s) } else { None } } else if suffix == "k" { if let Ok(k) = u64::from_str(num_part) { Some(k * 1024) } else { None } } else if suffix == "M" || suffix == "m" { if let Ok(m) = u64::from_str(num_part) { Some(m * 1024 * 1024) } else { None } } else if suffix == "G" || suffix == "g" { if let Ok(g) = u64::from_str(num_part) { Some(g * 1024 * 1024 * 1024) } else { None } } else if let Ok(_) = u64::from_str(suffix) { // no explicit unit if let Ok(s) = u64::from_str(s) { Some(s) } else { None } } else { None } } // helper functions for extracting values from TOML values fn get_string(tbl: &toml::Table, key: &str, default: String) -> String { if let Some(e) = tbl.get(key) { if let &toml::Value::String(ref s) = e { return s.clone(); } } default } fn get_int(tbl: &toml::Table, key: &str, default: i64) -> i64 { if let Some(e) = tbl.get(key) { if let &toml::Value::Integer(i) = e { return i; } } default } fn get_bool(tbl: &toml::Table, key: &str, default: bool) -> bool { if let Some(e) = tbl.get(key) { if let &toml::Value::Boolean(b) = e { return b; } } default } fn get_tbl<'a>(tbl: &'a toml::Table, key: &str) -> Option<&'a toml::Table> { if let Some(e) = tbl.get(key) { if let &toml::Value::Table(ref t) = e { return Some(t); } } None } /// Extract an array of tables. fn get_tbl_arr(tbl: &toml::Table, key: &str) -> Option<Vec<toml::Table>> { if let Some(e) = tbl.get(key) { println!("got tables"); if let &toml::Value::Array(ref a) = e { let mut v = Vec::with_capacity(a.len()); for elem in a { match elem { &toml::Value::Table(ref t) => { v.push(t.clone()) }, _ => (), } } return Some(v) } } None } fn get_tbls(tbl: &toml::Table) -> Option<Vec<(String, toml::Table)>> { let mut res = Vec::with_capacity(tbl.len()); for (k, t) in tbl.iter() { if let &toml::Value::Table(ref t) = t { res.push((k.clone(), t.clone())); } } Some(res) } fn tomlerr<T>(s: String) -> Result<T, toml::Error> { Err(toml::Error::Custom(s)) } // config structs #[derive(Clone, Debug, Default)] pub struct General { pub bind_path: String, pub max_msg_len: usize, } /// Takes a "general" table fn assemble_general(t: &toml::Table) -> Result<General, toml::Error> { let default_bind_path = "/dev/log".to_string(); let default_max_msg_len = 8192; Ok(General { bind_path: get_string(t, "bind_path", default_bind_path), max_msg_len: get_int(t, "max_msg_len", default_max_msg_len) as usize, }) } #[derive(Clone, Debug)] pub struct Remote { addr: SocketAddr, } /// Assemble a Remote from a `remote` entry, and resolve the address if needed; takes a "remote" /// table. fn assemble_remote(t: &toml::Table, name: &str) -> Result<Remote, toml::Error> { let addr = get_string(t, "addr", String::new()); let port = get_int(t, "port", 0); if !addr.is_empty() { if let Ok(mut ips) = dns_lookup::lookup_host(&addr) { if let Some(Ok(ip)) = ips.next() { return Ok(Remote { addr: SocketAddr::new(ip, port as u16) }); } } } Err(toml::Error::Custom(format!("Couldn't parse/resolve host {} for {}", addr, name))) } #[derive(Clone, Debug)] pub enum CompressType { NoCompression, // TODO: implement flate2 for compression. Gzip, } fn assemble_compress_type(v: &toml::Value) -> Result<CompressType, toml::Error> { if let &toml::Value::String(ref s) = v { if s == "none" || s == "" { return Ok(CompressType::NoCompression); } else if s == "gzip" { return Ok(CompressType::Gzip); } else { return Err(toml::Error::Custom(format!("Unknown compression type {}", s))); } } Err(toml::Error::Custom("Expected string for compression type".to_string())) } impl Default for CompressType { fn default() -> CompressType { CompressType::NoCompression } } #[derive(Clone, Debug, Default)] pub struct File { name: String, location: String, /// bytes max_size: usize, /// seconds max_age: u64, history: i32, compress: CompressType, } fn assemble_file(tbl: &toml::Table, name: &str) -> Result<File, toml::Error> { let f = File { name: name.to_string(), location: get_string(tbl, "file", "".to_string()), max_size: parse_size(&get_string(tbl, "max_size", "4M".to_string())).unwrap_or(4 * 1024 * 1024) as usize, max_age: parse_duration(&get_string(tbl, "max_age", "".to_string())).unwrap_or(0), history: get_int(tbl, "history", 10) as i32, compress: assemble_compress_type(tbl.get("compress") .unwrap_or(&toml::Value::String("".to_string()))) .unwrap_or(CompressType::NoCompression), }; Ok(f) } #[derive(Clone, Debug)] pub enum FacPattern { /// '*' Wildcard, Facility(priority::Facility), MultiFacility(Vec<priority::Facility>), } #[derive(Clone, Debug)] pub enum LvlPattern { Wildcard, /// Matches that level, or any lower (more important) level Level(priority::Level), /// Matches only that level ExactLevel(priority::Level), MultiLevel(Vec<priority::Level>), } impl Default for FacPattern { fn default() -> FacPattern { FacPattern::Wildcard } } impl Default for LvlPattern { fn default() -> LvlPattern { LvlPattern::Wildcard } } /// A Filter specifies one or more Facilities, and one or more Levels. type Filter = (FacPattern, LvlPattern); /// A Matcher is one or more Filters. type Matcher = Vec<Filter>; /// a pattern for a rule matches syslog messages based on level and facility. Valid patterns /// are: /// /// *.* -- match all /// mail.* -- match all mail records /// mail,news.* -- match all mail and news records /// mail.warn,info -- match all mail records at warn or info /// mail,news.info,warn -- match all mail or news records at warn or info. /// mail.*;news,auth.info -- match all mail records and records for news or auth at info or higher /// (!) => if only a single level is given, this means "this level or more important"; if you only want to /// match that single level, use a rule like "mail.=info". fn parse_matcher(s: &str) -> Result<Matcher, toml::Error> { let mut matcher = Vec::with_capacity(3); for filter in s.trim().split(';') { if filter.len() == 0 { return Ok(matcher); } match parse_filter(filter) { Ok(f) => matcher.push(f), Err(e) => return Err(e), } } Ok(matcher) } fn parse_filter(s: &str) -> Result<Filter, toml::Error> { let mut parts = s.trim().split('.'); if let Some(facilities) = parts.next() { if let Some(levels) = parts.next() { return match (parse_facility_pattern(facilities), parse_level_pattern(levels)) { (Ok(f), Ok(l)) => Ok((f, l)), (Err(e), Err(f)) => tomlerr(format!("{} {}", e, f)), (Ok(_), Err(e)) => tomlerr(format!("{}", e)), (Err(e), Ok(_)) => tomlerr(format!("{}", e)), }; } } tomlerr(format!("Bad filter pattern: {}", s)) } fn parse_facility_pattern(s: &str) -> Result<FacPattern, toml::Error> { let list: Vec<String> = s.trim().split(',').map(String::from).collect(); if list.len() > 1 { let mut facilities = Vec::with_capacity(list.len()); for f in list { if let Some(fac) = priority::str_to_facility(f.trim()) { facilities.push(fac); } else { return tomlerr(format!("Unrecognized facility: {}", f)); } } Ok(FacPattern::MultiFacility(facilities)) } else if list.len() == 1 { let l = &list[0]; if l == "*" { Ok(FacPattern::Wildcard) } else if let Some(fac) = priority::str_to_facility(l) { Ok(FacPattern::Facility(fac)) } else { tomlerr(format!("Unknown facility: {}", l)) } } else { return tomlerr(format!("Level pattern can't be empty")); } } fn parse_level_pattern(s: &str) -> Result<LvlPattern, toml::Error> { let list: Vec<String> = s.trim().split(',').map(String::from).collect(); if list.len() > 1 { let mut levels = Vec::with_capacity(list.len()); for l in list { if let Some(lvl) = priority::str_to_level(l.trim()) { levels.push(lvl); } else { return tomlerr(format!("Unrecognized facility: {}", l)); } } Ok(LvlPattern::MultiLevel(levels)) } else if list.len() == 1 { let l = &list[0]; // exact rule if &l[0..1] == "=" { if let Some(fac) = priority::str_to_level(&l[1..]) { Ok(LvlPattern::ExactLevel(fac)) } else { tomlerr(format!("Unknown exact facility: {}", l)) } } else if &l[..] == "*" { Ok(LvlPattern::Wildcard) } else if let Some(lvl) = priority::str_to_level(l) { Ok(LvlPattern::Level(lvl)) } else { tomlerr(format!("Unknown facility: {}", &list[0])) } } else { return tomlerr(format!("Level pattern can't be empty")); } } #[derive(Clone, Debug, Default)] pub struct Rule { pattern: Matcher, // refers to the file entry name, not the file name. file: String, remote_dest: String, stop: bool, } /// Takes a "rule" table fn assemble_rule(t: &toml::Table) -> Result<Rule, toml::Error> { let pattern = get_string(t, "pattern", "".to_string()); let matcher = try!(parse_matcher(&pattern)); let file = get_string(t, "dest", "null".to_string()); let remote = get_string(t, "remote_dest", "".to_string()); let stop = get_bool(t, "stop", false); Ok(Rule { pattern: matcher, file: file, remote_dest: remote, stop: stop, }) } #[derive(Clone, Debug, Default)] pub struct Config { pub general: General, remotes: HashMap<String, Remote>, files: HashMap<String, File>, rules: Vec<Rule>, } fn decode_config(text: &String) -> Result<Config, toml::Error> { let mut cfg = Config::default(); if let Some(tbl) = toml::Parser::new(&text).parse() { if let Some(gen) = get_tbl(&tbl, "general") { cfg.general = try!(assemble_general(&gen)); } else { return tomlerr("Could not find 'general' section".to_string()); } // remotes: { remote1: { addr: xy, port: 12 }, remote2: { addr: xz, port: 34 } } if let Some(remotes) = get_tbl(&tbl, "remotes") { if let Some(remotes) = get_tbls(&remotes) { for (name, remote) in remotes { match assemble_remote(&remote, &name) { Ok(remote) => { cfg.remotes.insert(name, remote); } Err(e) => return tomlerr(format!("Error setting up remotes: {}", e)), } } } } // files: { file1: { file: "/a/b/c" history: "1w" }, file2: { file: "/d/e/f", compress: // "none" } } if let Some(files) = get_tbl(&tbl, "files") { if let Some(files) = get_tbls(&files) { for (name, file) in files { match assemble_file(&file, &name) { Ok(file) => { cfg.files.insert(name, file); } Err(e) => return tomlerr(format!("Error setting up files: {}", e)), } } } } if let Some(rules) = get_tbl_arr(&tbl, "rules") { for r in rules { match assemble_rule(&r) { Ok(r) => { cfg.rules.push(r); } Err(e) => return tomlerr(format!("Error setting up rules: {}", e)), } } } Ok(cfg) } else { Err(toml::Error::Custom("Couldn't parse configuration".to_string())) } } pub fn open_and_read_config<P>(p: P) -> io::Result<Config> where P: AsRef<Path> { let f = try!(fs::OpenOptions::new().read(true).open(p)); let contents: Vec<u8> = f.bytes().map(|b| b.unwrap_or(' ' as u8)).collect(); match String::from_utf8(contents) { Ok(s) => { let cfg = decode_config(&s); match cfg { Ok(cfg) => Ok(cfg), Err(e) => { Err(io::Error::new(io::ErrorKind::InvalidData, format!("bad config: {}", e))) } } } Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "UTF8 decoding failed")), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_matcher_parse() { let m = "mail,news.info,warn;kern.=warn;syslog.warn;daemon.debug,info,warn"; assert_eq!(format!("{:?}", super::parse_matcher(m)), "Ok([(MultiFacility([NEWS, LOCAL0]), MultiLevel([WARNING, NONE])), \ (Facility(KERN), ExactLevel(WARNING)), (Facility(SYSLOG), Level(WARNING)), \ (Facility(DAEMON), MultiLevel([WARNING, NONE, NONE]))])"); let m = "mail, news.info; kern.warn"; assert_eq!(format!("{:?}", super::parse_matcher(m)), "Ok([(MultiFacility([NEWS, LOCAL0]), Level(INFO)), (Facility(KERN), \ Level(WARNING))])"); assert_eq!(format!("{:?}", super::parse_matcher("")), "Ok([])"); } #[test] fn test_config_duration_parse() { use super::parse_duration; let cases = vec![("1", 1), ("10", 10), ("10s", 10), ("1m", 60), ("30m", 30 * 60), ("60m", 60 * 60), ("1h", 60 * 60), ("24h", 24 * 60 * 60), ("1d", 24 * 60 * 60), ("4w", 4 * 7 * 24 * 60 * 60)]; for (test, result) in cases { assert_eq!(parse_duration(test), Some(result)); } } #[test] fn test_config_parse_prototype() { let config = open_and_read_config("./prototype.toml"); println!("{:?}", config); } }