keyring-manager 0.8.2

Cross-platform library for managing passwords
Documentation
use crate::error::*;
use unicode_categories::UnicodeCategories;

fn needs_double_quote(c: char) -> bool {
    matches!(c, '\'' | '\\') || c.is_whitespace() || c.is_separator() || c.is_other()
}
fn needs_quote(c: char) -> bool {
    matches!(
        c,
        '"' | ' '
            | '('
            | ')'
            | '&'
            | '~'
            | '$'
            | '#'
            | '`'
            | ';'
            | '*'
            | '?'
            | '!'
            | '['
            | '>'
            | '<'
            | '|'
    )
}
fn append_quoted(out: &mut String, c: char) {
    let s = match c {
        ' ' => r" ",
        '$' => r"\$",
        '`' => r"\`",
        '\\' => r"\\",
        '"' => r#"\""#,
        '\x07' => r"\a",
        '\x08' => r"\b",
        '\x1b' => r"\e",
        '\x0c' => r"\f",
        '\x0b' => r"\v",
        c if c.is_other() || c.is_separator() => {
            out.extend(c.escape_default());
            return;
        }
        c => {
            out.push(c);
            return;
        }
    };
    out.push_str(s);
}

pub fn escape(s: &str) -> String {
    let mut must_quote = false;
    let must_double_quote = s.chars().any(|x| {
        if needs_quote(x) {
            must_quote = true;
            false
        } else {
            needs_double_quote(x)
        }
    });
    if !must_double_quote {
        if !must_quote {
            return s.to_string();
        }
        return format!("'{s}'");
    }

    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        append_quoted(&mut out, c);
    }
    out.push('"');
    out
}

enum UnescapeState {
    None,
    Single,
    Double,
    DoubleEscape,
    DoubleEscapeUnicode,
    DoubleEscapeUnicodeBraced((u32, usize)),
}

pub fn unescape(s: &str) -> Result<String> {
    let mut state = UnescapeState::None;
    s.chars()
        .try_fold(String::with_capacity(s.len()), |mut acc, c| {
            match state {
                UnescapeState::None => match c {
                    '\'' => state = UnescapeState::Single,
                    '"' => state = UnescapeState::Double,
                    c => acc.push(c),
                },
                UnescapeState::Single => match c {
                    '\'' => state = UnescapeState::None,
                    c => acc.push(c),
                },
                UnescapeState::Double => match c {
                    '"' => state = UnescapeState::None,
                    '\\' => state = UnescapeState::DoubleEscape,
                    c => acc.push(c),
                },
                UnescapeState::DoubleEscape => {
                    match c {
                        ' ' | '\\' | '\'' | '"' | '$' | '`' => acc.push(c),
                        'a' => acc.push('\x07'),
                        'b' => acc.push('\x08'),
                        'e' | 'E' => acc.push('\x1B'),
                        'f' => acc.push('\x0C'),
                        'n' => acc.push('\n'),
                        'r' => acc.push('\r'),
                        't' => acc.push('\t'),
                        'v' => acc.push('\x0B'),
                        'u' => {
                            state = UnescapeState::DoubleEscapeUnicode;
                            return Ok(acc);
                        }
                        c => {
                            return Err(KeyringError::Generic(format!(
                                "invalid escape character: {c}"
                            )));
                        }
                    }
                    state = UnescapeState::Double;
                }
                UnescapeState::DoubleEscapeUnicode => match c {
                    '{' => state = UnescapeState::DoubleEscapeUnicodeBraced((0, 0)),
                    c => {
                        return Err(KeyringError::Generic(format!(
                            "expected unicode escape brace: {c}"
                        )));
                    }
                },
                UnescapeState::DoubleEscapeUnicodeBraced(v) => match c {
                    '}' => match v {
                        (_, 0) => {
                            return Err(KeyringError::Generic("empty unicode escape".to_string()));
                        }
                        (v, _) => {
                            let uc = char::from_u32(v).ok_or_else(|| {
                                KeyringError::Generic(format!("invalid unicode character: {v:x}"))
                            })?;
                            acc.push(uc);
                            state = UnescapeState::Double;
                        }
                    },
                    '0'..='9' | 'a'..='f' | 'A'..='F' => match v {
                        (_, 6..=0xFFFFFFFF) => {
                            return Err(KeyringError::Generic(
                                "overlong unicode escape".to_string(),
                            ));
                        }
                        (v, cnt) => {
                            state = UnescapeState::DoubleEscapeUnicodeBraced((
                                v << 4 | c.to_digit(16).unwrap(),
                                cnt + 1,
                            ));
                        }
                    },
                    c => {
                        return Err(KeyringError::Generic(format!(
                            "invalid character in unicode escape: {c}"
                        )));
                    }
                },
            };

            Ok(acc)
        })
}