Skip to main content

jmap_tools/pointer/
parser.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use crate::{JsonPointer, JsonPointerItem, Key, Property};
8
9enum TokenType {
10    Unknown,
11    Number,
12    String,
13    Wildcard,
14    Escaped,
15}
16
17struct State<P: Property> {
18    num: u64,
19    buf: Vec<u8>,
20    token: TokenType,
21    start_pos: usize,
22    path: Vec<JsonPointerItem<P>>,
23}
24
25impl<P: Property> JsonPointer<P> {
26    pub fn parse(value: &str) -> Self {
27        let mut state = State {
28            num: 0,
29            buf: Vec::new(),
30            token: TokenType::Unknown,
31            start_pos: 0,
32            path: Vec::new(),
33        };
34        let value = value.as_bytes();
35        let mut iter = value.iter().enumerate();
36
37        while let Some((pos, &ch)) = iter.next() {
38            match (ch, &state.token) {
39                (b'0'..=b'9', TokenType::Unknown | TokenType::Number) => {
40                    state.num = state
41                        .num
42                        .saturating_mul(10)
43                        .saturating_add((ch - b'0') as u64);
44                    state.token = TokenType::Number;
45                }
46                (b'*', TokenType::Unknown) => {
47                    state.token = TokenType::Wildcard;
48                }
49                (b'0', TokenType::Escaped) => {
50                    state.buf.push(b'~');
51                    state.token = TokenType::String;
52                }
53                (b'1', TokenType::Escaped) => {
54                    state.buf.push(b'/');
55                    state.token = TokenType::String;
56                }
57                (b'/', _) => {
58                    state.process(&value[state.start_pos..pos]);
59                    state.token = TokenType::Unknown;
60                    state.start_pos = pos + 1;
61                }
62                (_, _) => {
63                    if matches!(&state.token, TokenType::Number | TokenType::Wildcard)
64                        && pos > state.start_pos
65                    {
66                        state
67                            .buf
68                            .extend_from_slice(value.get(state.start_pos..pos).unwrap_or_default());
69                    }
70
71                    state.token = match ch {
72                        b'~' if !matches!(&state.token, TokenType::Escaped) => TokenType::Escaped,
73                        b'\\' => {
74                            state
75                                .buf
76                                .push(iter.next().map(|(_, &ch)| ch).unwrap_or(b'\\'));
77                            TokenType::String
78                        }
79                        _ => {
80                            state.buf.push(ch);
81                            TokenType::String
82                        }
83                    };
84                }
85            }
86        }
87
88        state.process(value.get(state.start_pos..).unwrap_or_default());
89
90        if state.path.is_empty() {
91            state.path.push(JsonPointerItem::Root);
92        }
93
94        JsonPointer(state.path)
95    }
96}
97
98impl<P: Property> State<P> {
99    pub fn process(&mut self, token_bytes: &[u8]) {
100        match self.token {
101            TokenType::String => {
102                let item = std::str::from_utf8(&self.buf).unwrap_or_default();
103                match P::try_parse(self.path.last().and_then(|item| item.as_key()), item) {
104                    Some(prop) => {
105                        self.path.push(JsonPointerItem::Key(Key::Property(prop)));
106                    }
107                    None => {
108                        self.path
109                            .push(JsonPointerItem::Key(Key::Owned(item.to_string())));
110                    }
111                }
112
113                self.buf.clear();
114            }
115            TokenType::Number => {
116                let item = std::str::from_utf8(token_bytes).unwrap_or_default();
117                match P::try_parse(self.path.last().and_then(|item| item.as_key()), item) {
118                    Some(prop) => {
119                        self.path.push(JsonPointerItem::Key(Key::Property(prop)));
120                    }
121                    None => {
122                        self.path.push(JsonPointerItem::Number(self.num));
123                    }
124                }
125                self.num = 0;
126            }
127            TokenType::Wildcard => {
128                self.path.push(JsonPointerItem::Wildcard);
129            }
130            TokenType::Unknown if self.start_pos > 0 => {
131                self.path.push(JsonPointerItem::Key("".into()));
132            }
133            _ => (),
134        }
135    }
136}
137
138impl<'de, P: Property> serde::Deserialize<'de> for JsonPointer<P> {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: serde::Deserializer<'de>,
142    {
143        <&str>::deserialize(deserializer).map(|s| JsonPointer::parse(s))
144    }
145}
146
147impl<P: Property> serde::Serialize for JsonPointer<P> {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: serde::Serializer,
151    {
152        serializer.serialize_str(&self.to_string())
153    }
154}
155
156#[cfg(test)]
157mod tests {
158
159    use super::{JsonPointer, JsonPointerItem};
160    use crate::{Key, Null, Property};
161    use std::borrow::Cow;
162
163    #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164    enum TestProp {
165        Ids,
166        Id(String),
167    }
168
169    impl Property for TestProp {
170        fn try_parse(key: Option<&Key<'_, Self>>, value: &str) -> Option<Self> {
171            if let Some(Key::Property(TestProp::Ids)) = key {
172                Some(TestProp::Id(value.to_string()))
173            } else if value == "ids" {
174                Some(TestProp::Ids)
175            } else {
176                None
177            }
178        }
179
180        fn to_cow(&self) -> Cow<'static, str> {
181            match self {
182                TestProp::Ids => Cow::Borrowed("ids"),
183                TestProp::Id(s) => Cow::Owned(s.clone()),
184            }
185        }
186    }
187
188    #[test]
189    fn json_pointer_parse() {
190        for (input, output) in vec![
191            ("hello", vec![JsonPointerItem::<Null>::Key("hello".into())]),
192            ("9a", vec![JsonPointerItem::Key("9a".into())]),
193            ("a9", vec![JsonPointerItem::Key("a9".into())]),
194            ("*a", vec![JsonPointerItem::Key("*a".into())]),
195            (
196                "/hello/world",
197                vec![
198                    JsonPointerItem::Key("hello".into()),
199                    JsonPointerItem::Key("world".into()),
200                ],
201            ),
202            ("*", vec![JsonPointerItem::Wildcard]),
203            (
204                "/hello/*",
205                vec![
206                    JsonPointerItem::Key("hello".into()),
207                    JsonPointerItem::Wildcard,
208                ],
209            ),
210            ("1234", vec![JsonPointerItem::Number(1234)]),
211            (
212                "/hello/1234",
213                vec![
214                    JsonPointerItem::Key("hello".into()),
215                    JsonPointerItem::Number(1234),
216                ],
217            ),
218            ("~0~1", vec![JsonPointerItem::Key("~/".into())]),
219            (
220                "/hello/~0~1",
221                vec![
222                    JsonPointerItem::Key("hello".into()),
223                    JsonPointerItem::Key("~/".into()),
224                ],
225            ),
226            (
227                "/hello/1~0~1/*~1~0",
228                vec![
229                    JsonPointerItem::Key("hello".into()),
230                    JsonPointerItem::Key("1~/".into()),
231                    JsonPointerItem::Key("*/~".into()),
232                ],
233            ),
234            (
235                "/hello/world/*/99",
236                vec![
237                    JsonPointerItem::Key("hello".into()),
238                    JsonPointerItem::Key("world".into()),
239                    JsonPointerItem::Wildcard,
240                    JsonPointerItem::Number(99),
241                ],
242            ),
243            ("/", vec![JsonPointerItem::Key("".into())]),
244            (
245                "///",
246                vec![
247                    JsonPointerItem::Key("".into()),
248                    JsonPointerItem::Key("".into()),
249                    JsonPointerItem::Key("".into()),
250                ],
251            ),
252            ("", vec![JsonPointerItem::Root]),
253        ] {
254            assert_eq!(JsonPointer::parse(input).0, output, "{input}");
255        }
256    }
257
258    #[test]
259    fn json_pointer_parse_promotes_digit() {
260        let pointer = JsonPointer::<TestProp>::parse("ids/2");
261        assert_eq!(
262            pointer.0,
263            vec![
264                JsonPointerItem::Key(Key::Property(TestProp::Ids)),
265                JsonPointerItem::Key(Key::Property(TestProp::Id("2".to_string()))),
266            ]
267        );
268
269        let pointer = JsonPointer::<TestProp>::parse("ids/abc");
270        assert_eq!(
271            pointer.0,
272            vec![
273                JsonPointerItem::Key(Key::Property(TestProp::Ids)),
274                JsonPointerItem::Key(Key::Property(TestProp::Id("abc".to_string()))),
275            ]
276        );
277
278        let pointer = JsonPointer::<TestProp>::parse("other/2");
279        assert_eq!(
280            pointer.0,
281            vec![
282                JsonPointerItem::Key(Key::Owned("other".to_string())),
283                JsonPointerItem::Number(2),
284            ]
285        );
286    }
287}