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/// Parses the display hint (e.g. the `ipv4` in `{:ipv4}`).
56fn parse_display_hint(s: &str) -> Result<DisplayHint, String> {
57    Ok(match s {
58        "p" | "x" => DisplayHint::LowerHex,
59        "X" => DisplayHint::UpperHex,
60        "i" => DisplayHint::Ip,
61        "mac" => DisplayHint::LowerMac,
62        "MAC" => DisplayHint::UpperMac,
63        _ => return Err(format!("unknown display hint: {s:?}")),
64    })
65}
66
67/// Parse `Param` from the given `&str` which can specify an optional format
68/// like `:x` or `:ipv4` (without curly braces, which are parsed by the `parse`
69/// function).
70fn parse_param(mut input: &str) -> Result<Parameter, String> {
71    const HINT_PREFIX: &str = ":";
72
73    // Then, optional hint
74    let mut hint = DisplayHint::Default;
75
76    if input.starts_with(HINT_PREFIX) {
77        // skip the prefix
78        input = &input[HINT_PREFIX.len()..];
79        if input.is_empty() {
80            return Err("malformed format string (missing display hint after ':')".into());
81        }
82
83        hint = parse_display_hint(input)?;
84    } else if !input.is_empty() {
85        return Err(format!("unexpected content {input:?} in format string"));
86    }
87
88    Ok(Parameter { hint })
89}
90
91/// Parses the given format string into string literals and parameters specified
92/// by curly braces (with optional format hints like `:x` or `:ipv4`).
93pub fn parse(format_string: &str) -> Result<Vec<Fragment>, String> {
94    let mut fragments = Vec::new();
95
96    // Index after the `}` of the last format specifier.
97    let mut end_pos = 0;
98
99    let mut chars = format_string.char_indices();
100    while let Some((brace_pos, ch)) = chars.next() {
101        if ch != '{' {
102            // Part of a literal fragment.
103            continue;
104        }
105
106        // Peek at the next char.
107        if chars.as_str().starts_with('{') {
108            // Escaped `{{`, also part of a literal fragment.
109            chars.next();
110            continue;
111        }
112
113        if brace_pos > end_pos {
114            // There's a literal fragment with at least 1 character before this
115            // parameter fragment.
116            let unescaped_literal = &format_string[end_pos..brace_pos];
117            push_literal(&mut fragments, unescaped_literal)?;
118        }
119
120        // Else, this is a format specifier. It ends at the next `}`.
121        let len = chars
122            .as_str()
123            .find('}')
124            .ok_or("missing `}` in format string")?;
125        end_pos = brace_pos + 1 + len + 1;
126
127        // Parse the contents inside the braces.
128        let param_str = &format_string[brace_pos + 1..][..len];
129        let param = parse_param(param_str)?;
130        fragments.push(Fragment::Parameter(param));
131    }
132
133    // Trailing literal.
134    if end_pos != format_string.len() {
135        push_literal(&mut fragments, &format_string[end_pos..])?;
136    }
137
138    Ok(fragments)
139}
140
141#[cfg(test)]
142mod test {
143    use super::*;
144
145    #[test]
146    fn test_parse() {
147        assert_eq!(
148            parse("foo {} bar {:x} test {:X} ayy {:i} lmao {{}} {{something}} {:p}"),
149            Ok(vec![
150                Fragment::Literal("foo ".into()),
151                Fragment::Parameter(Parameter {
152                    hint: DisplayHint::Default
153                }),
154                Fragment::Literal(" bar ".into()),
155                Fragment::Parameter(Parameter {
156                    hint: DisplayHint::LowerHex
157                }),
158                Fragment::Literal(" test ".into()),
159                Fragment::Parameter(Parameter {
160                    hint: DisplayHint::UpperHex
161                }),
162                Fragment::Literal(" ayy ".into()),
163                Fragment::Parameter(Parameter {
164                    hint: DisplayHint::Ip
165                }),
166                Fragment::Literal(" lmao {} {something} ".into()),
167                Fragment::Parameter(Parameter {
168                    hint: DisplayHint::LowerHex
169                }),
170            ])
171        );
172        assert!(parse("foo {:}").is_err());
173        assert!(parse("foo { bar").is_err());
174        assert!(parse("foo } bar").is_err());
175        assert!(parse("foo { bar }").is_err());
176    }
177}