1use std::collections::HashSet;
2use std::fmt;
3
4use crate::{NodeId, ResolvedScalar, SemanticKind, Span, 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> {
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 #[must_use]
68 pub fn as_str(&self) -> &str {
69 &self.original
70 }
71
72 #[must_use]
74 pub fn tokens(&self) -> &[ReferenceToken] {
75 &self.tokens
76 }
77
78 #[must_use]
80 pub fn is_root(&self) -> bool {
81 self.tokens.is_empty()
82 }
83
84 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum PointerErrorKind {
141 InvalidSyntax,
143 InvalidEscape,
145 EmptyDocument,
147 MissingValue,
149 TypeMismatch,
151 InvalidIndex,
153 IndexOutOfBounds,
155 DashNotAllowed,
157 NonStringKey,
159 AmbiguousKey,
161 UnresolvedAlias,
163 AliasCycle,
165 Semantic,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct PointerError {
172 pointer: String,
173 token_index: Option<usize>,
174 kind: PointerErrorKind,
175 message: String,
176 source_span: Option<Span>,
177}
178
179impl PointerError {
180 pub(crate) fn new(
181 pointer: impl Into<String>,
182 token_index: Option<usize>,
183 kind: PointerErrorKind,
184 message: impl Into<String>,
185 ) -> Self {
186 Self {
187 pointer: pointer.into(),
188 token_index,
189 kind,
190 message: message.into(),
191 source_span: None,
192 }
193 }
194
195 fn with_source_span(mut self, source_span: Span) -> Self {
196 self.source_span = Some(source_span);
197 self
198 }
199
200 #[must_use]
202 pub fn pointer(&self) -> &str {
203 &self.pointer
204 }
205
206 #[must_use]
208 pub const fn token_index(&self) -> Option<usize> {
209 self.token_index
210 }
211
212 #[must_use]
214 pub const fn kind(&self) -> PointerErrorKind {
215 self.kind
216 }
217
218 #[must_use]
220 pub const fn source_span(&self) -> Option<Span> {
221 self.source_span
222 }
223}
224
225impl fmt::Display for PointerError {
226 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227 if self.pointer.is_empty() {
228 write!(formatter, "cannot resolve document root: {}", self.message)
229 } else {
230 write!(
231 formatter,
232 "cannot resolve JSON Pointer {:?}: {}",
233 self.pointer, self.message
234 )
235 }
236 }
237}
238
239impl std::error::Error for PointerError {}
240
241#[derive(Debug, Clone, Copy)]
242pub(crate) struct MappingMatch {
243 pub(crate) key: NodeId,
244 pub(crate) value: NodeId,
245}
246
247impl YamlDoc {
248 pub fn resolve_pointer(
258 &self,
259 document: usize,
260 pointer: &JsonPointer,
261 ) -> Result<NodeId, PointerError> {
262 let mut current = self
263 .document_root(document)
264 .map_err(|error| {
265 PointerError::new(
266 pointer.as_str(),
267 None,
268 PointerErrorKind::Semantic,
269 error.to_string(),
270 )
271 })?
272 .ok_or_else(|| {
273 PointerError::new(
274 pointer.as_str(),
275 None,
276 PointerErrorKind::EmptyDocument,
277 "selected YAML document has no root node",
278 )
279 })?;
280
281 for (index, token) in pointer.tokens().iter().enumerate() {
282 current = self.resolve_aliases_for_pointer(current, pointer, index)?;
283 current = match self.semantic_kind(current) {
284 Some(SemanticKind::Mapping { .. }) => {
285 self.mapping_match(current, token, pointer, index)?
286 .ok_or_else(|| {
287 PointerError::new(
288 pointer.as_str(),
289 Some(index),
290 PointerErrorKind::MissingValue,
291 format!("mapping has no member {:?}", token.as_str()),
292 )
293 })?
294 .value
295 }
296 Some(SemanticKind::Sequence { .. }) => {
297 let items = self.sequence_items(current).collect::<Vec<_>>();
298 let item_index = parse_sequence_index(token, pointer, index, false)?;
299 *items.get(item_index).ok_or_else(|| {
300 PointerError::new(
301 pointer.as_str(),
302 Some(index),
303 PointerErrorKind::IndexOutOfBounds,
304 format!(
305 "sequence index {item_index} is out of bounds for length {}",
306 items.len()
307 ),
308 )
309 })?
310 }
311 Some(SemanticKind::Scalar { .. } | SemanticKind::Alias) => {
312 return Err(PointerError::new(
313 pointer.as_str(),
314 Some(index),
315 PointerErrorKind::TypeMismatch,
316 format!(
317 "token {:?} cannot be evaluated against a scalar",
318 token.as_str()
319 ),
320 ));
321 }
322 Some(SemanticKind::Document) | None => {
323 return Err(PointerError::new(
324 pointer.as_str(),
325 Some(index),
326 PointerErrorKind::TypeMismatch,
327 "token cannot be evaluated against this YAML node",
328 ));
329 }
330 };
331 }
332 Ok(current)
333 }
334
335 pub(crate) fn resolve_aliases_for_pointer(
336 &self,
337 mut node: NodeId,
338 pointer: &JsonPointer,
339 token_index: usize,
340 ) -> Result<NodeId, PointerError> {
341 let mut seen = HashSet::new();
342 while matches!(self.semantic_kind(node), Some(SemanticKind::Alias)) {
343 if !seen.insert(node) {
344 let mut error = PointerError::new(
345 pointer.as_str(),
346 Some(token_index),
347 PointerErrorKind::AliasCycle,
348 "cyclic alias chain",
349 );
350 if let Some(span) = self.node(node).map(|node| node.span()) {
351 error = error.with_source_span(span);
352 }
353 return Err(error);
354 }
355 let alias = node;
356 node = self.resolve_alias(alias).ok_or_else(|| {
357 let mut error = PointerError::new(
358 pointer.as_str(),
359 Some(token_index),
360 PointerErrorKind::UnresolvedAlias,
361 format!(
362 "unresolved alias `*{}`",
363 self.alias_name(alias).unwrap_or_default()
364 ),
365 );
366 if let Some(span) = self.node(alias).map(|node| node.span()) {
367 error = error.with_source_span(span);
368 }
369 error
370 })?;
371 }
372 Ok(node)
373 }
374
375 pub(crate) fn mapping_match(
376 &self,
377 mapping: NodeId,
378 token: &ReferenceToken,
379 pointer: &JsonPointer,
380 token_index: usize,
381 ) -> Result<Option<MappingMatch>, PointerError> {
382 let mut found = None;
383 for (key, value) in self.mapping_entries(mapping) {
384 let resolved_key = self.resolve_aliases_for_pointer(key, pointer, token_index)?;
385 let Some(SemanticKind::Scalar { style }) = self.semantic_kind(resolved_key) else {
386 return Err(non_string_key_error(pointer, token_index));
387 };
388 let scalar = self.scalar_value(resolved_key).map_err(|error| {
389 PointerError::new(
390 pointer.as_str(),
391 Some(token_index),
392 PointerErrorKind::Semantic,
393 error.to_string(),
394 )
395 })?;
396 let tag = self.resolved_tag(resolved_key).map_err(|error| {
397 PointerError::new(
398 pointer.as_str(),
399 Some(token_index),
400 PointerErrorKind::Semantic,
401 error.to_string(),
402 )
403 })?;
404 let resolved = resolve_scalar(&scalar, style, tag.as_deref())
405 .map_err(|_| non_string_key_error(pointer, token_index))?;
406 if resolved != ResolvedScalar::String {
407 return Err(non_string_key_error(pointer, token_index));
408 }
409 if scalar == token.as_str() {
410 if found.is_some() {
411 return Err(PointerError::new(
412 pointer.as_str(),
413 Some(token_index),
414 PointerErrorKind::AmbiguousKey,
415 format!(
416 "mapping contains multiple matching keys {:?}",
417 token.as_str()
418 ),
419 ));
420 }
421 found = Some(MappingMatch { key, value });
422 }
423 }
424 Ok(found)
425 }
426}
427
428fn non_string_key_error(pointer: &JsonPointer, token_index: usize) -> PointerError {
429 PointerError::new(
430 pointer.as_str(),
431 Some(token_index),
432 PointerErrorKind::NonStringKey,
433 "cannot traverse a mapping containing non-string keys",
434 )
435}
436
437pub(crate) fn parse_sequence_index(
438 token: &ReferenceToken,
439 pointer: &JsonPointer,
440 token_index: usize,
441 allow_dash: bool,
442) -> Result<usize, PointerError> {
443 let value = token.as_str();
444 if value == "-" {
445 return if allow_dash {
446 Ok(usize::MAX)
447 } else {
448 Err(PointerError::new(
449 pointer.as_str(),
450 Some(token_index),
451 PointerErrorKind::DashNotAllowed,
452 "`-` is allowed only as the final add destination token",
453 ))
454 };
455 }
456 if value.is_empty()
457 || !value.bytes().all(|byte| byte.is_ascii_digit())
458 || value.len() > 1 && value.starts_with('0')
459 {
460 return Err(PointerError::new(
461 pointer.as_str(),
462 Some(token_index),
463 PointerErrorKind::InvalidIndex,
464 format!("{value:?} is not a canonical sequence index"),
465 ));
466 }
467 value.parse().map_err(|_| {
468 PointerError::new(
469 pointer.as_str(),
470 Some(token_index),
471 PointerErrorKind::InvalidIndex,
472 format!("sequence index {value:?} overflows this platform"),
473 )
474 })
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn parses_and_decodes_plain_json_pointers() {
483 for (input, expected) in [
484 ("", vec![]),
485 ("/foo/0", vec!["foo", "0"]),
486 ("/a~1b", vec!["a/b"]),
487 ("/m~0n", vec!["m~n"]),
488 ("/~01", vec!["~1"]),
489 ] {
490 let pointer = JsonPointer::parse(input).unwrap();
491 assert_eq!(
492 pointer
493 .tokens()
494 .iter()
495 .map(ReferenceToken::as_str)
496 .collect::<Vec<_>>(),
497 expected
498 );
499 }
500 }
501
502 #[test]
503 fn rejects_invalid_pointer_syntax() {
504 for input in ["foo", "/foo/~", "/foo/~2"] {
505 assert!(JsonPointer::parse(input).is_err(), "{input}");
506 }
507 }
508
509 #[test]
510 fn resolves_mappings_sequences_and_escaped_keys() {
511 let doc = YamlDoc::parse("'a/b':\n - zero\n - one\n'm~n': value\n").expect("valid YAML");
512 let pointer = JsonPointer::parse("/a~1b/1").unwrap();
513 let node = doc.resolve_pointer(0, &pointer).unwrap();
514 assert_eq!(doc.scalar_value(node).unwrap(), "one");
515 let pointer = JsonPointer::parse("/m~0n").unwrap();
516 assert_eq!(
517 doc.scalar_value(doc.resolve_pointer(0, &pointer).unwrap())
518 .unwrap(),
519 "value"
520 );
521 }
522
523 #[test]
524 fn traverses_aliases_but_returns_a_terminal_alias() {
525 let doc =
526 YamlDoc::parse("defaults: &defaults\n timeout: 30\nservice:\n config: *defaults\n")
527 .unwrap();
528 let terminal = doc
529 .resolve_pointer(0, &JsonPointer::parse("/service/config").unwrap())
530 .unwrap();
531 assert_eq!(doc.alias_name(terminal), Some("defaults"));
532 let traversed = doc
533 .resolve_pointer(0, &JsonPointer::parse("/service/config/timeout").unwrap())
534 .unwrap();
535 assert_eq!(doc.scalar_value(traversed).unwrap(), "30");
536 }
537
538 #[test]
539 fn unresolved_alias_errors_preserve_source_spans_through_edits() {
540 let doc = YamlDoc::parse("bad: *missing\n").unwrap();
541 let pointer = JsonPointer::parse("/bad/value").unwrap();
542 let error = doc.resolve_pointer(0, &pointer).unwrap_err();
543 assert_eq!(error.kind(), PointerErrorKind::UnresolvedAlias);
544 assert_eq!(error.source_span(), Some(Span::new(5, 13)));
545
546 let mut edited = doc;
547 let error = edited.remove_at(0, &pointer).unwrap_err();
548 assert_eq!(error.source_span(), Some(Span::new(5, 13)));
549 }
550
551 #[test]
552 fn rejects_noncanonical_indices_and_non_string_keys() {
553 let sequence = YamlDoc::parse("[a, b]\n").unwrap();
554 for path in ["/01", "/-1", "/+1", "/ ", "/-"] {
555 assert!(
556 sequence
557 .resolve_pointer(0, &JsonPointer::parse(path).unwrap())
558 .is_err(),
559 "{path}"
560 );
561 }
562 let mapping = YamlDoc::parse("1: value\n").unwrap();
563 let error = mapping
564 .resolve_pointer(0, &JsonPointer::parse("/1").unwrap())
565 .unwrap_err();
566 assert_eq!(error.kind(), PointerErrorKind::NonStringKey);
567 }
568
569 #[test]
570 fn duplicate_matching_keys_are_ambiguous() {
571 let doc = YamlDoc::parse("foo: one\nfoo: two\n").unwrap();
572 let error = doc
573 .resolve_pointer(0, &JsonPointer::parse("/foo").unwrap())
574 .unwrap_err();
575 assert_eq!(error.kind(), PointerErrorKind::AmbiguousKey);
576 }
577}