props 0.1.0

A native Rust library for reading and writing Java properties
Documentation
use std::io::Write;

use super::{Properties, Result};

pub const CR: &[u8] = b"\r";
pub const LF: &[u8] = b"\n";
pub const CRLF: &[u8] = b"\r\n";

pub struct WriteOption {
    comments: String,
    escape_unicode: bool,
    line_ending: &'static [u8],
}

impl WriteOption {
    pub fn comments(&mut self, val: String) -> &Self {
        self.comments = val;
        self
    }

    pub fn escape_unicode(&mut self, val: bool) -> &Self {
        self.escape_unicode = val;
        self
    }

    pub fn line_ending(&mut self, val: &'static [u8]) -> &Self {
        self.line_ending = val;
        self
    }
}

impl Default for WriteOption {
    fn default() -> Self {
        Self {
            comments: String::new(),
            escape_unicode: false,
            line_ending: LF,
        }
    }
}

fn hex(c: usize) -> u8 {
    const CHARS: [u8; 16] = [
        b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E',
        b'F',
    ];
    return CHARS[c & 0x0F];
}

fn do_escape_unicode(target: &mut Vec<u8>, c: usize) {
    let mut ch = c;
    target.push(b'\\');
    target.push(b'u');
    if ch >= 0x10000 {
        ch = (((c - 0x10000) >> 10) & 0x3FF) + 0xD800;
        target.push(hex(ch >> 12));
        target.push(hex(ch >> 8));
        target.push(hex(ch >> 4));
        target.push(hex(ch));

        ch = (c & 0x03FF) + 0xDC00;
        target.push(b'\\');
        target.push(b'u');
    }
    target.push(hex(ch >> 12));
    target.push(hex(ch >> 8));
    target.push(hex(ch >> 4));
    target.push(hex(ch));
}

fn save_comment(
    data: &String,
    escape_unicode: bool,
    line_ending: &'static [u8],
) -> Result<Vec<u8>> {
    let mut result: Vec<u8> = Vec::new();
    let mut last: usize = 0;
    let bytes = data.as_bytes();
    let end = bytes.len();
    let mut indices = data.char_indices();

    result.push(b'#');
    while let Some((mut index, c)) = indices.next() {
        match c {
            '\r' | '\n' => {
                result.write(&bytes[last..index])?;
                result.write(line_ending)?;
                if c == '\r' && index + 1 < end && bytes[index + 1] == b'\n' {
                    indices.next();
                    index = index + 1;
                }
                last = index + 1;
                if last < end && bytes[last] != b'#' {
                    result.push(b'#');
                }
            }
            _ if c > '\u{007f}' && escape_unicode => {
                result.write(&bytes[last..index])?;
                do_escape_unicode(&mut result, c as usize);
                last = index + c.len_utf8();
            }
            _ => {}
        }
    }
    if last < end {
        result.write(&bytes[last..])?;
    }
    result.write(line_ending)?;
    return Ok(result);
}

fn save_convert(data: &String, escape_space: bool, escape_unicode: bool) -> Result<Vec<u8>> {
    let bytes = data.as_bytes();
    let mut result: Vec<u8> = Vec::new();

    for (i, c) in data.char_indices() {
        if c > 61 as char && c < 127 as char {
            if c == '\\' {
                result.push(b'\\');
                result.push(b'\\');
            } else {
                result.push(c as u8);
            }
        } else {
            match c {
                ' ' => {
                    if i == 0 || escape_space {
                        result.push(b'\\');
                    }
                    result.push(b' ');
                }
                '\t' => {
                    result.push(b'\\');
                    result.push(b't');
                }
                '\n' => {
                    result.push(b'\\');
                    result.push(b'n');
                }
                '\r' => {
                    result.push(b'\\');
                    result.push(b'r');
                }
                '\x0c' => {
                    result.push(b'\\');
                    result.push(b'f');
                }
                '=' | ':' | '#' | '!' => {
                    result.push(b'\\');
                    result.push(c as u8);
                }
                c if escape_unicode && (c < '\u{0020}' || c > '\u{007e}') => {
                    do_escape_unicode(&mut result, c as usize);
                }
                _ => {
                    result.write(&bytes[i..i + c.len_utf8()])?;
                }
            }
        }
    }
    return Ok(result);
}

impl Properties {
    pub fn store<W: Write>(&mut self, mut writer: W, opt: &WriteOption) -> Result<()> {
        if opt.comments.len() > 0 {
            writer.write(&save_comment(
                &opt.comments,
                opt.escape_unicode,
                opt.line_ending,
            )?)?;
        }

        let data = self.data.lock().unwrap();
        for (k, v) in data.iter() {
            let key = save_convert(k, true, opt.escape_unicode)?;
            let val = save_convert(v, false, opt.escape_unicode)?;

            writer.write(&key)?;
            writer.write(b"=")?;
            writer.write(&val)?;
            writer.write(opt.line_ending)?;
        }
        writer.flush()?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::{Properties, WriteOption, CR, CRLF};

    #[test]
    fn normal() {
        let cases = vec![
            ("a0", "b", "a0=b\n"),
            (" a 1 ", " b c ", "\\ a\\ 1\\ =\\ b c \n"),
            ("a2", "\\b", "a2=\\\\b\n"),
            ("a3", "\t\n\r\x0c=:#!b", "a3=\\t\\n\\r\\f\\=\\:\\#\\!b\n"),
            ("a4", "你好©🌐", "a4=你好©🌐\n"),
        ];
        for &(key, val, expected) in &cases {
            let mut buff = Vec::new();
            let mut prop = Properties::new();
            prop.set(key, val);
            if let Err(e) = prop.store(&mut buff, &WriteOption::default()) {
                panic!("store properties failed, {}", e);
            }
            let actual = String::from_utf8(buff).unwrap();
            if actual != expected {
                panic!("unexpected result, got {} expect {}", actual, expected);
            }
        }
    }

    #[test]
    fn escape_unicode() {
        let cases = vec![
            (
                "a0",
                "你好©🌐",
                "a0=\\u4F60\\u597D\\u00A9\\uD83C\\uDF10\r\n",
            ),
            ("a1", "\x01Hello", "a1=\\u0001Hello\r\n"),
        ];
        let mut opt = WriteOption::default();
        opt.escape_unicode(true);
        opt.line_ending(CRLF);

        for &(key, val, expected) in &cases {
            let mut buff = Vec::new();
            let mut prop = Properties::new();
            prop.set(key, val);
            if let Err(e) = prop.store(&mut buff, &opt) {
                panic!("store properties failed, {}", e);
            }
            let actual = String::from_utf8(buff).unwrap();
            if actual != expected {
                panic!("unexpected result, got {} expect {}", actual, expected);
            }
        }
    }

    #[test]
    fn comments() {
        let cases = vec![(
            true,
            "Hello\r\n你好©🌐\nWorld",
            "a0",
            "b",
            "#Hello\r#\\u4F60\\u597D\\u00A9\\uD83C\\uDF10\r#World\ra0=b\r",
        )];
        let mut opt = WriteOption::default();
        opt.line_ending(CR);

        for &(escape, comment, key, val, expected) in &cases {
            let mut buff = Vec::new();
            let mut prop = Properties::new();
            opt.escape_unicode(escape);
            opt.comments(comment.to_string());
            prop.set(key, val);
            if let Err(e) = prop.store(&mut buff, &opt) {
                panic!("store properties failed, {}", e);
            }
            let actual = String::from_utf8(buff).unwrap();
            if actual != expected {
                panic!("unexpected result, got {} expect {}", actual, expected);
            }
        }
    }
}