use assert_cmd::Command;
use libmagic_rs::EvaluationConfig;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_cli_rejects_planted_missing_magic_in_cwd() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("missing.magic"),
"0 string TEST planted-magic-pwn\n",
)
.unwrap();
let target = dir.path().join("target.bin");
fs::write(&target, b"TEST").unwrap();
let out = Command::cargo_bin("rmagic")
.unwrap()
.current_dir(dir.path())
.arg(target.file_name().unwrap())
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stdout.contains("planted-magic-pwn"),
"CLI resolved planted ./missing.magic from cwd (S-H1 regression)\n\
stdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn test_cli_rejects_planted_third_party_magic_in_ci_env() {
let dir = TempDir::new().unwrap();
let tp = dir.path().join("third_party");
fs::create_dir_all(&tp).unwrap();
fs::write(tp.join("magic.mgc"), "0 string EVIL planted-ci-magic-pwn\n").unwrap();
let target = dir.path().join("target.bin");
fs::write(&target, b"EVIL").unwrap();
let out = Command::cargo_bin("rmagic")
.unwrap()
.current_dir(dir.path())
.env("CI", "true")
.env("GITHUB_ACTIONS", "true")
.arg(target.file_name().unwrap())
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stdout.contains("planted-ci-magic-pwn"),
"CLI resolved planted third_party/magic.mgc under CI env (S-H1 regression)\n\
stdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn test_file_buffer_error_uses_caller_path_not_canonical() {
use libmagic_rs::io::{FileBuffer, IoError};
use std::path::PathBuf;
let dir = TempDir::new().unwrap();
let path = dir.path().join("empty.bin");
fs::write(&path, b"").unwrap();
let err = FileBuffer::new(&path).unwrap_err();
match err {
IoError::EmptyFile { path: reported } => {
assert_eq!(
reported,
PathBuf::from(&path),
"EmptyFile error should report caller-supplied path, not canonicalized"
);
}
other => panic!("Expected EmptyFile, got {other:?}"),
}
}
#[test]
fn test_regex_compile_bounded_for_pathological_patterns() {
use libmagic_rs::evaluator::{EvaluationContext, evaluate_rules};
use libmagic_rs::parser::ast::{RegexCount, RegexFlags};
use libmagic_rs::{MagicRule, OffsetSpec, Operator, TypeKind, Value};
use std::time::Instant;
let cases: &[(&str, &str)] = &[
("[a-z]{1000000}", "huge character-class repetition"),
("a{1000000}", "huge literal repetition"),
(".{1000000}", "huge any-char repetition"),
];
let buf = vec![b'a'; 128];
let config = EvaluationConfig::default().with_timeout_ms(Some(1000));
for (pat, label) in cases {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::Regex {
flags: RegexFlags::default(),
count: RegexCount::Default,
},
Operator::Equal,
Value::String((*pat).to_string()),
"never-matches".to_string(),
);
let mut ctx = EvaluationContext::new(config.clone());
let start = Instant::now();
let _ = evaluate_rules(&[rule], &buf, &mut ctx);
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 500,
"{label}: pathological regex ran for {elapsed:?} (S-M2 regression)"
);
}
}
#[test]
fn test_evaluation_config_default_is_unbounded() {
let cfg = EvaluationConfig::default();
assert_eq!(
cfg.timeout_ms, None,
"EvaluationConfig::default() is expected to leave timeout_ms unset. \
If you are intentionally changing this behavior, update GOTCHAS S13.1 \
and the rustdoc `# Security` section on the MagicDatabase constructors."
);
}
fn one_mib_nul_free() -> Vec<u8> {
vec![b'A'; 1_048_576]
}
fn unflagged_string_x_rule() -> libmagic_rs::MagicRule {
use libmagic_rs::parser::ast::StringFlags;
use libmagic_rs::{MagicRule, OffsetSpec, Operator, TypeKind, Value};
MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::AnyValue,
Value::Uint(0),
"captured: %s".to_string(),
)
}
fn flagged_string_equal_rule(
flags: libmagic_rs::parser::ast::StringFlags,
pattern: &str,
) -> libmagic_rs::MagicRule {
use libmagic_rs::{MagicRule, OffsetSpec, Operator, TypeKind, Value};
MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags,
},
Operator::Equal,
Value::String(pattern.to_string()),
"flagged hit".to_string(),
)
}
fn captured_value(
rule: &libmagic_rs::MagicRule,
buffer: &[u8],
cap: usize,
) -> Option<libmagic_rs::parser::ast::Value> {
use libmagic_rs::evaluator::{EvaluationContext, evaluate_rules};
let config = EvaluationConfig::default()
.with_max_string_length(cap)
.with_timeout_ms(Some(5_000));
let mut ctx = EvaluationContext::new(config);
let matches = evaluate_rules(std::slice::from_ref(rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for these simple rules");
matches.into_iter().next().map(|m| m.value)
}
fn captured_len(v: &libmagic_rs::parser::ast::Value) -> usize {
use libmagic_rs::parser::ast::Value;
match v {
Value::String(s) => s.len(),
Value::Bytes(b) => b.len(),
other => panic!("expected string/bytes capture, got {other:?}"),
}
}
#[test]
fn test_max_string_length_caps_unflagged_string_x() {
let buf = one_mib_nul_free();
let rule = unflagged_string_x_rule();
let captured =
captured_value(&rule, &buf, 64).expect("unflagged `string x` should match any buffer");
let len = captured_len(&captured);
assert_eq!(
len, 64,
"unflagged string x must cap at max_string_length=64; got {len} bytes \
(2A-H1 regression: dispatcher dropped the cap)"
);
}
#[test]
fn test_max_string_length_flagged_path_works_at_non_zero_offset() {
use libmagic_rs::evaluator::{EvaluationContext, evaluate_rules};
use libmagic_rs::parser::ast::StringFlags;
use libmagic_rs::{MagicRule, OffsetSpec, Operator, TypeKind, Value};
let mut buf = vec![b'A'; 50];
buf.extend_from_slice(b"hit");
let rule = MagicRule::new(
OffsetSpec::Absolute(50),
TypeKind::String {
max_length: None,
flags: StringFlags::default().with_ignore_lowercase(true),
},
Operator::Equal,
Value::String("hit".to_string()),
"found at offset".to_string(),
);
let config = EvaluationConfig::default().with_max_string_length(1024);
let mut ctx = EvaluationContext::new(config);
let matches =
evaluate_rules(std::slice::from_ref(&rule), &buf, &mut ctx).expect("must not error");
assert_eq!(
matches.len(),
1,
"flagged string/c at offset 50 must match `hit` with cap=1024; \
a regression to pre-slice from offset would break this"
);
}
#[test]
fn test_max_string_length_caps_flagged_w_whitespace_walk() {
use libmagic_rs::parser::ast::StringFlags;
use std::time::Instant;
let buf = vec![b' '; 16 * 1024 * 1024];
let rule =
flagged_string_equal_rule(StringFlags::default().with_compact_whitespace(true), " X");
let cap = 1024usize;
let start = Instant::now();
let result = captured_value(&rule, &buf, cap);
let elapsed = start.elapsed();
assert!(
result.is_none(),
"flagged string/W ' X' must NOT match an all-whitespace buffer; got {result:?}"
);
assert!(
elapsed.as_millis() < 100,
"flagged string/W against 16 MiB whitespace ran for {elapsed:?} \
(2A-H1 regression: flagged-string scan_buffer ignored max_string_length=1024)"
);
}
#[test]
fn test_max_string_length_minimum_cap_returns_one_byte() {
let buf = one_mib_nul_free();
let v = captured_value(&unflagged_string_x_rule(), &buf, 1).expect("must match");
assert_eq!(
captured_len(&v),
1,
"unflagged: cap=1 must yield 1-byte capture; got {} bytes",
captured_len(&v)
);
}
#[test]
fn test_max_string_length_unflagged_stops_at_nul_before_cap() {
let mut buf = b"hello\0".to_vec();
buf.extend(std::iter::repeat_n(b'A', 1_048_576));
let v = captured_value(&unflagged_string_x_rule(), &buf, 64).expect("must match");
assert_eq!(
captured_len(&v),
5,
"unflagged path must stop at NUL even when cap is larger; \
got {} bytes",
captured_len(&v)
);
}
#[test]
fn test_evaluation_context_clamps_invalid_max_string_length() {
use libmagic_rs::evaluator::EvaluationContext;
let invalid = EvaluationConfig::default().with_max_string_length(0);
let ctx = EvaluationContext::new(invalid);
assert!(
ctx.max_string_length() >= 1,
"EvaluationContext::new must clamp max_string_length=0 to a safe default; \
got {} (SF-1 regression)",
ctx.max_string_length()
);
}
#[test]
fn test_max_string_length_cap_larger_than_buffer_returns_full_buffer() {
let buf = vec![b'A'; 100];
let v = captured_value(&unflagged_string_x_rule(), &buf, 1_000_000).expect("must match");
assert_eq!(
captured_len(&v),
100,
"cap larger than buffer should return full buffer; got {} bytes",
captured_len(&v)
);
}