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> {
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}
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 #[must_use]
195 pub fn pointer(&self) -> &str {
196 &self.pointer
197 }
198
199 #[must_use]
201 pub const fn token_index(&self) -> Option<usize> {
202 self.token_index
203 }
204
205 #[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 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}