pub mod symlink;
use libmagic_rs::LibmagicError;
use std::io::Write;
use std::path::Path;
pub enum FileOutcome {
Classified,
ClassifiedUnreadable(LibmagicError),
}
pub fn stdout_is_terminal() -> bool {
static IS_TERMINAL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*IS_TERMINAL.get_or_init(|| std::io::IsTerminal::is_terminal(&std::io::stdout()))
}
pub fn synthetic_result(description: &str) -> libmagic_rs::EvaluationResult {
let rule_match = libmagic_rs::RuleMatch::new(
description.to_string(),
0,
0,
libmagic_rs::Value::Bytes(Vec::new()),
libmagic_rs::TypeKind::String {
max_length: None,
flags: libmagic_rs::parser::ast::StringFlags::default(),
},
1.0,
);
libmagic_rs::EvaluationResult::new(
description.to_string(),
None,
1.0,
vec![rule_match],
libmagic_rs::EvaluationMetadata::new(0, 0.0, 0, None, false),
)
}
pub fn output_description_bytes(
writer: &mut impl Write,
file_path: &Path,
description: &[u8],
args: &crate::Args,
is_multiple_files: bool,
escape_control_bytes: bool,
) -> Result<(), LibmagicError> {
match args.output_format() {
crate::OutputFormat::Text => {
write!(writer, "{}: ", file_path.display()).map_err(LibmagicError::IoError)?;
let rendered =
symlink::escape_terminal_control_bytes(description, escape_control_bytes);
writer
.write_all(&rendered)
.map_err(LibmagicError::IoError)?;
writeln!(writer).map_err(LibmagicError::IoError)?;
Ok(())
}
crate::OutputFormat::Json => {
let result = synthetic_result(&String::from_utf8_lossy(description));
crate::output_result(
writer,
file_path,
&result,
args,
is_multiple_files,
escape_control_bytes,
)
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use clap::Parser as _;
fn args_text() -> crate::Args {
crate::Args::try_parse_from(["rmagic", "unused.bin"]).unwrap()
}
fn args_json() -> crate::Args {
crate::Args::try_parse_from(["rmagic", "--json", "unused.bin"]).unwrap()
}
#[test]
fn test_output_description_bytes_text_escapes_when_flag_is_set() {
let mut buf = Vec::new();
let description = b"before\x1b]0;pwn\x07after";
let args = args_text();
output_description_bytes(
&mut buf,
Path::new("f.bin"),
description,
&args,
false,
true,
)
.unwrap();
let out = String::from_utf8(buf).unwrap();
assert_eq!(out, "f.bin: before\\x1b]0;pwn\\x07after\n");
}
#[test]
fn test_output_description_bytes_text_is_verbatim_when_flag_is_unset() {
let mut buf = Vec::new();
let description = b"before\x1b]0;pwn\x07after";
let args = args_text();
output_description_bytes(
&mut buf,
Path::new("f.bin"),
description,
&args,
false,
false,
)
.unwrap();
assert_eq!(buf, b"f.bin: before\x1b]0;pwn\x07after\n");
}
#[test]
fn test_output_description_bytes_json_is_unaffected_by_the_escape_flag() {
let description = b"before\x1b]0;pwn\x07after";
let args = args_json();
let mut escaped_run = Vec::new();
output_description_bytes(
&mut escaped_run,
Path::new("f.bin"),
description,
&args,
false,
true,
)
.unwrap();
let mut plain_run = Vec::new();
output_description_bytes(
&mut plain_run,
Path::new("f.bin"),
description,
&args,
false,
false,
)
.unwrap();
assert_eq!(
escaped_run, plain_run,
"R11: JSON carries no file-derived text, so the escape flag must not change it"
);
let json = String::from_utf8(escaped_run).unwrap();
assert!(
json.contains("pwn"),
"the raw control bytes must still reach JSON unescaped: {json}"
);
}
}