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