Skip to main content

laser_wire/filter/
path.rs

1use crate::error::InvalidError;
2use crate::limits::{MAX_FILTER_PATH_BYTES, MAX_FILTER_PATH_SEGMENTS};
3use serde::de::Error as _;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6
7/// One step of a [`FieldPath`]: an object key or an array index.
8#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub enum PathSegment {
10    Key(String),
11    Index(u32),
12}
13
14/// A validated location inside a decoded payload.
15///
16/// The text form separates object keys with `.` and selects array elements
17/// with `[n]`, so `after.ground_stations[0]` reads the first ground station. A
18/// literal `.`, `[`, `]`, or `\` inside a key is escaped with `\`. A digit-only
19/// key stays a key (`codes.200`), only brackets select an index. There is one
20/// text form per path, so the rendered string is also the canonical encoding
21/// the filter digest covers.
22#[derive(Clone, Debug, PartialEq, Eq, Hash)]
23pub struct FieldPath {
24    segments: Vec<PathSegment>,
25}
26
27impl FieldPath {
28    /// Parse the text form. Rejects empty keys, unterminated escapes or
29    /// brackets, non-canonical indexes, and paths over the segment or byte caps.
30    pub fn parse(text: &str) -> Result<Self, InvalidError> {
31        if text.is_empty() {
32            return Err(InvalidError::new("field path must not be empty"));
33        }
34        if text.len() > MAX_FILTER_PATH_BYTES {
35            return Err(InvalidError::new(format!(
36                "field path is {}B, exceeds cap {MAX_FILTER_PATH_BYTES}B",
37                text.len()
38            )));
39        }
40        let mut parser = PathParser {
41            bytes: text.as_bytes(),
42            position: 0,
43            text,
44        };
45        let segments = parser.segments()?;
46        if segments.len() > MAX_FILTER_PATH_SEGMENTS {
47            return Err(InvalidError::new(format!(
48                "field path `{text}` has {} segments, exceeds cap {MAX_FILTER_PATH_SEGMENTS}",
49                segments.len()
50            )));
51        }
52        Ok(Self { segments })
53    }
54
55    pub fn segments(&self) -> &[PathSegment] {
56        &self.segments
57    }
58}
59
60impl fmt::Display for FieldPath {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        for (position, segment) in self.segments.iter().enumerate() {
63            match segment {
64                PathSegment::Key(key) => {
65                    if position > 0 {
66                        formatter.write_str(".")?;
67                    }
68                    for character in key.chars() {
69                        if matches!(character, '.' | '[' | ']' | '\\') {
70                            formatter.write_str("\\")?;
71                        }
72                        write!(formatter, "{character}")?;
73                    }
74                }
75                PathSegment::Index(index) => write!(formatter, "[{index}]")?,
76            }
77        }
78        Ok(())
79    }
80}
81
82impl std::str::FromStr for FieldPath {
83    type Err = InvalidError;
84
85    fn from_str(text: &str) -> Result<Self, Self::Err> {
86        Self::parse(text)
87    }
88}
89
90impl Serialize for FieldPath {
91    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
92        serializer.collect_str(self)
93    }
94}
95
96impl<'de> Deserialize<'de> for FieldPath {
97    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
98        let text = String::deserialize(deserializer)?;
99        Self::parse(&text).map_err(D::Error::custom)
100    }
101}
102
103struct PathParser<'a> {
104    bytes: &'a [u8],
105    position: usize,
106    text: &'a str,
107}
108
109impl PathParser<'_> {
110    fn segments(&mut self) -> Result<Vec<PathSegment>, InvalidError> {
111        let mut segments = Vec::new();
112        if self.peek() == Some(b'[') {
113            segments.push(self.index()?);
114        } else {
115            segments.push(self.key()?);
116        }
117        while let Some(byte) = self.peek() {
118            match byte {
119                b'.' => {
120                    self.position += 1;
121                    segments.push(self.key()?);
122                }
123                b'[' => segments.push(self.index()?),
124                _ => return Err(self.invalid("expected `.` or `[` after a segment")),
125            }
126        }
127        Ok(segments)
128    }
129
130    fn key(&mut self) -> Result<PathSegment, InvalidError> {
131        let mut key = String::new();
132        while let Some(byte) = self.peek() {
133            match byte {
134                b'.' | b'[' => break,
135                b']' => return Err(self.invalid("unescaped `]` inside a key")),
136                b'\\' => {
137                    self.position += 1;
138                    match self.peek() {
139                        Some(escaped @ (b'.' | b'[' | b']' | b'\\')) => {
140                            key.push(char::from(escaped));
141                            self.position += 1;
142                        }
143                        _ => return Err(self.invalid("`\\` must escape `.`, `[`, `]`, or `\\`")),
144                    }
145                }
146                _ => {
147                    let rest = &self.text[self.position..];
148                    let character = rest
149                        .chars()
150                        .next()
151                        .expect("a peeked byte starts a character");
152                    key.push(character);
153                    self.position += character.len_utf8();
154                }
155            }
156        }
157        if key.is_empty() {
158            return Err(self.invalid("empty key"));
159        }
160        Ok(PathSegment::Key(key))
161    }
162
163    fn index(&mut self) -> Result<PathSegment, InvalidError> {
164        self.position += 1;
165        let start = self.position;
166        while self.peek().is_some_and(|byte| byte.is_ascii_digit()) {
167            self.position += 1;
168        }
169        let digits = &self.text[start..self.position];
170        if self.peek() != Some(b']') {
171            return Err(self.invalid("an index must be digits closed by `]`"));
172        }
173        self.position += 1;
174        if digits.is_empty() || (digits.len() > 1 && digits.starts_with('0')) {
175            return Err(self.invalid("an index must be a canonical decimal number"));
176        }
177        digits
178            .parse::<u32>()
179            .map(PathSegment::Index)
180            .map_err(|_| self.invalid("an index must fit in 32 bits"))
181    }
182
183    fn peek(&self) -> Option<u8> {
184        self.bytes.get(self.position).copied()
185    }
186
187    fn invalid(&self, reason: &str) -> InvalidError {
188        InvalidError::new(format!(
189            "field path `{}` is invalid at byte {}: {reason}",
190            self.text, self.position
191        ))
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn round_trip(text: &str) -> Vec<PathSegment> {
200        let path = FieldPath::parse(text).expect("the path parses");
201        assert_eq!(path.to_string(), text, "the text form is canonical");
202        path.segments().to_vec()
203    }
204
205    #[test]
206    fn given_dotted_keys_when_parsed_then_should_split_on_dots() {
207        assert_eq!(
208            round_trip("after.orbit.kind"),
209            vec![
210                PathSegment::Key("after".to_owned()),
211                PathSegment::Key("orbit".to_owned()),
212                PathSegment::Key("kind".to_owned()),
213            ]
214        );
215    }
216
217    #[test]
218    fn given_brackets_when_parsed_then_should_select_indexes() {
219        assert_eq!(
220            round_trip("[0].ground_stations[12]"),
221            vec![
222                PathSegment::Index(0),
223                PathSegment::Key("ground_stations".to_owned()),
224                PathSegment::Index(12),
225            ]
226        );
227    }
228
229    #[test]
230    fn given_a_digit_key_when_parsed_then_should_stay_a_key() {
231        assert_eq!(
232            round_trip("codes.200"),
233            vec![
234                PathSegment::Key("codes".to_owned()),
235                PathSegment::Key("200".to_owned()),
236            ]
237        );
238    }
239
240    #[test]
241    fn given_escaped_characters_when_parsed_then_should_keep_them_in_the_key() {
242        assert_eq!(
243            round_trip(r"a\.b.c\[d\]\\"),
244            vec![
245                PathSegment::Key("a.b".to_owned()),
246                PathSegment::Key(r"c[d]\".to_owned()),
247            ]
248        );
249    }
250
251    #[test]
252    fn given_malformed_paths_when_parsed_then_should_be_rejected() {
253        for text in [
254            "",
255            "a..b",
256            ".a",
257            "a.",
258            "a[",
259            "a[]",
260            "a[01]",
261            "a[x]",
262            "a]",
263            r"a\b",
264            r"a\",
265            "a[1]b",
266            "a[4294967296]",
267        ] {
268            assert!(FieldPath::parse(text).is_err(), "`{text}` must be rejected");
269        }
270    }
271
272    #[test]
273    fn given_too_many_segments_when_parsed_then_should_be_rejected() {
274        let text = vec!["a"; MAX_FILTER_PATH_SEGMENTS + 1].join(".");
275        assert!(FieldPath::parse(&text).is_err());
276        let text = vec!["a"; MAX_FILTER_PATH_SEGMENTS].join(".");
277        assert!(FieldPath::parse(&text).is_ok());
278    }
279
280    #[test]
281    fn given_a_path_when_serialized_then_should_be_its_text_form() {
282        let path = FieldPath::parse("fields.mode").expect("parses");
283        let json = serde_json::to_string(&path).expect("serializes");
284        assert_eq!(json, r#""fields.mode""#);
285        let back: FieldPath = serde_json::from_str(&json).expect("deserializes");
286        assert_eq!(back, path);
287        assert!(serde_json::from_str::<FieldPath>(r#""a..b""#).is_err());
288    }
289}