use std::fs;
use std::process::Command;
use tempfile::TempDir;
#[test]
fn test_cli_json_output_format() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let test_file = temp_dir.path().join("test.bin");
fs::write(&test_file, b"Hello, World!").expect("Failed to write test file");
let magic_file = temp_dir.path().join("test.magic");
fs::write(&magic_file, "0 byte 72 Hello file\n").expect("Failed to write magic file");
let output = Command::new("cargo")
.args([
"run",
"--bin",
"rmagic",
"--",
test_file.to_str().unwrap(),
"--json",
"--magic-file",
magic_file.to_str().unwrap(),
])
.output()
.expect("Failed to execute command");
if !output.status.success() {
eprintln!(
"Command failed with stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
panic!("Command failed with exit code: {:?}", output.status.code());
}
let stdout = String::from_utf8(output.stdout).expect("Invalid UTF-8 in stdout");
let json_value: serde_json::Value =
serde_json::from_str(&stdout).expect("Failed to parse JSON output");
assert!(json_value.is_object(), "Output should be a JSON object");
let json_obj = json_value.as_object().unwrap();
assert!(
json_obj.contains_key("matches"),
"JSON should contain 'matches' field"
);
let matches = json_obj["matches"]
.as_array()
.expect("'matches' should be an array");
if !matches.is_empty() {
let first_match = &matches[0];
assert!(first_match.is_object(), "Match should be a JSON object");
let match_obj = first_match.as_object().unwrap();
assert!(
match_obj.contains_key("text"),
"Match should contain 'text' field"
);
assert!(
match_obj.contains_key("offset"),
"Match should contain 'offset' field"
);
assert!(
match_obj.contains_key("value"),
"Match should contain 'value' field"
);
assert!(
match_obj.contains_key("tags"),
"Match should contain 'tags' field"
);
assert!(
match_obj.contains_key("score"),
"Match should contain 'score' field"
);
assert!(match_obj["text"].is_string(), "'text' should be a string");
assert!(
match_obj["offset"].is_number(),
"'offset' should be a number"
);
assert!(match_obj["value"].is_string(), "'value' should be a string");
assert!(match_obj["tags"].is_array(), "'tags' should be an array");
assert!(match_obj["score"].is_number(), "'score' should be a number");
}
}
#[test]
fn test_cli_json_output_no_matches() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let test_file = temp_dir.path().join("test.bin");
fs::write(&test_file, b"Random binary data").expect("Failed to write test file");
let magic_file = temp_dir.path().join("test.magic");
fs::write(&magic_file, "0 byte 255 No match file\n").expect("Failed to write magic file");
let output = Command::new("cargo")
.args([
"run",
"--bin",
"rmagic",
"--",
test_file.to_str().unwrap(),
"--json",
"--magic-file",
magic_file.to_str().unwrap(),
])
.output()
.expect("Failed to execute command");
if !output.status.success() {
eprintln!(
"Command failed with stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
panic!("Command failed with exit code: {:?}", output.status.code());
}
let stdout = String::from_utf8(output.stdout).expect("Invalid UTF-8 in stdout");
let json_value: serde_json::Value =
serde_json::from_str(&stdout).expect("Failed to parse JSON output");
assert!(json_value.is_object(), "Output should be a JSON object");
let json_obj = json_value.as_object().unwrap();
assert!(
json_obj.contains_key("matches"),
"JSON should contain 'matches' field"
);
let matches = json_obj["matches"]
.as_array()
.expect("'matches' should be an array");
if !matches.is_empty() {
assert_eq!(matches.len(), 1, "Should have exactly one fallback match");
let match_obj = matches[0].as_object().unwrap();
assert!(match_obj["text"].is_string(), "'text' should be a string");
}
}
#[test]
fn test_cli_json_output_validity() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "#!/bin/bash\necho 'Hello World'\n").expect("Failed to write test file");
let magic_file = temp_dir.path().join("test.magic");
fs::write(&magic_file, "0 byte 35 Bash script\n").expect("Failed to write magic file");
let output = Command::new("cargo")
.args([
"run",
"--bin",
"rmagic",
"--",
test_file.to_str().unwrap(),
"--json",
"--magic-file",
magic_file.to_str().unwrap(),
])
.output()
.expect("Failed to execute command");
if !output.status.success() {
eprintln!(
"Command failed with stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
panic!("Command failed with exit code: {:?}", output.status.code());
}
let stdout = String::from_utf8(output.stdout).expect("Invalid UTF-8 in stdout");
let json_value: serde_json::Value =
serde_json::from_str(&stdout).expect("Failed to parse JSON output");
let serialized =
serde_json::to_string(&json_value).expect("Failed to serialize JSON back to string");
let _reparsed: serde_json::Value =
serde_json::from_str(&serialized).expect("Failed to reparse serialized JSON");
assert!(json_value.is_object(), "Root should be an object");
let root_obj = json_value.as_object().unwrap();
assert!(
root_obj.contains_key("matches"),
"Should contain matches array"
);
let matches = root_obj["matches"]
.as_array()
.expect("matches should be an array");
for match_item in matches {
assert!(match_item.is_object(), "Each match should be an object");
let match_obj = match_item.as_object().unwrap();
assert!(match_obj.contains_key("text") && match_obj["text"].is_string());
assert!(match_obj.contains_key("offset") && match_obj["offset"].is_number());
assert!(match_obj.contains_key("value") && match_obj["value"].is_string());
assert!(match_obj.contains_key("tags") && match_obj["tags"].is_array());
assert!(match_obj.contains_key("score") && match_obj["score"].is_number());
let score = match_obj["score"]
.as_u64()
.expect("score should be a number");
assert!(score <= 100, "Score should be <= 100, got {}", score);
}
}
#[test]
fn test_cli_json_vs_text_output() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let test_file = temp_dir.path().join("test.bin");
fs::write(&test_file, b"Test content").expect("Failed to write test file");
let magic_file = temp_dir.path().join("test.magic");
fs::write(&magic_file, "0 byte 84 Test file\n").expect("Failed to write magic file");
let json_output = Command::new("cargo")
.args([
"run",
"--bin",
"rmagic",
"--",
test_file.to_str().unwrap(),
"--json",
"--magic-file",
magic_file.to_str().unwrap(),
])
.output()
.expect("Failed to execute JSON command");
let text_output = Command::new("cargo")
.args([
"run",
"--bin",
"rmagic",
"--",
test_file.to_str().unwrap(),
"--text",
"--magic-file",
magic_file.to_str().unwrap(),
])
.output()
.expect("Failed to execute text command");
assert!(json_output.status.success(), "JSON command should succeed");
assert!(text_output.status.success(), "Text command should succeed");
let json_stdout = String::from_utf8(json_output.stdout).expect("Invalid UTF-8 in JSON stdout");
let text_stdout = String::from_utf8(text_output.stdout).expect("Invalid UTF-8 in text stdout");
assert_ne!(
json_stdout, text_stdout,
"JSON and text outputs should be different"
);
let _json_value: serde_json::Value =
serde_json::from_str(&json_stdout).expect("JSON output should be valid JSON");
assert!(
serde_json::from_str::<serde_json::Value>(&text_stdout).is_err(),
"Text output should not be valid JSON"
);
assert!(
text_stdout.contains(test_file.file_name().unwrap().to_str().unwrap()),
"Text output should contain filename"
);
}
#[test]
fn test_rule_match_type_kind_not_serialized_in_evaluation_result() {
use libmagic_rs::parser::ast::{TypeKind, Value};
use libmagic_rs::{EvaluationMetadata, EvaluationResult, evaluator::RuleMatch};
let rule_match = RuleMatch::new(
"ELF executable".to_string(),
0,
0,
Value::Uint(0x7f),
TypeKind::Byte { signed: false },
0.9,
);
let metadata = EvaluationMetadata::default();
let result = EvaluationResult::new(
"ELF executable".to_string(),
None,
0.9,
vec![rule_match],
metadata,
);
let json = serde_json::to_string(&result).expect("must serialize");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("emitted JSON must round-trip through serde_json");
let matches = parsed
.get("matches")
.and_then(|v| v.as_array())
.expect("EvaluationResult JSON must have a `matches` array");
assert!(
!matches.is_empty(),
"EvaluationResult JSON must include at least one rule match for this assertion to be meaningful"
);
for (i, m) in matches.iter().enumerate() {
let obj = m.as_object().unwrap_or_else(|| {
panic!("matches[{i}] must be a JSON object, got {m}");
});
assert!(
!obj.contains_key("type_kind"),
"matches[{i}] must not contain `type_kind` key \
(1B-H2 / 2A-M1 regression: parser AST leaking into JSON output). \
Got keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
assert!(
obj.contains_key("value"),
"matches[{i}] must still include `value` -- #[serde(skip)] should \
only suppress type_kind, not the surrounding fields. Got keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
assert!(
obj.contains_key("confidence"),
"matches[{i}] must still include `confidence`. Got keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn test_rule_match_type_kind_still_accessible_in_rust() {
use libmagic_rs::evaluator::RuleMatch;
use libmagic_rs::parser::ast::{Endianness, TypeKind, Value};
let m = RuleMatch::new(
"test".to_string(),
0,
0,
Value::Uint(0),
TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
1.0,
);
match m.type_kind {
TypeKind::Long {
endian: Endianness::Little,
signed: false,
} => {}
ref other => panic!("type_kind field access broke: got {other:?}"),
}
assert_eq!(m.type_kind.bit_width(), Some(32));
}