use std::str;
use aya_log_common::DisplayHint;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Parameter {
pub hint: DisplayHint,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Fragment {
Literal(String),
Parameter(Parameter),
}
fn push_literal(frag: &mut Vec<Fragment>, unescaped_literal: &str) -> Result<(), String> {
let mut last_open = false;
let mut last_close = false;
for c in unescaped_literal.chars() {
match c {
'{' => last_open = !last_open,
'}' => last_close = !last_close,
_ => {
if last_open {
return Err("unmatched `{` in format string".into());
}
if last_close {
return Err("unmatched `}` in format string".into());
}
}
}
}
if last_open {
return Err("unmatched `{` in format string".into());
}
if last_close {
return Err("unmatched `}` in format string".into());
}
let literal = unescaped_literal.replace("{{", "{").replace("}}", "}");
frag.push(Fragment::Literal(literal));
Ok(())
}
fn parse_param(input: &str) -> Result<Parameter, String> {
let hint = if let Some(input) = input.strip_prefix(":") {
match input {
"" => return Err("malformed format string (missing display hint after ':')".into()),
"x" => DisplayHint::LowerHex,
"X" => DisplayHint::UpperHex,
"i" => DisplayHint::Ip,
"mac" => DisplayHint::LowerMac,
"MAC" => DisplayHint::UpperMac,
"p" => DisplayHint::Pointer,
input => return Err(format!("unknown display hint: {input:?}")),
}
} else {
if !input.is_empty() {
return Err(format!("unexpected content {input:?} in format string"));
}
DisplayHint::Default
};
Ok(Parameter { hint })
}
pub fn parse(format_string: &str) -> Result<Vec<Fragment>, String> {
let mut fragments = Vec::new();
let mut end_pos = 0;
let mut chars = format_string.char_indices();
while let Some((brace_pos, ch)) = chars.next() {
if ch != '{' {
continue;
}
if chars.as_str().starts_with('{') {
chars.next();
continue;
}
if brace_pos > end_pos {
let unescaped_literal = &format_string[end_pos..brace_pos];
push_literal(&mut fragments, unescaped_literal)?;
}
let len = chars
.as_str()
.find('}')
.ok_or("missing `}` in format string")?;
end_pos = brace_pos + 1 + len + 1;
let param_str = &format_string[brace_pos + 1..][..len];
let param = parse_param(param_str)?;
fragments.push(Fragment::Parameter(param));
}
if end_pos != format_string.len() {
push_literal(&mut fragments, &format_string[end_pos..])?;
}
Ok(fragments)
}
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
use super::*;
#[expect(
clippy::literal_string_with_formatting_args,
reason = "that's the point"
)]
#[test]
fn test_parse() {
assert_eq!(
parse("foo {} bar {:x} test {:X} ayy {:i} lmao {{}} {{something}} {:p}"),
Ok(vec![
Fragment::Literal("foo ".into()),
Fragment::Parameter(Parameter {
hint: DisplayHint::Default
}),
Fragment::Literal(" bar ".into()),
Fragment::Parameter(Parameter {
hint: DisplayHint::LowerHex
}),
Fragment::Literal(" test ".into()),
Fragment::Parameter(Parameter {
hint: DisplayHint::UpperHex
}),
Fragment::Literal(" ayy ".into()),
Fragment::Parameter(Parameter {
hint: DisplayHint::Ip
}),
Fragment::Literal(" lmao {} {something} ".into()),
Fragment::Parameter(Parameter {
hint: DisplayHint::Pointer
}),
])
);
assert_matches!(parse("foo {:}"), Err(_));
assert_matches!(parse("foo { bar"), Err(_));
assert_matches!(parse("foo } bar"), Err(_));
assert_matches!(parse("foo { bar }"), Err(_));
}
}