#![allow(clippy::non_ascii_literal)]
use super::*;
use crate::parser::ast::StringFlags;
fn byte_t() -> TypeKind {
TypeKind::Byte { signed: false }
}
fn long_t() -> TypeKind {
TypeKind::Long {
endian: crate::parser::ast::Endianness::Little,
signed: true,
}
}
#[test]
fn test_signed_decimal_substitution() {
let cases = [
("v=%d", Value::Int(-7), "v=-7"),
("v=%i", Value::Int(42), "v=42"),
("v=%ld", Value::Int(10), "v=10"),
("at_offset %lld", Value::Uint(11), "at_offset 11"),
];
for (tmpl, val, expected) in cases {
assert_eq!(
format_magic_message(tmpl, &val, &byte_t()),
expected,
"template {tmpl:?} with value {val:?}",
);
}
}
#[test]
fn test_unsigned_decimal_substitution() {
let out = format_magic_message("n=%u", &Value::Uint(200), &byte_t());
assert_eq!(out, "n=200");
let out = format_magic_message("n=%llu", &Value::Int(i64::MIN), &long_t());
assert_eq!(out, "n=9223372036854775808");
}
#[test]
fn test_hex_substitution_with_byte_width_masking() {
let out = format_magic_message("0x%02x", &Value::Uint(0x31), &byte_t());
assert_eq!(out, "0x31");
let out = format_magic_message("0x%02x", &Value::Int(-1), &byte_t());
assert_eq!(out, "0xff");
let out = format_magic_message("%X", &Value::Uint(0xdead_beef), &long_t());
assert_eq!(out, "DEADBEEF");
let out = format_magic_message("%#x", &Value::Uint(0xab), &byte_t());
assert_eq!(out, "0xab");
let out = format_magic_message("%#06x", &Value::Uint(0xab), &byte_t());
assert_eq!(out, "0x00ab");
let out = format_magic_message("%#6x", &Value::Uint(0xab), &byte_t());
assert_eq!(out, " 0xab");
let out = format_magic_message("%-#6x|", &Value::Uint(0xab), &byte_t());
assert_eq!(out, "0xab |");
let out = format_magic_message("%#08o", &Value::Uint(8), &byte_t());
assert_eq!(out, "00000010");
let out = format_magic_message("%#X", &Value::Uint(0xab), &byte_t());
assert_eq!(out, "0XAB");
}
#[test]
fn test_string_substitution() {
let out = format_magic_message(
"hello %s",
&Value::String("world".to_string()),
&TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
);
assert_eq!(out, "hello world");
let out = format_magic_message(
"data=%s",
&Value::Bytes(b"abc".to_vec()),
&TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
);
assert_eq!(out, "data=abc");
}
#[test]
fn test_string_precision_truncation() {
let str_t = TypeKind::String {
max_length: None,
flags: StringFlags::default(),
};
let cases: &[(&str, &str, &str)] = &[
(
"%.3s document text",
"1.0\" encoding=\"UTF-8\"?>",
"1.0 document text",
),
("%.1s", "1.0", "1"),
("%.10s", "abc", "abc"),
("%.3s", "abc", "abc"),
("%.0s", "abc", ""),
("%-.4s", "versionX", "vers"),
("[%4.4s]", "ab", "[ ab]"),
("[%-4.2s]", "hello", "[he ]"),
];
for (template, value, expected) in cases {
let out = format_magic_message(template, &Value::String((*value).to_string()), &str_t);
assert_eq!(
out, *expected,
"template {template:?} on value {value:?} should render {expected:?}",
);
}
}
#[test]
fn test_alt_form_prefix_suppressed_on_zero_value() {
let out = format_magic_message("%#o", &Value::Uint(0), &byte_t());
assert_eq!(out, "0", "%#o with 0 must emit single '0', not '00'");
let out = format_magic_message("%#x", &Value::Uint(0), &byte_t());
assert_eq!(out, "0", "%#x with 0 must emit single '0', not '0x0'");
let out = format_magic_message("%#X", &Value::Uint(0), &byte_t());
assert_eq!(out, "0", "%#X with 0 must emit single '0', not '0X0'");
let out = format_magic_message("%#x", &Value::Uint(1), &byte_t());
assert_eq!(out, "0x1");
}
#[test]
fn test_octal_substitution() {
let out = format_magic_message("%o", &Value::Uint(8), &byte_t());
assert_eq!(out, "10");
let out = format_magic_message("%#o", &Value::Uint(8), &byte_t());
assert_eq!(out, "010");
}
#[test]
fn test_char_substitution() {
let out = format_magic_message("[%c]", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, "[A]");
let out = format_magic_message("%c", &Value::Uint(0xa9), &byte_t());
assert_eq!(out, "\u{00a9}");
let out = format_magic_message("%3c", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, " A");
let out = format_magic_message("%-3c|", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, "A |");
}
#[test]
fn test_char_zero_flag_ignored() {
let out = format_magic_message("%03c", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, " A", "%03c must use space-padding, not zero-padding");
let out = format_magic_message("%-03c|", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, "A |", "%-03c must left-align with spaces");
}
#[test]
fn test_percent_escape() {
let out = format_magic_message("100%% sure", &Value::Uint(0), &byte_t());
assert_eq!(out, "100% sure");
}
#[test]
fn test_non_ascii_template_preserved() {
let out = format_magic_message("café %d", &Value::Int(42), &long_t());
assert_eq!(out, "café 42");
let out = format_magic_message("→ %s ←", &Value::String("ok".into()), &byte_t());
assert_eq!(out, "→ ok ←");
let out = format_magic_message("über", &Value::Uint(0), &byte_t());
assert_eq!(out, "über");
}
#[test]
fn test_multiple_specifiers_in_one_template() {
let out = format_magic_message("a=%d b=%d", &Value::Int(5), &long_t());
assert_eq!(out, "a=5 b=5");
}
#[test]
fn test_width_padding() {
let out = format_magic_message("%05d", &Value::Int(-7), &long_t());
assert_eq!(out, "-0007");
let out = format_magic_message("%06d", &Value::Int(-42), &long_t());
assert_eq!(out, "-00042");
let out = format_magic_message("%05d", &Value::Int(42), &long_t());
assert_eq!(out, "00042");
let out = format_magic_message("%5d", &Value::Int(42), &long_t());
assert_eq!(out, " 42");
let out = format_magic_message("%5d", &Value::Int(-7), &long_t());
assert_eq!(out, " -7");
let out = format_magic_message("%-5d|", &Value::Int(42), &long_t());
assert_eq!(out, "42 |");
let out = format_magic_message("%-6d|", &Value::Int(-7), &long_t());
assert_eq!(out, "-7 |");
}
#[test]
fn test_width_cap_prevents_large_allocation() {
let huge_width = format!("%{}d", usize::MAX);
let out = format_magic_message(&huge_width, &Value::Int(1), &long_t());
assert!(
out.len() <= MAX_FORMAT_WIDTH + 1,
"output too long: {}",
out.len()
);
assert!(out.ends_with('1'), "rendered value must appear: {out:?}");
}
#[test]
fn test_empty_template() {
assert_eq!(
format_magic_message("", &Value::Uint(0), &byte_t()),
String::new()
);
}
#[test]
fn test_literal_with_no_specifiers() {
assert_eq!(
format_magic_message("hello world", &Value::Uint(0), &byte_t()),
"hello world"
);
}
#[test]
fn test_trailing_percent_with_no_spec() {
let out = format_magic_message("done %", &Value::Uint(0), &byte_t());
assert_eq!(out, "done %");
}
#[test]
fn test_unknown_specifier_pass_through() {
let out = format_magic_message("bad %q end", &Value::Uint(0), &byte_t());
assert_eq!(out, "bad %q end");
}
#[test]
fn test_type_mismatch_string_conv_on_uint_still_renders() {
let out = format_magic_message("v=%s", &Value::Uint(42), &byte_t());
assert_eq!(out, "v=42");
}
#[test]
fn test_type_mismatch_numeric_conv_on_string_passes_through() {
let out = format_magic_message(
"v=%d",
&Value::String("hi".to_string()),
&TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
);
assert_eq!(out, "v=%d");
}
#[test]
fn test_char_specifier_accepts_full_byte_range() {
let out = format_magic_message("[%c]", &Value::Uint(0xff), &byte_t());
assert_eq!(out, "[\u{00ff}]");
let out = format_magic_message("[%c]", &Value::Uint(u64::from(b'A')), &byte_t());
assert_eq!(out, "[A]");
let out = format_magic_message("[%c]", &Value::Uint(0x1_0000), &byte_t());
assert_eq!(out, "[%c]");
}
#[test]
fn test_byte_width_masking_on_negative_signed_byte() {
let out = format_magic_message("%x", &Value::Int(-1), &byte_t());
assert_eq!(out, "ff");
}
#[test]
fn test_hex_width_masking_respects_16bit() {
let short_t = TypeKind::Short {
endian: crate::parser::ast::Endianness::Little,
signed: true,
};
let out = format_magic_message("%x", &Value::Int(-1), &short_t);
assert_eq!(out, "ffff");
}
fn string16_t() -> TypeKind {
TypeKind::String16 {
endian: crate::parser::ast::Endianness::Little,
}
}
fn pstring_t() -> TypeKind {
TypeKind::PString {
max_length: None,
length_width: crate::parser::ast::PStringLengthWidth::OneByte,
length_includes_itself: false,
}
}
fn regex_t() -> TypeKind {
TypeKind::Regex {
flags: crate::parser::ast::RegexFlags::default(),
count: crate::parser::ast::RegexCount::Default,
}
}
fn str_t() -> TypeKind {
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
}
}
#[test]
fn test_bound_truncates_long_ascii_value_to_127_bytes() {
let value = "A".repeat(304);
let out = format_magic_message_with_gate("%s", &Value::String(value), &str_t(), false);
assert_eq!(out.len(), 127, "rendered field must be exactly 127 bytes");
}
#[test]
fn test_bound_boundary_lengths_127_passes_128_truncates() {
let exactly_127 = "B".repeat(127);
let out =
format_magic_message_with_gate("%s", &Value::String(exactly_127.clone()), &str_t(), false);
assert_eq!(
out, exactly_127,
"a 127-byte value must pass through untouched"
);
let exactly_128 = "B".repeat(128);
let out = format_magic_message_with_gate("%s", &Value::String(exactly_128), &str_t(), false);
assert_eq!(out.len(), 127, "a 128-byte value must render as 127 bytes");
}
#[test]
fn test_bound_cuts_on_utf8_character_boundary() {
let mut value = "C".repeat(126);
value.push('\u{00e9}');
value.push_str("TAIL");
assert_eq!(
value.as_bytes()[126],
0xc3,
"fixture sanity: char starts at 126"
);
let out = format_magic_message_with_gate("%s", &Value::String(value), &str_t(), false);
assert!(
std::str::from_utf8(out.as_bytes()).is_ok(),
"output must be valid UTF-8"
);
assert_eq!(
out.len(),
126,
"the straddling character is dropped whole, not split"
);
assert_eq!(out, "C".repeat(126));
}
#[test]
fn test_gated_rule_stops_at_first_newline() {
let out = format_magic_message_with_gate(
"STR=[%s]",
&Value::String("ZZZZABC\nSECOND".to_string()),
&str_t(),
true,
);
assert_eq!(out, "STR=[ZZZZABC]", "must stop before the \\n");
}
#[test]
fn test_gated_rule_stops_at_first_carriage_return() {
let out = format_magic_message_with_gate(
"STR=[%s]",
&Value::String("ZZZZABC\rSECOND".to_string()),
&str_t(),
true,
);
assert_eq!(out, "STR=[ZZZZABC]", "must stop before the \\r");
}
#[test]
fn test_ungated_rule_does_not_stop_at_newline() {
let out = format_magic_message_with_gate(
"STR=[%s]",
&Value::String("ZZZZABC\nSECOND".to_string()),
&str_t(),
false,
);
assert_eq!(out, "STR=[ZZZZABC\nSECOND]");
}
#[test]
fn test_regex_type_exempt_from_bound_and_newline_stop() {
let mut value = "D".repeat(150);
value.push('\n');
value.push_str("more-after-newline");
let expected = value.clone();
let out = format_magic_message_with_gate("%s", &Value::String(value), ®ex_t(), true);
assert_eq!(
out, expected,
"regex must render past 127 bytes and across a newline"
);
}
#[test]
fn test_pstring_and_string16_are_bounded_and_gated() {
let value = "E".repeat(304);
for type_kind in [pstring_t(), string16_t()] {
let out =
format_magic_message_with_gate("%s", &Value::String(value.clone()), &type_kind, false);
assert_eq!(
out.len(),
127,
"type {type_kind:?} must be bounded to 127 bytes"
);
}
let mut newline_value = "F".repeat(10);
newline_value.push('\n');
newline_value.push_str("tail");
for type_kind in [pstring_t(), string16_t()] {
let out = format_magic_message_with_gate(
"%s",
&Value::String(newline_value.clone()),
&type_kind,
true,
);
assert_eq!(
out,
"F".repeat(10),
"type {type_kind:?} must stop at newline when gated"
);
}
}
#[test]
fn test_bound_measured_on_raw_bytes_not_lossy_decoded_string() {
let mut raw = vec![b'H'; 200];
raw[10] = 0xff;
raw[11] = 0xff;
raw[12] = 0xff;
let out = format_magic_message_with_gate("%s", &Value::Bytes(raw.clone()), &str_t(), false);
let expected = String::from_utf8_lossy(&raw[..127]).into_owned();
assert_eq!(
out, expected,
"must decode exactly the first 127 raw file bytes, not 127 post-decode bytes"
);
}
#[test]
fn test_precision_composes_as_minimum_with_bound() {
let value = "G".repeat(300);
let out =
format_magic_message_with_gate("%.200s", &Value::String(value.clone()), &str_t(), false);
assert_eq!(
out.len(),
127,
"precision wider than the bound is capped at the bound"
);
let out = format_magic_message_with_gate("%.50s", &Value::String(value), &str_t(), false);
assert_eq!(
out.len(),
50,
"precision narrower than the bound still applies"
);
}