view src/config.rs @ 26:071e6d446dbd draft

Move tests for rule into right module
author Lewin Bormann <lbo@spheniscida.de>
date Sun, 04 Dec 2016 17:47:36 +0100
parents 2b5e442c8bbb
children
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 rule::{self, Rule};

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

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

/// 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!(rule::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::new_rule(matcher, file, remote, 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_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);
    }
}