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);
    }
}