1use std::str;
2
3use aya_log_common::DisplayHint;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Parameter {
8 pub hint: DisplayHint,
10}
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum Fragment {
14 Literal(String),
16
17 Parameter(Parameter),
19}
20
21fn push_literal(frag: &mut Vec<Fragment>, unescaped_literal: &str) -> Result<(), String> {
22 let mut last_open = false;
26 let mut last_close = false;
27 for c in unescaped_literal.chars() {
28 match c {
29 '{' => last_open = !last_open,
30 '}' => last_close = !last_close,
31 _ => {
32 if last_open {
33 return Err("unmatched `{` in format string".into());
34 }
35 if last_close {
36 return Err("unmatched `}` in format string".into());
37 }
38 }
39 }
40 }
41
42 if last_open {
44 return Err("unmatched `{` in format string".into());
45 }
46 if last_close {
47 return Err("unmatched `}` in format string".into());
48 }
49
50 let literal = unescaped_literal.replace("{{", "{").replace("}}", "}");
51 frag.push(Fragment::Literal(literal));
52 Ok(())
53}
54
55fn parse_param(input: &str) -> Result<Parameter, String> {
59 let hint = if let Some(input) = input.strip_prefix(":") {
60 match input {
61 "" => return Err("malformed format string (missing display hint after ':')".into()),
62 "x" => DisplayHint::LowerHex,
63 "X" => DisplayHint::UpperHex,
64 "i" => DisplayHint::Ip,
65 "mac" => DisplayHint::LowerMac,
66 "MAC" => DisplayHint::UpperMac,
67 "p" => DisplayHint::Pointer,
68 input => return Err(format!("unknown display hint: {input:?}")),
69 }
70 } else {
71 if !input.is_empty() {
72 return Err(format!("unexpected content {input:?} in format string"));
73 }
74 DisplayHint::Default
75 };
76 Ok(Parameter { hint })
77}
78
79pub fn parse(format_string: &str) -> Result<Vec<Fragment>, String> {
82 let mut fragments = Vec::new();
83
84 let mut end_pos = 0;
86
87 let mut chars = format_string.char_indices();
88 while let Some((brace_pos, ch)) = chars.next() {
89 if ch != '{' {
90 continue;
92 }
93
94 if chars.as_str().starts_with('{') {
96 chars.next();
98 continue;
99 }
100
101 if brace_pos > end_pos {
102 let unescaped_literal = &format_string[end_pos..brace_pos];
105 push_literal(&mut fragments, unescaped_literal)?;
106 }
107
108 let len = chars
110 .as_str()
111 .find('}')
112 .ok_or("missing `}` in format string")?;
113 end_pos = brace_pos + 1 + len + 1;
114
115 let param_str = &format_string[brace_pos + 1..][..len];
117 let param = parse_param(param_str)?;
118 fragments.push(Fragment::Parameter(param));
119 }
120
121 if end_pos != format_string.len() {
123 push_literal(&mut fragments, &format_string[end_pos..])?;
124 }
125
126 Ok(fragments)
127}
128
129#[cfg(test)]
130mod test {
131 use assert_matches::assert_matches;
132
133 use super::*;
134
135 #[expect(
136 clippy::literal_string_with_formatting_args,
137 reason = "that's the point"
138 )]
139 #[test]
140 fn test_parse() {
141 assert_eq!(
142 parse("foo {} bar {:x} test {:X} ayy {:i} lmao {{}} {{something}} {:p}"),
143 Ok(vec![
144 Fragment::Literal("foo ".into()),
145 Fragment::Parameter(Parameter {
146 hint: DisplayHint::Default
147 }),
148 Fragment::Literal(" bar ".into()),
149 Fragment::Parameter(Parameter {
150 hint: DisplayHint::LowerHex
151 }),
152 Fragment::Literal(" test ".into()),
153 Fragment::Parameter(Parameter {
154 hint: DisplayHint::UpperHex
155 }),
156 Fragment::Literal(" ayy ".into()),
157 Fragment::Parameter(Parameter {
158 hint: DisplayHint::Ip
159 }),
160 Fragment::Literal(" lmao {} {something} ".into()),
161 Fragment::Parameter(Parameter {
162 hint: DisplayHint::Pointer
163 }),
164 ])
165 );
166 assert_matches!(parse("foo {:}"), Err(_));
167 assert_matches!(parse("foo { bar"), Err(_));
168 assert_matches!(parse("foo } bar"), Err(_));
169 assert_matches!(parse("foo { bar }"), Err(_));
170 }
171}