Skip to main content

aya_log_parser/
lib.rs

1use std::str;
2
3use aya_log_common::DisplayHint;
4
5/// A parsed formatting parameter (contents of `{` `}` block).
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Parameter {
8    /// The display hint, e.g. ':ipv4', ':x'.
9    pub hint: DisplayHint,
10}
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum Fragment {
14    /// A literal string (eg. `"literal "` in `"literal {}"`).
15    Literal(String),
16
17    /// A format parameter.
18    Parameter(Parameter),
19}
20
21fn push_literal(frag: &mut Vec<Fragment>, unescaped_literal: &str) -> Result<(), String> {
22    // Replace `{{` with `{` and `}}` with `}`. Single braces are errors.
23
24    // Scan for single braces first. The rest is trivial.
25    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    // Handle trailing unescaped `{` or `}`.
43    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
55/// Parse `Param` from the given `&str` which can specify an optional format
56/// like `:x` or `:ipv4` (without curly braces, which are parsed by the `parse`
57/// function).
58fn 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
79/// Parses the given format string into string literals and parameters specified
80/// by curly braces (with optional format hints like `:x` or `:ipv4`).
81pub fn parse(format_string: &str) -> Result<Vec<Fragment>, String> {
82    let mut fragments = Vec::new();
83
84    // Index after the `}` of the last format specifier.
85    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            // Part of a literal fragment.
91            continue;
92        }
93
94        // Peek at the next char.
95        if chars.as_str().starts_with('{') {
96            // Escaped `{{`, also part of a literal fragment.
97            chars.next();
98            continue;
99        }
100
101        if brace_pos > end_pos {
102            // There's a literal fragment with at least 1 character before this
103            // parameter fragment.
104            let unescaped_literal = &format_string[end_pos..brace_pos];
105            push_literal(&mut fragments, unescaped_literal)?;
106        }
107
108        // Else, this is a format specifier. It ends at the next `}`.
109        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        // Parse the contents inside the braces.
116        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    // Trailing literal.
122    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}