use log::debug;
use crate::parser::ast::{TypeKind, Value};
#[must_use]
pub fn format_magic_message(template: &str, value: &Value, type_kind: &TypeKind) -> String {
let mut out = String::with_capacity(template.len());
let bytes = template.as_bytes();
let mut i = 0;
let mut plain_start = 0;
while i < bytes.len() {
if bytes[i] != b'%' {
i += 1;
continue;
}
if plain_start < i {
out.push_str(&template[plain_start..i]);
}
let spec_start = i;
let Some(parsed_spec) = parse_spec(bytes, i + 1) else {
debug!(
"format_magic_message: malformed specifier at byte {i} in template {template:?}; passing through remainder literally",
);
out.push_str(&template[i..]);
plain_start = bytes.len();
break;
};
let next_i = parsed_spec.end;
if let Some(rendered) = render(&parsed_spec, value, type_kind) {
out.push_str(&rendered);
} else {
let literal = &template[spec_start..next_i];
debug!(
"format_magic_message: unsupported specifier {literal:?} for value {value:?}; passing through literally",
);
out.push_str(literal);
}
i = next_i;
plain_start = i;
}
if plain_start < bytes.len() {
out.push_str(&template[plain_start..]);
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Conv {
Signed,
Unsigned,
HexLower,
HexUpper,
Octal,
Str,
Char,
Percent,
}
#[derive(Debug, Clone)]
struct Spec {
zero_pad: bool,
left_align: bool,
alt_form: bool,
width: usize,
conv: Conv,
end: usize,
}
const MAX_FORMAT_WIDTH: usize = 4096;
fn parse_spec(bytes: &[u8], start: usize) -> Option<Spec> {
let mut i = start;
let mut zero_pad = false;
let mut left_align = false;
let mut alt_form = false;
while i < bytes.len() {
match bytes[i] {
b'0' => {
zero_pad = true;
i += 1;
}
b'-' => {
left_align = true;
i += 1;
}
b'#' => {
alt_form = true;
i += 1;
}
b'+' | b' ' => {
i += 1;
}
_ => break,
}
}
let mut width: usize = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
let digit = (bytes[i] - b'0') as usize;
width = width
.saturating_mul(10)
.saturating_add(digit)
.min(MAX_FORMAT_WIDTH);
i += 1;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
while i < bytes.len() {
match bytes[i] {
b'l' | b'h' | b'j' | b'z' | b't' => i += 1,
_ => break,
}
}
if i >= bytes.len() {
return None;
}
let conv = match bytes[i] {
b'd' | b'i' => Conv::Signed,
b'u' => Conv::Unsigned,
b'x' => Conv::HexLower,
b'X' => Conv::HexUpper,
b'o' => Conv::Octal,
b's' => Conv::Str,
b'c' => Conv::Char,
b'%' => Conv::Percent,
_ => return None,
};
i += 1;
Some(Spec {
zero_pad,
left_align,
alt_form,
width,
conv,
end: i,
})
}
fn render(spec: &Spec, value: &Value, type_kind: &TypeKind) -> Option<String> {
match spec.conv {
Conv::Percent => Some("%".to_string()),
Conv::Str => Some(render_string(value)),
Conv::Signed => {
let n = coerce_to_i64(value)?;
Some(pad_numeric(&n.to_string(), spec))
}
Conv::Unsigned => {
let n = coerce_to_u64(value)?;
Some(pad_numeric(&n.to_string(), spec))
}
Conv::HexLower => {
let n = coerce_to_u64_masked(value, type_kind)?;
let prefix = if spec.alt_form && n != 0 { "0x" } else { "" };
Some(render_prefixed_int(&format!("{n:x}"), prefix, spec))
}
Conv::HexUpper => {
let n = coerce_to_u64_masked(value, type_kind)?;
let prefix = if spec.alt_form && n != 0 { "0X" } else { "" };
Some(render_prefixed_int(&format!("{n:X}"), prefix, spec))
}
Conv::Octal => {
let n = coerce_to_u64_masked(value, type_kind)?;
let prefix = if spec.alt_form && n != 0 { "0" } else { "" };
Some(render_prefixed_int(&format!("{n:o}"), prefix, spec))
}
Conv::Char => {
let n = coerce_to_u64(value)?;
let byte = u8::try_from(n).ok()?;
Some(pad_non_numeric(&char::from(byte).to_string(), spec))
}
}
}
fn render_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Bytes(b) => String::from_utf8_lossy(b).into_owned(),
Value::Uint(n) => n.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap
)]
fn coerce_to_i64(value: &Value) -> Option<i64> {
match value {
Value::Int(n) => Some(*n),
Value::Uint(n) => Some(*n as i64),
Value::Float(f) => Some(*f as i64),
Value::String(_) | Value::Bytes(_) => None,
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn coerce_to_u64(value: &Value) -> Option<u64> {
match value {
Value::Uint(n) => Some(*n),
Value::Int(n) => Some(*n as u64),
Value::Float(f) => Some(*f as u64),
Value::String(_) | Value::Bytes(_) => None,
}
}
fn coerce_to_u64_masked(value: &Value, type_kind: &TypeKind) -> Option<u64> {
let raw = coerce_to_u64(value)?;
let mask = match type_kind.bit_width() {
Some(8) => 0xff_u64,
Some(16) => 0xffff_u64,
Some(32) => 0xffff_ffff_u64,
_ => return Some(raw),
};
Some(raw & mask)
}
fn render_prefixed_int(digits: &str, prefix: &str, spec: &Spec) -> String {
let body_len = prefix.len() + digits.len();
if body_len >= spec.width {
return format!("{prefix}{digits}");
}
let pad = spec.width - body_len;
if spec.zero_pad && !spec.left_align {
let zeros: String = std::iter::repeat_n('0', pad).collect();
format!("{prefix}{zeros}{digits}")
} else if spec.left_align {
let spaces: String = std::iter::repeat_n(' ', pad).collect();
format!("{prefix}{digits}{spaces}")
} else {
let spaces: String = std::iter::repeat_n(' ', pad).collect();
format!("{spaces}{prefix}{digits}")
}
}
fn pad_non_numeric(body: &str, spec: &Spec) -> String {
if body.len() >= spec.width {
return body.to_string();
}
let pad = spec.width - body.len();
let padding: String = std::iter::repeat_n(' ', pad).collect();
if spec.left_align {
format!("{body}{padding}")
} else {
format!("{padding}{body}")
}
}
fn pad_numeric(body: &str, spec: &Spec) -> String {
if body.len() >= spec.width {
return body.to_string();
}
if spec.zero_pad
&& !spec.left_align
&& let Some(digits) = body.strip_prefix('-')
{
let needed = spec.width.saturating_sub(1 + digits.len());
if needed == 0 {
return body.to_string();
}
let zeros: String = std::iter::repeat_n('0', needed).collect();
return format!("-{zeros}{digits}");
}
let pad = spec.width - body.len();
let pad_char = if spec.zero_pad && !spec.left_align {
'0'
} else {
' '
};
let padding: String = std::iter::repeat_n(pad_char, pad).collect();
if spec.left_align {
format!("{body}{padding}")
} else {
format!("{padding}{body}")
}
}
#[cfg(test)]
mod tests {
use super::*;
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 },
);
assert_eq!(out, "hello world");
let out = format_magic_message(
"data=%s",
&Value::Bytes(b"abc".to_vec()),
&TypeKind::String { max_length: None },
);
assert_eq!(out, "data=abc");
}
#[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 },
);
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");
}
}