use log::debug;
use crate::parser::ast::{TypeKind, Value};
#[must_use]
pub fn format_magic_message(template: &str, value: &Value, type_kind: &TypeKind) -> String {
format_magic_message_with_gate(template, value, type_kind, false)
}
#[must_use]
#[allow(clippy::indexing_slicing)]
pub fn format_magic_message_with_gate(
template: &str,
value: &Value,
type_kind: &TypeKind,
stop_at_newline: bool,
) -> 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, stop_at_newline) {
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,
precision: Option<usize>,
conv: Conv,
end: usize,
}
const MAX_FORMAT_WIDTH: usize = 4096;
pub(crate) const MAX_DESCRIPTION_FIELD_LEN: usize = 127;
#[allow(clippy::indexing_slicing)]
fn bounded_newline_cut(bytes: &[u8], stop_at_newline: bool) -> usize {
let mut cut = bytes.len().min(MAX_DESCRIPTION_FIELD_LEN);
if stop_at_newline
&& let Some(newline_pos) = bytes[..cut].iter().position(|&b| b == b'\r' || b == b'\n')
{
cut = newline_pos;
}
cut
}
#[allow(clippy::indexing_slicing)]
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;
}
let mut precision: Option<usize> = None;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
let mut prec: usize = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
let digit = (bytes[i] - b'0') as usize;
prec = prec
.saturating_mul(10)
.saturating_add(digit)
.min(MAX_FORMAT_WIDTH);
i += 1;
}
precision = Some(prec);
}
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,
precision,
conv,
end: i,
})
}
fn render(
spec: &Spec,
value: &Value,
type_kind: &TypeKind,
stop_at_newline: bool,
) -> Option<String> {
match spec.conv {
Conv::Percent => Some("%".to_string()),
Conv::Str => Some(render_str_spec(spec, value, type_kind, stop_at_newline)),
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 is_bounded_string_family(type_kind: &TypeKind) -> bool {
matches!(
type_kind,
TypeKind::String { .. } | TypeKind::String16 { .. } | TypeKind::PString { .. }
)
}
#[allow(clippy::indexing_slicing)]
fn bound_description_field(base: &str, stop_at_newline: bool) -> String {
let mut cut = bounded_newline_cut(base.as_bytes(), stop_at_newline);
while cut > 0 && !base.is_char_boundary(cut) {
cut -= 1;
}
base[..cut].to_string()
}
#[allow(clippy::indexing_slicing)]
fn bound_bytes(bytes: &[u8], stop_at_newline: bool) -> Vec<u8> {
let mut cut = bounded_newline_cut(bytes, stop_at_newline);
while cut > 0
&& bytes
.get(cut)
.is_some_and(|&b| b & 0b1100_0000 == 0b1000_0000)
{
cut -= 1;
}
bytes[..cut].to_vec()
}
fn render_string_bounded(value: &Value, apply_bound: bool, stop_at_newline: bool) -> String {
match value {
Value::String(s) if apply_bound => bound_description_field(s, stop_at_newline),
Value::String(s) => s.clone(),
Value::Bytes(b) if apply_bound => {
String::from_utf8_lossy(&bound_bytes(b, stop_at_newline)).into_owned()
}
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(),
}
}
fn render_str_spec(
spec: &Spec,
value: &Value,
type_kind: &TypeKind,
stop_at_newline: bool,
) -> String {
let apply_bound = is_bounded_string_family(type_kind);
let bounded = render_string_bounded(value, apply_bound, stop_at_newline);
let truncated = match spec.precision {
Some(p) => bounded.chars().take(p).collect::<String>(),
None => bounded,
};
pad_non_numeric(&truncated, spec)
}
#[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;