use clap::{CommandFactory, Parser};
use clap_complete::Shell;
use clap_stdin::FileOrStdin;
use libmagic_rs::output::json::{format_json_line_output, format_json_output};
use libmagic_rs::parser::{MagicFileFormat, detect_format};
use libmagic_rs::{LibmagicError, MagicDatabase};
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Parser, Debug)]
#[command(
name = "rmagic",
version = env!("CARGO_PKG_VERSION"),
author = "Rust Libmagic Contributors",
about = "A pure-Rust implementation of libmagic for file type identification. Supports multiple files and stdin input.",
after_help = "\
Examples:
rmagic file1.bin file2.txt file3.dat
rmagic -j file.bin # Single file: pretty-printed JSON
rmagic -j file1.bin file2.txt # Multiple files: JSON Lines format
rmagic -s -m custom.magic file1 file2
rmagic -b file.bin # Use built-in rules
rmagic -b -s -j *.bin
rmagic - < input.dat # Read from stdin
rmagic --generate-completion bash > rmagic.bash",
group(clap::ArgGroup::new("format").args(["json", "text"]))
)]
pub struct Args {
#[arg(value_name = "FILE", required_unless_present = "generate_completion", num_args = 1..)]
pub files: Vec<FileOrStdin>,
#[arg(short = 'j', long)]
pub json: bool,
#[arg(long)]
pub text: bool,
#[arg(short = 'm', long = "magic-file", value_name = "FILE")]
pub magic_file: Option<PathBuf>,
#[arg(short = 's', long)]
pub strict: bool,
#[arg(short = 'b', long, conflicts_with = "magic_file")]
pub use_builtin: bool,
#[arg(short = 't', long = "timeout-ms", value_name = "MS",
value_parser = clap::value_parser!(u64).range(1..=300_000))]
pub timeout_ms: Option<u64>,
#[arg(long, value_name = "SHELL")]
pub generate_completion: Option<Shell>,
}
impl Args {
pub fn output_format(&self) -> OutputFormat {
if self.json {
OutputFormat::Json
} else {
OutputFormat::Text
}
}
pub fn get_magic_file_path(&self) -> PathBuf {
if let Some(ref custom_path) = self.magic_file {
custom_path.clone()
} else {
Self::default_magic_file_path()
}
}
pub fn to_evaluation_config(&self) -> libmagic_rs::EvaluationConfig {
libmagic_rs::EvaluationConfig {
timeout_ms: self.timeout_ms,
..Default::default()
}
}
#[cfg(unix)]
const MAGIC_FILE_CANDIDATES: &'static [&'static str] = &[
"/usr/share/file/magic/Magdir", "/usr/share/file/magic", "/usr/share/misc/magic", "/usr/local/share/misc/magic", "/etc/magic", "/opt/local/share/file/magic", "/usr/share/file/magic.mgc", "/usr/local/share/misc/magic.mgc", "/opt/local/share/file/magic.mgc", "/etc/magic.mgc", "/usr/share/misc/magic.mgc", ];
#[cfg(unix)]
pub fn magic_file_candidates() -> &'static [&'static str] {
Self::MAGIC_FILE_CANDIDATES
}
fn default_magic_file_path() -> PathBuf {
#[cfg(unix)]
{
let mut first_binary: Option<PathBuf> = None;
for candidate in Self::MAGIC_FILE_CANDIDATES {
let path = PathBuf::from(candidate);
if !path.exists() {
continue;
}
if let Ok(format) = detect_format(&path) {
match format {
MagicFileFormat::Text | MagicFileFormat::Directory => return path,
MagicFileFormat::Binary => {
if first_binary.is_none() {
first_binary = Some(path);
}
}
}
}
}
if let Some(binary_path) = first_binary {
return binary_path;
}
let repo_magic = PathBuf::from("missing.magic");
if repo_magic.exists() {
return repo_magic;
}
let dev_magic = PathBuf::from("third_party/magic.mgc");
if dev_magic.exists() {
return dev_magic;
}
if std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok() {
return PathBuf::from("third_party/magic.mgc");
}
PathBuf::from("/usr/share/file/magic.mgc")
}
#[cfg(windows)]
{
if let Ok(appdata) = std::env::var("APPDATA") {
let magic_path = PathBuf::from(appdata).join("Magic").join("magic");
if magic_path.exists() {
return magic_path;
}
}
PathBuf::from("third_party/magic.mgc")
}
#[cfg(not(any(unix, windows)))]
{
PathBuf::from("third_party/magic.mgc")
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Text,
Json,
}
fn main() {
let args = Args::parse();
if let Some(shell) = args.generate_completion {
let mut cmd = Args::command();
clap_complete::generate(shell, &mut cmd, "rmagic", &mut std::io::stdout());
return;
}
let interrupted = Arc::new(AtomicBool::new(false));
let interrupted_clone = Arc::clone(&interrupted);
if let Err(e) = ctrlc::set_handler(move || {
interrupted_clone.store(true, Ordering::SeqCst);
}) {
eprintln!("Warning: failed to set signal handler: {e}");
}
let exit_code = match run_analysis(&args, &interrupted) {
Ok(()) => {
if interrupted.load(Ordering::SeqCst) {
eprintln!("Interrupted");
130
} else {
0
}
}
Err(e) => handle_error(e),
};
process::exit(exit_code);
}
fn handle_error(error: LibmagicError) -> i32 {
match error {
LibmagicError::IoError(ref io_err) => handle_io_error(io_err),
LibmagicError::ParseError(ref parse_err) => handle_parse_error_new(parse_err),
LibmagicError::EvaluationError(ref eval_err) => handle_evaluation_error_new(eval_err),
LibmagicError::Timeout { timeout_ms } => handle_timeout_error(timeout_ms),
LibmagicError::ConfigError { ref reason } => {
eprintln!("Configuration error: {reason}");
1
}
LibmagicError::FileError(ref msg) => {
eprintln!("File error: {msg}");
3
}
}
}
fn handle_io_error(io_err: &std::io::Error) -> i32 {
match io_err.kind() {
std::io::ErrorKind::NotFound => {
eprintln!(
"Error: File not found\nThe specified file does not exist or cannot be accessed.\nPlease check the file path and try again."
);
3
}
std::io::ErrorKind::PermissionDenied => {
eprintln!(
"Error: Permission denied\nYou do not have permission to access the specified file.\nPlease check file permissions or run with appropriate privileges."
);
3
}
std::io::ErrorKind::InvalidInput => {
eprintln!(
"Error: Invalid input\nThe file path or arguments provided are invalid.\nPlease check your input and try again."
);
2
}
_ => {
eprintln!(
"Error: File access failed\nFailed to access file: {}\nPlease check the file path and permissions.",
io_err
);
3
}
}
}
fn handle_parse_error_new(parse_err: &libmagic_rs::ParseError) -> i32 {
eprintln!(
"Error: Magic file parse error\n{}\nThe magic file contains invalid syntax or formatting.\nPlease check the magic file format or try a different magic file.",
parse_err
);
4
}
fn handle_evaluation_error_new(eval_err: &libmagic_rs::EvaluationError) -> i32 {
eprintln!(
"Error: Rule evaluation failed\n{}\nFailed to evaluate magic rules against the file.\nThe file may be corrupted or the magic rules may be incompatible.",
eval_err
);
1
}
fn handle_timeout_error(timeout_ms: u64) -> i32 {
eprintln!(
"Error: Evaluation timeout\nFile analysis timed out after {}ms\nThe file may be too large or complex to analyze within the time limit.\nTry using a simpler magic file or increasing the timeout limit.",
timeout_ms
);
5
}
fn load_magic_database(args: &Args) -> Result<MagicDatabase, LibmagicError> {
let config = args.to_evaluation_config();
if args.use_builtin {
return MagicDatabase::with_builtin_rules_and_config(config);
}
let magic_file_path = args.get_magic_file_path();
if !magic_file_path.exists() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(
0,
format!(
"Magic file not found at {}. Please ensure a magic file is available at one of the standard locations or specify a custom path with --magic-file.",
magic_file_path.display()
),
),
));
}
validate_magic_file(&magic_file_path)?;
MagicDatabase::load_from_file_with_config(&magic_file_path, config)
}
fn output_result(
writer: &mut impl Write,
file_path: &Path,
result: &libmagic_rs::EvaluationResult,
args: &Args,
is_multiple_files: bool,
) -> Result<(), LibmagicError> {
match args.output_format() {
OutputFormat::Json => {
let output_result =
libmagic_rs::output::EvaluationResult::from_library_result(result, file_path);
let json_result = if is_multiple_files {
format_json_line_output(file_path, &output_result.matches)
} else {
format_json_output(&output_result.matches)
};
match json_result {
Ok(json_str) => {
writeln!(writer, "{json_str}").map_err(LibmagicError::IoError)?;
}
Err(e) => {
return Err(LibmagicError::EvaluationError(
libmagic_rs::EvaluationError::unsupported_type(format!(
"Failed to serialize JSON: {e}"
)),
));
}
}
}
OutputFormat::Text => {
writeln!(writer, "{}: {}", file_path.display(), result.description)
.map_err(LibmagicError::IoError)?;
}
}
Ok(())
}
fn process_file(
writer: &mut impl Write,
file_or_stdin: &FileOrStdin,
db: &MagicDatabase,
args: &Args,
) -> Result<(), LibmagicError> {
if file_or_stdin.is_stdin() {
use std::io::Read;
let max_string_length = db.config().max_string_length;
let mut buffer = Vec::with_capacity(max_string_length + 1);
let reader = file_or_stdin.clone().into_reader().map_err(|e| {
LibmagicError::IoError(std::io::Error::other(format!("Failed to open stdin: {e}")))
})?;
let mut limited_reader = reader.take((max_string_length + 1) as u64);
limited_reader.read_to_end(&mut buffer).map_err(|e| {
LibmagicError::IoError(std::io::Error::new(
e.kind(),
format!("Failed to read stdin: {e}"),
))
})?;
if buffer.len() > max_string_length {
eprintln!(
"Warning: stdin input truncated to {} bytes",
max_string_length
);
buffer.truncate(max_string_length);
}
let result = db.evaluate_buffer(&buffer)?;
let stdin_path = PathBuf::from("stdin");
let is_multiple_files = args.files.len() > 1;
output_result(writer, &stdin_path, &result, args, is_multiple_files)?;
return Ok(());
}
let file_path = PathBuf::from(file_or_stdin.filename());
validate_input_file(&file_path)?;
let result = db.evaluate_file(&file_path)?;
let is_multiple_files = args.files.len() > 1;
output_result(writer, &file_path, &result, args, is_multiple_files)?;
Ok(())
}
fn run_analysis(args: &Args, interrupted: &AtomicBool) -> Result<(), LibmagicError> {
validate_arguments(args)?;
let db = load_magic_database(args)?;
let mut writer = BufWriter::new(std::io::stdout().lock());
let mut first_error: Option<LibmagicError> = None;
for file_or_stdin in &args.files {
if interrupted.load(Ordering::SeqCst) {
break;
}
match process_file(&mut writer, file_or_stdin, &db, args) {
Ok(()) => {} Err(e) => {
eprintln!("Error processing {}: {}", file_or_stdin.filename(), e);
if first_error.is_none() {
first_error = Some(e);
}
}
}
}
writer.flush().map_err(LibmagicError::IoError)?;
if let Some(error) = first_error
&& args.strict
{
return Err(error);
}
Ok(())
}
fn validate_arguments(args: &Args) -> Result<(), LibmagicError> {
if args.files.is_empty() {
return Err(LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"At least one file must be specified",
)));
}
if !args.use_builtin
&& let Some(ref magic_file) = args.magic_file
{
let magic_str = magic_file.to_string_lossy();
if magic_str.trim().is_empty() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(0, "Magic file path cannot be empty"),
));
}
}
Ok(())
}
fn validate_input_file(file_path: &Path) -> Result<(), LibmagicError> {
if !file_path.exists() {
return Err(LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", file_path.display()),
)));
}
if file_path.is_dir() {
return Err(LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Path is a directory, not a file: {}", file_path.display()),
)));
}
match fs::File::open(file_path) {
Ok(_) => Ok(()),
Err(e) => Err(LibmagicError::IoError(e)),
}
}
fn validate_magic_file(magic_file_path: &Path) -> Result<(), LibmagicError> {
if !magic_file_path.exists() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(
0,
format!("Magic file not found: {}", magic_file_path.display()),
),
));
}
if magic_file_path.is_dir() {
return Ok(());
}
match fs::read(magic_file_path) {
Ok(content) => {
if content.is_empty() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(0, "Magic file is empty"),
));
}
if content.starts_with(b"\x0d\x0a\x1a\x0a") || content.len() > 100_000 {
Ok(())
} else {
match std::str::from_utf8(&content) {
Ok(text_content) => {
if text_content.trim().is_empty() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(0, "Magic file is empty"),
));
}
Ok(())
}
Err(_) => {
Ok(())
}
}
}
}
Err(e) => Err(LibmagicError::IoError(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_basic_file_argument() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "test.bin");
assert!(!args.json);
assert!(!args.text);
assert_eq!(args.output_format(), OutputFormat::Text);
assert!(args.magic_file.is_none());
}
#[test]
fn test_json_output_flag() {
let args = Args::try_parse_from(["rmagic", "test.bin", "--json"]).unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "test.bin");
assert!(args.json);
assert!(!args.text);
assert_eq!(args.output_format(), OutputFormat::Json);
}
#[test]
fn test_text_output_flag() {
let args = Args::try_parse_from(["rmagic", "test.bin", "--text"]).unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "test.bin");
assert!(!args.json);
assert!(args.text);
assert_eq!(args.output_format(), OutputFormat::Text);
}
#[test]
fn test_magic_file_argument() {
let args =
Args::try_parse_from(["rmagic", "test.bin", "--magic-file", "custom.magic"]).unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "test.bin");
assert_eq!(args.magic_file, Some(PathBuf::from("custom.magic")));
}
#[test]
fn test_all_arguments_combined() {
let args = Args::try_parse_from([
"rmagic",
"test.bin",
"--json",
"--magic-file",
"custom.magic",
])
.unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "test.bin");
assert!(args.json);
assert!(!args.text);
assert_eq!(args.output_format(), OutputFormat::Json);
assert_eq!(args.magic_file, Some(PathBuf::from("custom.magic")));
}
#[test]
fn test_json_text_conflict() {
let result = Args::try_parse_from(["rmagic", "test.bin", "--json", "--text"]);
assert!(result.is_err());
}
#[test]
fn test_missing_file_argument() {
let result = Args::try_parse_from(["rmagic"]);
assert!(result.is_err());
}
#[test]
fn test_output_format_default() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
assert_eq!(args.output_format(), OutputFormat::Text);
}
#[test]
fn test_output_format_json() {
let args = Args::try_parse_from(["rmagic", "test.bin", "--json"]).unwrap();
assert_eq!(args.output_format(), OutputFormat::Json);
}
#[test]
fn test_output_format_text_explicit() {
let args = Args::try_parse_from(["rmagic", "test.bin", "--text"]).unwrap();
assert_eq!(args.output_format(), OutputFormat::Text);
}
#[test]
fn test_args_defaults() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
assert!(!args.strict, "strict should default to false");
assert!(!args.use_builtin, "use_builtin should default to false");
}
#[test]
fn test_args_strict_flag() {
let args = Args::try_parse_from(["rmagic", "--strict", "test.bin"]).unwrap();
assert!(args.strict);
}
#[test]
fn test_args_strict_with_json() {
let args = Args::try_parse_from(["rmagic", "--strict", "--json", "test.bin"]).unwrap();
assert!(args.strict);
assert!(args.json);
assert_eq!(args.output_format(), OutputFormat::Json);
}
#[test]
fn test_use_builtin_flag_parsing() {
let args = Args::try_parse_from(["rmagic", "--use-builtin", "test.bin"]).unwrap();
assert!(args.use_builtin);
}
#[test]
fn test_args_single_file_backwards_compatible() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
assert_eq!(args.files.len(), 1);
assert!(!args.strict);
}
#[test]
fn test_args_multiple_files() {
let args = Args::try_parse_from(["rmagic", "file1.bin", "file2.bin", "file3.bin"]).unwrap();
assert_eq!(args.files.len(), 3);
}
#[test]
fn test_args_stdin_detection() {
let args = Args::try_parse_from(["rmagic", "-"]).unwrap();
assert_eq!(args.files.len(), 1);
assert!(args.files[0].is_stdin());
}
#[test]
fn test_complex_file_paths() {
let args = Args::try_parse_from(["rmagic", "/path/to/complex file.bin"]).unwrap();
assert_eq!(args.files.len(), 1);
assert_eq!(args.files[0].filename(), "/path/to/complex file.bin");
}
#[test]
fn test_magic_file_with_spaces() {
let args = Args::try_parse_from([
"rmagic",
"test.bin",
"--magic-file",
"/path/to/magic file.magic",
])
.unwrap();
assert_eq!(
args.magic_file,
Some(PathBuf::from("/path/to/magic file.magic"))
);
}
#[test]
fn test_get_magic_file_path_custom() {
let args =
Args::try_parse_from(["rmagic", "test.bin", "--magic-file", "custom.magic"]).unwrap();
assert_eq!(args.get_magic_file_path(), PathBuf::from("custom.magic"));
}
#[test]
fn test_get_magic_file_path_default() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
let default_path = args.get_magic_file_path();
#[cfg(unix)]
{
let candidates = Args::magic_file_candidates();
let mut valid_paths: Vec<&str> = candidates.to_vec();
valid_paths.push("missing.magic");
valid_paths.push("third_party/magic.mgc");
assert!(
valid_paths.contains(&default_path.to_str().unwrap()),
"Got unexpected path: {:?}",
default_path
);
}
#[cfg(windows)]
assert_eq!(default_path, PathBuf::from("third_party/magic.mgc"));
#[cfg(not(any(unix, windows)))]
assert_eq!(default_path, PathBuf::from("third_party/magic.mgc"));
}
#[test]
fn test_default_magic_file_path() {
let default_path = Args::default_magic_file_path();
#[cfg(unix)]
{
let candidates = Args::magic_file_candidates();
let mut valid_paths: Vec<&str> = candidates.to_vec();
valid_paths.push("missing.magic");
valid_paths.push("third_party/magic.mgc");
assert!(
valid_paths.contains(&default_path.to_str().unwrap()),
"Got unexpected path: {:?}",
default_path
);
}
#[cfg(windows)]
assert_eq!(default_path, PathBuf::from("third_party/magic.mgc"));
#[cfg(not(any(unix, windows)))]
assert_eq!(default_path, PathBuf::from("third_party/magic.mgc"));
}
#[test]
fn test_handle_error_file_not_found() {
let error = LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"File not found",
));
let exit_code = handle_error(error);
assert_eq!(exit_code, 3);
}
#[test]
fn test_handle_error_permission_denied() {
let error = LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Permission denied",
));
let exit_code = handle_error(error);
assert_eq!(exit_code, 3);
}
#[test]
fn test_handle_error_invalid_input() {
let error = LibmagicError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid input",
));
let exit_code = handle_error(error);
assert_eq!(exit_code, 2);
}
#[test]
fn test_handle_error_parse_error() {
let error = LibmagicError::ParseError(libmagic_rs::ParseError::invalid_syntax(
42,
"Invalid syntax",
));
let exit_code = handle_error(error);
assert_eq!(exit_code, 4);
}
#[test]
fn test_handle_error_evaluation_error() {
let error = LibmagicError::EvaluationError(libmagic_rs::EvaluationError::unsupported_type(
"Evaluation failed",
));
let exit_code = handle_error(error);
assert_eq!(exit_code, 1);
}
#[test]
fn test_handle_error_timeout() {
let error = LibmagicError::Timeout { timeout_ms: 5000 };
let exit_code = handle_error(error);
assert_eq!(exit_code, 5);
}
#[test]
fn test_validate_arguments_empty_files() {
let _args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
let args_empty = Args {
files: vec![],
json: false,
text: false,
magic_file: None,
strict: false,
use_builtin: false,
timeout_ms: None,
generate_completion: None,
};
let result = validate_arguments(&args_empty);
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::IoError(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::InvalidInput);
assert!(
e.to_string()
.contains("At least one file must be specified")
);
}
_ => panic!("Expected IoError with InvalidInput"),
}
}
#[test]
fn test_validate_arguments_empty_magic_file() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
let args_with_empty_magic = Args {
files: args.files,
json: false,
text: false,
magic_file: Some(PathBuf::from("")),
strict: false,
use_builtin: false,
timeout_ms: None,
generate_completion: None,
};
let result = validate_arguments(&args_with_empty_magic);
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ParseError(parse_err) => {
let msg = parse_err.to_string();
assert!(msg.contains("Magic file path cannot be empty"));
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_validate_arguments_valid() {
let args = Args::try_parse_from(["rmagic", "test.bin"]).unwrap();
let args_with_magic = Args {
files: args.files,
json: false,
text: false,
magic_file: Some(PathBuf::from("magic.db")),
strict: false,
use_builtin: false,
timeout_ms: None,
generate_completion: None,
};
let result = validate_arguments(&args_with_magic);
assert!(result.is_ok());
}
#[test]
fn test_validate_input_file_not_found() {
let result = validate_input_file(&PathBuf::from("nonexistent_file.bin"));
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::IoError(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
assert!(e.to_string().contains("File not found"));
}
_ => panic!("Expected IoError with NotFound"),
}
}
#[test]
fn test_validate_input_file_directory() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let result = validate_input_file(temp_dir.path());
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::IoError(e) => {
assert_eq!(e.kind(), std::io::ErrorKind::InvalidInput);
assert!(e.to_string().contains("Path is a directory"));
}
_ => panic!("Expected IoError with InvalidInput"),
}
}
#[test]
fn test_validate_input_file_valid() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test_validate_file.bin");
fs::write(&temp_file, b"test content").expect("Failed to write test file");
let result = validate_input_file(&temp_file);
assert!(result.is_ok());
}
#[test]
fn test_validate_magic_file_not_found() {
let result = validate_magic_file(&PathBuf::from("nonexistent_magic.db"));
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ParseError(parse_err) => {
let msg = parse_err.to_string();
assert!(msg.contains("Magic file not found"));
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_validate_magic_file_directory() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let result = validate_magic_file(temp_dir.path());
assert!(result.is_ok());
}
#[test]
fn test_validate_magic_file_empty() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test_empty_magic.db");
fs::write(&temp_file, "").expect("Failed to write test file");
let result = validate_magic_file(&temp_file);
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ParseError(parse_err) => {
let msg = parse_err.to_string();
assert!(msg.contains("Magic file is empty"));
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_validate_magic_file_whitespace_only() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test_whitespace_magic.db");
fs::write(&temp_file, " \n\t \r\n ").expect("Failed to write test file");
let result = validate_magic_file(&temp_file);
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ParseError(parse_err) => {
let msg = parse_err.to_string();
assert!(msg.contains("Magic file is empty"));
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_validate_magic_file_valid() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let temp_file = temp_dir.path().join("test_valid_magic.db");
fs::write(&temp_file, "# Magic file\n0 string test Test file")
.expect("Failed to write test file");
let result = validate_magic_file(&temp_file);
assert!(result.is_ok());
}
#[test]
#[cfg(unix)]
fn test_magic_file_search_order_text_first() {
let candidates = Args::magic_file_candidates();
let first_binary_index = candidates
.iter()
.position(|c| c.ends_with(".mgc"))
.expect("Should have at least one .mgc candidate");
for (i, candidate) in candidates.iter().enumerate() {
if i < first_binary_index {
assert!(
!candidate.ends_with(".mgc"),
"Candidate at index {} should be text (not .mgc): {}",
i,
candidate
);
}
}
for (i, candidate) in candidates.iter().enumerate() {
if i >= first_binary_index {
assert!(
candidate.ends_with(".mgc"),
"Candidate at index {} should be binary (.mgc): {}",
i,
candidate
);
}
}
assert!(
first_binary_index > 0,
"Should have at least one text candidate before binary candidates"
);
assert!(
first_binary_index < candidates.len(),
"Should have at least one binary candidate"
);
}
#[test]
#[cfg(unix)]
fn test_magic_file_search_order_magdir_priority() {
let candidates = Args::magic_file_candidates();
assert_eq!(
candidates[0], "/usr/share/file/magic/Magdir",
"First candidate should be the Magdir directory"
);
}
#[test]
#[cfg(unix)]
fn test_magic_file_candidates_exact_sequence() {
let candidates = Args::magic_file_candidates();
let expected = [
"/usr/share/file/magic/Magdir",
"/usr/share/file/magic",
"/usr/share/misc/magic",
"/usr/local/share/misc/magic",
"/etc/magic",
"/opt/local/share/file/magic",
"/usr/share/file/magic.mgc",
"/usr/local/share/misc/magic.mgc",
"/opt/local/share/file/magic.mgc",
"/etc/magic.mgc",
"/usr/share/misc/magic.mgc",
];
assert_eq!(
candidates.len(),
expected.len(),
"Candidate list length mismatch"
);
for (i, (actual, expected)) in candidates.iter().zip(expected.iter()).enumerate() {
assert_eq!(
actual, expected,
"Candidate mismatch at index {}: got '{}', expected '{}'",
i, actual, expected
);
}
}
#[test]
#[cfg(unix)]
fn test_magic_file_search_selects_first_existing() {
use std::io::Write;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let text_magic_path = temp_dir.path().join("text_magic");
let mut text_file =
fs::File::create(&text_magic_path).expect("Failed to create text magic file");
writeln!(text_file, "# Text magic file").expect("Failed to write");
writeln!(text_file, "0 string test Test file").expect("Failed to write");
let binary_magic_path = temp_dir.path().join("binary.mgc");
fs::write(&binary_magic_path, b"\x1c\x04\x1e\xf1test")
.expect("Failed to create binary magic file");
assert!(text_magic_path.exists());
let text_format = detect_format(&text_magic_path);
assert!(
matches!(text_format, Ok(MagicFileFormat::Text)),
"Text magic file should be detected as Text format, got {:?}",
text_format
);
assert!(binary_magic_path.exists());
let binary_format = detect_format(&binary_magic_path);
assert!(
matches!(binary_format, Ok(MagicFileFormat::Binary)),
"Binary magic file should be detected as Binary format, got {:?}",
binary_format
);
}
#[test]
#[cfg(unix)]
fn test_magic_file_search_binary_fallback() {
let candidates = Args::magic_file_candidates();
let text_count = candidates.iter().filter(|c| !c.ends_with(".mgc")).count();
let binary_count = candidates.iter().filter(|c| c.ends_with(".mgc")).count();
assert!(text_count > 0, "Should have text candidates");
assert!(binary_count > 0, "Should have binary candidates");
let first_text_idx = candidates
.iter()
.position(|c| !c.ends_with(".mgc"))
.unwrap();
let first_binary_idx = candidates.iter().position(|c| c.ends_with(".mgc")).unwrap();
assert!(
first_text_idx < first_binary_idx,
"Text candidates should come before binary candidates"
);
}
}