Skip to main content

saddle_core/
json_pointer.rs

1//! RFC 6901 selectors shared by context APIs and logging configuration.
2//! Validation and matching borrow the selector; they allocate no decoded keys.
3
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct InvalidJsonPointer;
6
7#[derive(Clone, Copy, Debug)]
8pub struct JsonPointer<'a>(&'a str);
9
10impl<'a> JsonPointer<'a> {
11    pub fn parse(path: &'a str) -> Result<Self, InvalidJsonPointer> {
12        if !path.is_empty() && !path.starts_with('/') {
13            return Err(InvalidJsonPointer);
14        }
15        let mut bytes = path.bytes();
16        while let Some(byte) = bytes.next() {
17            if byte == b'~' && !matches!(bytes.next(), Some(b'0' | b'1')) {
18                return Err(InvalidJsonPointer);
19            }
20        }
21        Ok(Self(path))
22    }
23
24    pub fn segments(self) -> impl Iterator<Item = PointerSegment<'a>> {
25        self.0.split('/').skip(1).map(PointerSegment)
26    }
27}
28
29#[derive(Clone, Copy)]
30pub struct PointerSegment<'a>(&'a str);
31
32impl PointerSegment<'_> {
33    pub fn decode_into<'a>(self, target: &'a mut [u8]) -> Result<&'a str, InvalidJsonPointer> {
34        let mut chars = self.0.chars();
35        let mut used = 0;
36        while let Some(ch) = chars.next() {
37            let ch = if ch == '~' {
38                match chars.next() { Some('0') => '~', Some('1') => '/', _ => return Err(InvalidJsonPointer) }
39            } else { ch };
40            let mut bytes = [0; 4];
41            let bytes = ch.encode_utf8(&mut bytes).as_bytes();
42            let end = used + bytes.len();
43            target.get_mut(used..end).ok_or(InvalidJsonPointer)?.copy_from_slice(bytes);
44            used = end;
45        }
46        std::str::from_utf8(&target[..used]).map_err(|_| InvalidJsonPointer)
47    }
48    pub fn matches(self, key: &str) -> bool {
49        let mut encoded = self.0.chars();
50        let decoded = std::iter::from_fn(move || {
51            encoded.next().map(|ch| {
52                if ch == '~' {
53                    match encoded.next() {
54                        Some('0') => '~',
55                        Some('1') => '/',
56                        _ => unreachable!("validated JSON Pointer"),
57                    }
58                } else {
59                    ch
60                }
61            })
62        });
63        decoded.eq(key.chars())
64    }
65
66    pub fn array_index(self) -> Option<usize> {
67        let raw = self.0;
68        if raw.is_empty() || (raw.len() > 1 && raw.starts_with('0'))
69            || !raw.bytes().all(|b| b.is_ascii_digit()) {
70            return None;
71        }
72        raw.parse().ok()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn escaped_keys_empty_keys_and_canonical_array_indices() {
82        let path = JsonPointer::parse("/a~1b/~0/界//0").unwrap();
83        for (segment, key) in path.segments().zip(["a/b", "~", "界", "", "0"]) {
84            assert!(segment.matches(key));
85        }
86        assert_eq!(JsonPointer::parse("").unwrap().segments().count(), 0);
87        for path in ["a", "/~", "/~2", "/~~0"] {
88            assert!(JsonPointer::parse(path).is_err());
89        }
90        for key in ["00", "01", "-", "-1", "+1", "", "184467440737095516160"] {
91            assert!(PointerSegment(key).array_index().is_none());
92        }
93        assert_eq!(PointerSegment("0").array_index(), Some(0));
94        assert_eq!(PointerSegment("12").array_index(), Some(12));
95    }
96}