attheme 0.3.0

A crate for parsing and serialization of .attheme files.
Documentation
use super::{Attheme, Color, IndexMap, Wallpaper};
use std::borrow::Borrow;

fn parse_hex(string: &str) -> Option<u8> {
    u8::from_str_radix(string, 16).ok()
}

#[allow(clippy::cast_possible_truncation)]
fn parse_color(value: &str) -> Option<Color> {
    if let Ok(value) = value.parse::<i32>() {
        // -1 will become 0xffffffff, just like needed here.
        #[allow(clippy::cast_sign_loss)]
        let value = value as u32;

        // Only the last 8 bits will be left, just like needed here.
        let blue = value as u8;
        let green = (value >> 8) as u8;
        let red = (value >> (8 * 2)) as u8;
        let alpha = (value >> (8 * 3)) as u8;

        Some(Color::new(red, green, blue, alpha))
    } else if value.starts_with('#') {
        // #rrggbb
        if value.len() == 1 + 2 * 3 {
            let red = parse_hex(&value[1..3])?;
            let green = parse_hex(&value[3..5])?;
            let blue = parse_hex(&value[5..7])?;

            Some(Color::new(red, green, blue, 255))
        // #aarrggbb
        } else if value.len() == 1 + 2 * 4 {
            let alpha = parse_hex(&value[1..3])?;
            let red = parse_hex(&value[3..5])?;
            let green = parse_hex(&value[5..7])?;
            let blue = parse_hex(&value[7..9])?;

            Some(Color::new(red, green, blue, alpha))
        } else {
            None
        }
    } else {
        None
    }
}

pub fn from_bytes(contents: &[u8]) -> Attheme {
    let mut variables = IndexMap::new();
    let mut wallpaper: Option<Wallpaper> = None;

    for (index, line) in contents.split(|x| *x == b'\n').enumerate() {
        if let Some(bytes) = &mut wallpaper {
            if line.starts_with(b"WPE") {
                bytes.pop();
                break;
            }

            bytes.extend_from_slice(line);
            bytes.push(b'\n');
            continue;
        }

        if line.starts_with(b"WPS") {
            // \nWPE\n is a typial end of an .attheme file
            let capacity = contents.len() - index - "\nWPE\n".len();

            wallpaper = Some(Vec::with_capacity(capacity));
        }

        let raw_line = String::from_utf8_lossy(line);
        let line = if let Some(index) = raw_line.find("//") {
            &raw_line[..index]
        } else {
            raw_line.borrow()
        };

        let (variable, value) = if let Some(index) = line.find('=') {
            (&line[..index], &line[index + 1..])
        } else {
            continue;
        };

        // This is not a color but a leaked internal integer value.
        if variable == "wallpaperFileOffset" {
            continue;
        }

        // Telegram will silently ignore a variable if it couldn't parse its
        // value. So do we.
        if let Some(value) = parse_color(value) {
            variables.insert(variable.to_string(), value);
        }
    }

    Attheme {
        variables,
        wallpaper,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ignores_invalid_variables() {
        use indexmap::indexmap;

        let contents = b"foo=#12345678\n\
            bar\n\
            quux= #123 \n\
            spam=eggs";

        assert_eq!(
            from_bytes(contents),
            Attheme {
                variables: indexmap! {
                    String::from("foo") => Color::new(0x34, 0x56, 0x78, 0x12),
                },
                wallpaper: None,
            }
        )
    }

    #[test]
    fn does_not_parse_variables_in_wallpaper() {
        let contents = b"WPS\n\
        foo=#123456\n\
        bar=-1\n\
        WPE\n";

        assert_eq!(
            from_bytes(contents),
            Attheme {
                variables: IndexMap::new(),
                wallpaper: Some(
                    b"foo=#123456\n\
                    bar=-1"
                        .to_vec()
                ),
            }
        )
    }
}