Skip to main content

yaml_rt_core/
pointer.rs

1use std::collections::HashSet;
2use std::fmt;
3
4use crate::{NodeId, ResolvedScalar, SemanticKind, YamlDoc, resolve_scalar};
5
6/// One decoded RFC 6901 reference token.
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct ReferenceToken(String);
9
10impl ReferenceToken {
11    /// Returns the decoded token text.
12    #[must_use]
13    pub fn as_str(&self) -> &str {
14        &self.0
15    }
16}
17
18/// A parsed plain RFC 6901 JSON Pointer.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct JsonPointer {
21    original: String,
22    tokens: Vec<ReferenceToken>,
23}
24
25impl JsonPointer {
26    /// Parses a plain JSON Pointer. URI fragment syntax is not accepted.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error when `input` is not valid RFC 6901 pointer syntax.
31    pub fn parse(input: &str) -> Result<Self, PointerError> {
32        if input.is_empty() {
33            return Ok(Self {
34                original: String::new(),
35                tokens: Vec::new(),
36            });
37        }
38        if !input.starts_with('/') {
39            return Err(PointerError::new(
40                input,
41                None,
42                PointerErrorKind::InvalidSyntax,
43                "a non-empty JSON Pointer must begin with `/`",
44            ));
45        }
46        let tokens = input[1..]
47            .split('/')
48            .enumerate()
49            .map(|(index, token)| {
50                decode_token(token).map(ReferenceToken).map_err(|escape| {
51                    PointerError::new(
52                        input,
53                        Some(index),
54                        PointerErrorKind::InvalidEscape,
55                        format!("invalid escape `{escape}`"),
56                    )
57                })
58            })
59            .collect::<Result<Vec<_>, _>>()?;
60        Ok(Self {
61            original: input.to_owned(),
62            tokens,
63        })
64    }
65
66    /// Returns the original pointer spelling.
67    #[must_use]
68    pub fn as_str(&self) -> &str {
69        &self.original
70    }
71
72    /// Returns the decoded reference tokens.
73    #[must_use]
74    pub fn tokens(&self) -> &[ReferenceToken] {
75        &self.tokens
76    }
77
78    /// Returns whether this pointer identifies the document root.
79    #[must_use]
80    pub fn is_root(&self) -> bool {
81        self.tokens.is_empty()
82    }
83
84    /// Returns whether this pointer is a proper token-prefix of `other`.
85    #[must_use]
86    pub fn is_proper_prefix_of(&self, other: &Self) -> bool {
87        self.tokens.len() < other.tokens.len() && other.tokens.starts_with(&self.tokens)
88    }
89
90    pub(crate) fn parent(&self) -> Option<(Self, &ReferenceToken)> {
91        let (last, parent) = self.tokens.split_last()?;
92        let original = encode_tokens(parent);
93        Some((
94            Self {
95                original,
96                tokens: parent.to_vec(),
97            },
98            last,
99        ))
100    }
101}
102
103impl std::str::FromStr for JsonPointer {
104    type Err = PointerError;
105
106    fn from_str(value: &str) -> Result<Self, Self::Err> {
107        Self::parse(value)
108    }
109}
110
111fn decode_token(token: &str) -> Result<String, String> {
112    let mut output = String::with_capacity(token.len());
113    let mut characters = token.chars();
114    while let Some(character) = characters.next() {
115        if character != '~' {
116            output.push(character);
117            continue;
118        }
119        match characters.next() {
120            Some('0') => output.push('~'),
121            Some('1') => output.push('/'),
122            Some(other) => return Err(format!("~{other}")),
123            None => return Err("~".to_owned()),
124        }
125    }
126    Ok(output)
127}
128
129fn encode_tokens(tokens: &[ReferenceToken]) -> String {
130    let mut pointer = String::new();
131    for token in tokens {
132        pointer.push('/');
133        pointer.push_str(&token.as_str().replace('~', "~0").replace('/', "~1"));
134    }
135    pointer
136}
137
138/// Classification of a JSON Pointer parse or resolution failure.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum PointerErrorKind {
141    /// The whole pointer has invalid syntax.
142    InvalidSyntax,
143    /// A reference token contains an invalid `~` escape.
144    InvalidEscape,
145    /// The selected YAML document has no root node.
146    EmptyDocument,
147    /// A mapping member or sequence item does not exist.
148    MissingValue,
149    /// A reference token was evaluated against a scalar.
150    TypeMismatch,
151    /// A sequence token is not a canonical unsigned index.
152    InvalidIndex,
153    /// A canonical sequence index is larger than the sequence.
154    IndexOutOfBounds,
155    /// The special `-` token was used outside an add destination.
156    DashNotAllowed,
157    /// A traversed YAML mapping contains a non-string key.
158    NonStringKey,
159    /// More than one mapping key matches the token.
160    AmbiguousKey,
161    /// An alias has no preceding anchor binding.
162    UnresolvedAlias,
163    /// An alias chain is cyclic.
164    AliasCycle,
165    /// A YAML semantic operation failed during resolution.
166    Semantic,
167}
168
169/// Structured JSON Pointer parse or resolution error.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct PointerError {
172    pointer: String,
173    token_index: Option<usize>,
174    kind: PointerErrorKind,
175    message: String,
176}
177
178impl PointerError {
179    pub(crate) fn new(
180        pointer: impl Into<String>,
181        token_index: Option<usize>,
182        kind: PointerErrorKind,
183        message: impl Into<String>,
184    ) -> Self {
185        Self {
186            pointer: pointer.into(),
187            token_index,
188            kind,
189            message: message.into(),
190        }
191    }
192
193    /// Returns the pointer associated with the failure.
194    #[must_use]
195    pub fn pointer(&self) -> &str {
196        &self.pointer
197    }
198
199    /// Returns the zero-based failing token index, when applicable.
200    #[must_use]
201    pub const fn token_index(&self) -> Option<usize> {
202        self.token_index
203    }
204
205    /// Returns the failure classification.
206    #[must_use]
207    pub const fn kind(&self) -> PointerErrorKind {
208        self.kind
209    }
210}
211
212impl fmt::Display for PointerError {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        if self.pointer.is_empty() {
215            write!(formatter, "cannot resolve document root: {}", self.message)
216        } else {
217            write!(
218                formatter,
219                "cannot resolve JSON Pointer {:?}: {}",
220                self.pointer, self.message
221            )
222        }
223    }
224}
225
226impl std::error::Error for PointerError {}
227
228#[derive(Debug, Clone, Copy)]
229pub(crate) struct MappingMatch {
230    pub(crate) value: NodeId,
231}
232
233impl YamlDoc {
234    /// Resolves a JSON Pointer against one YAML document representation graph.
235    ///
236    /// Alias nodes are traversed when another reference token remains. A
237    /// pointer that ends on an alias returns the alias occurrence itself.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error when the document, path, or selected mapping or sequence
242    /// element does not exist, or when alias traversal fails.
243    pub fn resolve_pointer(
244        &self,
245        document: usize,
246        pointer: &JsonPointer,
247    ) -> Result<NodeId, PointerError> {
248        let mut current = self
249            .document_root(document)
250            .map_err(|error| {
251                PointerError::new(
252                    pointer.as_str(),
253                    None,
254                    PointerErrorKind::Semantic,
255                    error.to_string(),
256                )
257            })?
258            .ok_or_else(|| {
259                PointerError::new(
260                    pointer.as_str(),
261                    None,
262                    PointerErrorKind::EmptyDocument,
263                    "selected YAML document has no root node",
264                )
265            })?;
266
267        for (index, token) in pointer.tokens().iter().enumerate() {
268            current = self.resolve_aliases_for_pointer(current, pointer, index)?;
269            current = match self.semantic_kind(current) {
270                Some(SemanticKind::Mapping { .. }) => {
271                    self.mapping_match(current, token, pointer, index)?
272                        .ok_or_else(|| {
273                            PointerError::new(
274                                pointer.as_str(),
275                                Some(index),
276                                PointerErrorKind::MissingValue,
277                                format!("mapping has no member {:?}", token.as_str()),
278                            )
279                        })?
280                        .value
281                }
282                Some(SemanticKind::Sequence { .. }) => {
283                    let items = self.sequence_items(current).collect::<Vec<_>>();
284                    let item_index = parse_sequence_index(token, pointer, index, false)?;
285                    *items.get(item_index).ok_or_else(|| {
286                        PointerError::new(
287                            pointer.as_str(),
288                            Some(index),
289                            PointerErrorKind::IndexOutOfBounds,
290                            format!(
291                                "sequence index {item_index} is out of bounds for length {}",
292                                items.len()
293                            ),
294                        )
295                    })?
296                }
297                Some(SemanticKind::Scalar { .. } | SemanticKind::Alias) => {
298                    return Err(PointerError::new(
299                        pointer.as_str(),
300                        Some(index),
301                        PointerErrorKind::TypeMismatch,
302                        format!(
303                            "token {:?} cannot be evaluated against a scalar",
304                            token.as_str()
305                        ),
306                    ));
307                }
308                Some(SemanticKind::Document) | None => {
309                    return Err(PointerError::new(
310                        pointer.as_str(),
311                        Some(index),
312                        PointerErrorKind::TypeMismatch,
313                        "token cannot be evaluated against this YAML node",
314                    ));
315                }
316            };
317        }
318        Ok(current)
319    }
320
321    pub(crate) fn resolve_aliases_for_pointer(
322        &self,
323        mut node: NodeId,
324        pointer: &JsonPointer,
325        token_index: usize,
326    ) -> Result<NodeId, PointerError> {
327        let mut seen = HashSet::new();
328        while matches!(self.semantic_kind(node), Some(SemanticKind::Alias)) {
329            if !seen.insert(node) {
330                return Err(PointerError::new(
331                    pointer.as_str(),
332                    Some(token_index),
333                    PointerErrorKind::AliasCycle,
334                    "cyclic alias chain",
335                ));
336            }
337            node = self.resolve_alias(node).ok_or_else(|| {
338                PointerError::new(
339                    pointer.as_str(),
340                    Some(token_index),
341                    PointerErrorKind::UnresolvedAlias,
342                    format!(
343                        "unresolved alias `*{}`",
344                        self.alias_name(node).unwrap_or_default()
345                    ),
346                )
347            })?;
348        }
349        Ok(node)
350    }
351
352    pub(crate) fn mapping_match(
353        &self,
354        mapping: NodeId,
355        token: &ReferenceToken,
356        pointer: &JsonPointer,
357        token_index: usize,
358    ) -> Result<Option<MappingMatch>, PointerError> {
359        let mut found = None;
360        for (key, value) in self.mapping_entries(mapping) {
361            let key = self.resolve_aliases_for_pointer(key, pointer, token_index)?;
362            let Some(SemanticKind::Scalar { style }) = self.semantic_kind(key) else {
363                return Err(non_string_key_error(pointer, token_index));
364            };
365            let scalar = self.scalar_value(key).map_err(|error| {
366                PointerError::new(
367                    pointer.as_str(),
368                    Some(token_index),
369                    PointerErrorKind::Semantic,
370                    error.to_string(),
371                )
372            })?;
373            let tag = self.resolved_tag(key).map_err(|error| {
374                PointerError::new(
375                    pointer.as_str(),
376                    Some(token_index),
377                    PointerErrorKind::Semantic,
378                    error.to_string(),
379                )
380            })?;
381            let resolved = resolve_scalar(&scalar, style, tag.as_deref())
382                .map_err(|_| non_string_key_error(pointer, token_index))?;
383            if resolved != ResolvedScalar::String {
384                return Err(non_string_key_error(pointer, token_index));
385            }
386            if scalar == token.as_str() {
387                if found.is_some() {
388                    return Err(PointerError::new(
389                        pointer.as_str(),
390                        Some(token_index),
391                        PointerErrorKind::AmbiguousKey,
392                        format!(
393                            "mapping contains multiple matching keys {:?}",
394                            token.as_str()
395                        ),
396                    ));
397                }
398                found = Some(MappingMatch { value });
399            }
400        }
401        Ok(found)
402    }
403}
404
405fn non_string_key_error(pointer: &JsonPointer, token_index: usize) -> PointerError {
406    PointerError::new(
407        pointer.as_str(),
408        Some(token_index),
409        PointerErrorKind::NonStringKey,
410        "cannot traverse a mapping containing non-string keys",
411    )
412}
413
414pub(crate) fn parse_sequence_index(
415    token: &ReferenceToken,
416    pointer: &JsonPointer,
417    token_index: usize,
418    allow_dash: bool,
419) -> Result<usize, PointerError> {
420    let value = token.as_str();
421    if value == "-" {
422        return if allow_dash {
423            Ok(usize::MAX)
424        } else {
425            Err(PointerError::new(
426                pointer.as_str(),
427                Some(token_index),
428                PointerErrorKind::DashNotAllowed,
429                "`-` is allowed only as the final add destination token",
430            ))
431        };
432    }
433    if value.is_empty()
434        || !value.bytes().all(|byte| byte.is_ascii_digit())
435        || value.len() > 1 && value.starts_with('0')
436    {
437        return Err(PointerError::new(
438            pointer.as_str(),
439            Some(token_index),
440            PointerErrorKind::InvalidIndex,
441            format!("{value:?} is not a canonical sequence index"),
442        ));
443    }
444    value.parse().map_err(|_| {
445        PointerError::new(
446            pointer.as_str(),
447            Some(token_index),
448            PointerErrorKind::InvalidIndex,
449            format!("sequence index {value:?} overflows this platform"),
450        )
451    })
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn parses_and_decodes_plain_json_pointers() {
460        for (input, expected) in [
461            ("", vec![]),
462            ("/foo/0", vec!["foo", "0"]),
463            ("/a~1b", vec!["a/b"]),
464            ("/m~0n", vec!["m~n"]),
465            ("/~01", vec!["~1"]),
466        ] {
467            let pointer = JsonPointer::parse(input).unwrap();
468            assert_eq!(
469                pointer
470                    .tokens()
471                    .iter()
472                    .map(ReferenceToken::as_str)
473                    .collect::<Vec<_>>(),
474                expected
475            );
476        }
477    }
478
479    #[test]
480    fn rejects_invalid_pointer_syntax() {
481        for input in ["foo", "/foo/~", "/foo/~2"] {
482            assert!(JsonPointer::parse(input).is_err(), "{input}");
483        }
484    }
485
486    #[test]
487    fn resolves_mappings_sequences_and_escaped_keys() {
488        let doc = YamlDoc::parse("'a/b':\n  - zero\n  - one\n'm~n': value\n").expect("valid YAML");
489        let pointer = JsonPointer::parse("/a~1b/1").unwrap();
490        let node = doc.resolve_pointer(0, &pointer).unwrap();
491        assert_eq!(doc.scalar_value(node).unwrap(), "one");
492        let pointer = JsonPointer::parse("/m~0n").unwrap();
493        assert_eq!(
494            doc.scalar_value(doc.resolve_pointer(0, &pointer).unwrap())
495                .unwrap(),
496            "value"
497        );
498    }
499
500    #[test]
501    fn traverses_aliases_but_returns_a_terminal_alias() {
502        let doc =
503            YamlDoc::parse("defaults: &defaults\n  timeout: 30\nservice:\n  config: *defaults\n")
504                .unwrap();
505        let terminal = doc
506            .resolve_pointer(0, &JsonPointer::parse("/service/config").unwrap())
507            .unwrap();
508        assert_eq!(doc.alias_name(terminal), Some("defaults"));
509        let traversed = doc
510            .resolve_pointer(0, &JsonPointer::parse("/service/config/timeout").unwrap())
511            .unwrap();
512        assert_eq!(doc.scalar_value(traversed).unwrap(), "30");
513    }
514
515    #[test]
516    fn rejects_noncanonical_indices_and_non_string_keys() {
517        let sequence = YamlDoc::parse("[a, b]\n").unwrap();
518        for path in ["/01", "/-1", "/+1", "/ ", "/-"] {
519            assert!(
520                sequence
521                    .resolve_pointer(0, &JsonPointer::parse(path).unwrap())
522                    .is_err(),
523                "{path}"
524            );
525        }
526        let mapping = YamlDoc::parse("1: value\n").unwrap();
527        let error = mapping
528            .resolve_pointer(0, &JsonPointer::parse("/1").unwrap())
529            .unwrap_err();
530        assert_eq!(error.kind(), PointerErrorKind::NonStringKey);
531    }
532
533    #[test]
534    fn duplicate_matching_keys_are_ambiguous() {
535        let doc = YamlDoc::parse("foo: one\nfoo: two\n").unwrap();
536        let error = doc
537            .resolve_pointer(0, &JsonPointer::parse("/foo").unwrap())
538            .unwrap_err();
539        assert_eq!(error.kind(), PointerErrorKind::AmbiguousKey);
540    }
541}