forbidden-strings 0.2.0

Out-of-band scanner for forbidden literal strings and regex patterns. Gitignore-aware, fast, dependency-light: built for CI deny-listing of leaked credentials and banned tokens.
// Round-trip and adversarial tests for literal escaping into the verbose dialect.

use super::escape_literal;
use forbidden_regex::RegexSet;

// Compiles one escaped literal into a single-rule set.
fn compiled(literal: &str) -> RegexSet {
    let pattern = escape_literal(literal);
    return RegexSet::new(std::slice::from_ref(&pattern)).expect("escaped literal must compile")
}

// Asserts the escaped literal matches itself and the same bytes embedded in noise.
fn assert_round_trips(literal: &str) {
    let set = compiled(literal);
    assert!(set.is_match(literal.as_bytes()), "did not match literal {literal:?}");
    let embedded = format!("xy{literal}z9");
    assert!(set.is_match(embedded.as_bytes()), "did not match embedded {literal:?}");
}

#[test]
fn metacharacter_literals_round_trip() {
    for literal in [
        "a.b", "a*b", "a+b", "a?b", "a|b", "a^b", "a$b", "a[b", "a]b", "a(b", "a)b", "a{b",
        "a}b", "a&b", "a~b", "a-b", "a/b", "(?:group)", "[class]", "{2,5}", ".*+?", "a.b.c",
    ] {
        assert_round_trips(literal);
    }
}

#[test]
fn dot_is_literal_not_wildcard() {
    let set = compiled("a.c");
    assert!(set.is_match(b"a.c"));
    assert!(!set.is_match(b"axc"));
}

#[test]
fn quantifiers_are_literal() {
    let star = compiled("a*b");
    assert!(star.is_match(b"a*b"));
    assert!(!star.is_match(b"b"));
    assert!(!star.is_match(b"aaab"));

    let plus = compiled("a+b");
    assert!(plus.is_match(b"a+b"));
    assert!(!plus.is_match(b"aaab"));

    let opt = compiled("ab?c");
    assert!(opt.is_match(b"ab?c"));
    assert!(!opt.is_match(b"ac"));
}

#[test]
fn class_brackets_are_literal() {
    let set = compiled("[abc]");
    assert!(set.is_match(b"[abc]"));
    assert!(!set.is_match(b"a"));
    assert!(!set.is_match(b"b"));
}

#[test]
fn pipe_is_literal_not_alternation() {
    let set = compiled("a|b");
    assert!(set.is_match(b"a|b"));
    assert!(!set.is_match(b"a"));
    assert!(!set.is_match(b"b"));
}

#[test]
fn backslash_shorthand_is_two_literal_bytes() {
    // Backslash escaped, so `\d`/`\w` are the two literal bytes, not shorthand classes.
    let digit = compiled("\\d");
    assert!(digit.is_match(b"\\d"));
    assert!(!digit.is_match(b"5"));

    let word = compiled("\\w+");
    assert!(word.is_match(b"\\w+"));
    assert!(!word.is_match(b"abc"));
}

#[test]
fn whitespace_is_preserved() {
    let space = compiled("foo bar");
    assert!(space.is_match(b"foo bar"));
    assert!(!space.is_match(b"foobar"));

    let tab = compiled("a\tb");
    assert!(tab.is_match(b"a\tb"));
    assert!(!tab.is_match(b"ab"));
}

#[test]
fn hash_is_literal_even_leading() {
    // A leading '#' unescaped would begin a verbose comment and swallow the rule,
    // compiling to an empty-matchable pattern; escaping keeps it a literal byte.
    let lead = compiled("#nocomment");
    assert!(lead.is_match(b"#nocomment"));

    let mid = compiled("a#b");
    assert!(mid.is_match(b"a#b"));
    assert!(!mid.is_match(b"ab"));
}

#[test]
fn embedded_newline_round_trips() {
    // Destination-boundary case: the escaper emits a backslash then the real
    // newline byte, which the engine reads as a literal newline.
    let set = compiled("a\nb");
    assert!(set.is_match(b"a\nb"));
    assert!(!set.is_match(b"ab"));
    assert!(escape_literal("x\ny").as_bytes().contains(&b'\n'));
}

#[test]
fn quotes_and_punctuation_pass_through() {
    for literal in ["key=\"value\"", "it's", "a,b;c:d", "<tag>", "@user", "a`b`c", "50%off"] {
        assert_round_trips(literal);
    }
}

#[test]
fn secret_shaped_literal_round_trips() {
    // The final byte uses the `\x50` ('P') convention so the raw 20-char token
    // never appears in this source file, which the repo commit gate scans.
    let secret = "AKIA1234567890ABCDE\x50";
    let set = compiled(secret);
    assert!(set.is_match(secret.as_bytes()));
    assert!(!set.is_match(b"AKIA"));
}