Skip to main content

noyalib/
borrowed.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! Zero-copy YAML values that borrow strings from the input.
5//!
6//! [`BorrowedValue`] is the zero-copy counterpart of [`Value`](crate::Value).
7//! String scalars and mapping keys use `Cow<'a, str>`, borrowing directly from
8//! the input buffer when no escape processing was needed. This eliminates heap
9//! allocations for the majority of YAML content.
10//!
11//! # Examples
12//!
13//! ```rust
14//! use noyalib::borrowed::{from_str_borrowed, BorrowedValue};
15//!
16//! let yaml = "name: noyalib\nversion: 1\n";
17//! let value: BorrowedValue<'_> = from_str_borrowed(yaml).unwrap();
18//! assert_eq!(value.as_mapping().unwrap().get("name").unwrap().as_str(), Some("noyalib"));
19//! ```
20
21use crate::error::{Error, Result};
22use crate::parser::meter::{AliasCost, CostTally, Meter};
23use crate::parser::{
24    Event, InternalDuplicateKeyPolicy, InternalMergeKeyPolicy, ParseConfig, Parser, ScalarStyle,
25    budget,
26};
27use crate::path::{QueryPath, QuerySegment, parse_query_path};
28use crate::prelude::IndexMap;
29use crate::prelude::*;
30use crate::prelude::{FxBuildHasher, FxHashMap};
31use core::hash::{Hash, Hasher};
32
33/// Why a YAML scalar could not be borrowed directly from the input
34/// buffer and had to be materialised into an owned `String`.
35///
36/// Surfaced for users introspecting [`BorrowedValue`] / streaming
37/// deserialisation paths: when a `Cow<'a, str>` resolves to
38/// [`Cow::Owned`] instead of [`Cow::Borrowed`], one of these reasons
39/// applies. Useful in benchmarks and allocation-budget audits.
40///
41/// # Examples
42///
43/// ```
44/// use noyalib::borrowed::TransformReason;
45/// assert_eq!(TransformReason::EscapeSequence.as_str(),
46///            "scalar contained escape sequences that required decoding");
47/// ```
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[non_exhaustive]
50pub enum TransformReason {
51    /// The scalar contained `\n`, `\t`, `\xNN`, `\uNNNN`, `\UNNNNNNNN`,
52    /// or other escape sequences that required decoding into a fresh
53    /// allocation.
54    EscapeSequence,
55    /// The scalar spans multiple physical lines and required line
56    /// folding (block scalar `>` or `|`, or a multi-line flow scalar).
57    LineFold,
58    /// Tag resolution materialised a fresh representation (`!!binary`
59    /// base64 decode, custom-tag dispatch via [`crate::TagRegistry`]).
60    TagResolution,
61    /// The scalar was double-quoted and contained at least one escape,
62    /// so the parser produced an owned post-escape buffer.
63    QuotedScalar,
64    /// The scalar arrived via alias replay (`*anchor`) and the replayed
65    /// buffer is owned by the alias-expansion machinery, not the input
66    /// slice.
67    AliasExpansion,
68}
69
70impl TransformReason {
71    /// A human-readable explanation suitable for inclusion in error
72    /// messages.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use noyalib::borrowed::TransformReason;
78    /// assert_eq!(TransformReason::LineFold.as_str(),
79    ///            "scalar spans multiple lines and required line folding");
80    /// ```
81    #[must_use]
82    pub fn as_str(self) -> &'static str {
83        match self {
84            Self::EscapeSequence => "scalar contained escape sequences that required decoding",
85            Self::LineFold => "scalar spans multiple lines and required line folding",
86            Self::TagResolution => "tag resolution materialised a fresh representation",
87            Self::QuotedScalar => "double-quoted scalar with escapes produced an owned buffer",
88            Self::AliasExpansion => "scalar arrived via alias replay (`*anchor`)",
89        }
90    }
91}
92
93impl fmt::Display for TransformReason {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.write_str(self.as_str())
96    }
97}
98
99/// A zero-copy YAML value that borrows strings from the input.
100///
101/// # Examples
102///
103/// ```
104/// use noyalib::borrowed::{from_str_borrowed, BorrowedValue};
105/// let v: BorrowedValue<'_> = from_str_borrowed("k: 1\n").unwrap();
106/// assert!(v.as_mapping().is_some());
107/// ```
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum BorrowedValue<'a> {
110    /// YAML null.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use noyalib::borrowed::BorrowedValue;
116    /// assert!(BorrowedValue::Null.is_null());
117    /// ```
118    Null,
119    /// YAML boolean.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use noyalib::borrowed::BorrowedValue;
125    /// let v = BorrowedValue::Bool(true);
126    /// assert_eq!(v.as_bool(), Some(true));
127    /// ```
128    Bool(bool),
129    /// YAML number.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use noyalib::{borrowed::BorrowedValue, Number};
135    /// let v = BorrowedValue::Number(Number::Integer(42));
136    /// assert_eq!(v.as_i64(), Some(42));
137    /// ```
138    Number(crate::value::Number),
139    /// YAML string — borrows from input when possible.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use std::borrow::Cow;
145    /// use noyalib::borrowed::BorrowedValue;
146    /// let v = BorrowedValue::String(Cow::Borrowed("hi"));
147    /// assert_eq!(v.as_str(), Some("hi"));
148    /// ```
149    String(Cow<'a, str>),
150    /// YAML sequence.
151    ///
152    /// # Examples
153    ///
154    /// ```
155    /// use noyalib::borrowed::BorrowedValue;
156    /// let v = BorrowedValue::Sequence(vec![BorrowedValue::Null]);
157    /// assert_eq!(v.as_sequence().unwrap().len(), 1);
158    /// ```
159    Sequence(Vec<Self>),
160    /// YAML mapping with borrowed keys.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use noyalib::borrowed::{from_str_borrowed, BorrowedValue};
166    /// let v = from_str_borrowed("k: 1\n").unwrap();
167    /// assert!(matches!(v, BorrowedValue::Mapping(_)));
168    /// ```
169    Mapping(IndexMap<Cow<'a, str>, Self, FxBuildHasher>),
170}
171
172impl<'a> BorrowedValue<'a> {
173    /// Returns `true` if this is a null value.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use noyalib::borrowed::BorrowedValue;
179    /// assert!(BorrowedValue::Null.is_null());
180    /// ```
181    #[must_use]
182    pub fn is_null(&self) -> bool {
183        matches!(self, Self::Null)
184    }
185
186    /// Returns the string value if this is a string.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use std::borrow::Cow;
192    /// use noyalib::borrowed::BorrowedValue;
193    /// let v = BorrowedValue::String(Cow::Borrowed("hi"));
194    /// assert_eq!(v.as_str(), Some("hi"));
195    /// ```
196    #[must_use]
197    pub fn as_str(&self) -> Option<&str> {
198        match self {
199            Self::String(s) => Some(s),
200            _ => None,
201        }
202    }
203
204    /// Returns the i64 value if this is an integer.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// use noyalib::{borrowed::BorrowedValue, Number};
210    /// let v = BorrowedValue::Number(Number::Integer(42));
211    /// assert_eq!(v.as_i64(), Some(42));
212    /// ```
213    #[must_use]
214    pub fn as_i64(&self) -> Option<i64> {
215        match self {
216            Self::Number(n) => n.as_i64(),
217            _ => None,
218        }
219    }
220
221    /// Returns the bool value if this is a boolean.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use noyalib::borrowed::BorrowedValue;
227    /// assert_eq!(BorrowedValue::Bool(true).as_bool(), Some(true));
228    /// ```
229    #[must_use]
230    pub fn as_bool(&self) -> Option<bool> {
231        match self {
232            Self::Bool(b) => Some(*b),
233            _ => None,
234        }
235    }
236
237    /// Returns the sequence if this is a sequence.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use noyalib::borrowed::BorrowedValue;
243    /// let v = BorrowedValue::Sequence(vec![BorrowedValue::Null]);
244    /// assert_eq!(v.as_sequence().unwrap().len(), 1);
245    /// ```
246    #[must_use]
247    pub fn as_sequence(&self) -> Option<&[Self]> {
248        match self {
249            Self::Sequence(s) => Some(s),
250            _ => None,
251        }
252    }
253
254    /// Returns the mapping if this is a mapping.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use noyalib::borrowed::from_str_borrowed;
260    /// let v = from_str_borrowed("k: 1\n").unwrap();
261    /// assert!(v.as_mapping().is_some());
262    /// ```
263    #[must_use]
264    pub fn as_mapping(&self) -> Option<&IndexMap<Cow<'a, str>, Self, FxBuildHasher>> {
265        match self {
266            Self::Mapping(m) => Some(m),
267            _ => None,
268        }
269    }
270
271    /// Query nested values through a prevalidated [`QueryPath`].
272    #[must_use]
273    pub fn query_path(&self, path: &QueryPath) -> Vec<&Self> {
274        let mut results = Vec::new();
275        borrowed_query_recursive(self, path.segments(), 0, &mut results);
276        results
277    }
278
279    /// Query nested values using an extended path expression.
280    ///
281    /// Returns all matching values. Supports dot notation, bracket indexing,
282    /// bracket-quoted keys (`labels["app.kubernetes.io/name"]`, see
283    /// [`crate::path`]), wildcards (`*`), and recursive descent (`..`).
284    ///
285    /// # Examples
286    ///
287    /// ```rust
288    /// use noyalib::borrowed::from_str_borrowed;
289    ///
290    /// let yaml = "items:\n  - name: a\n  - name: b\n";
291    /// let v = from_str_borrowed(yaml).unwrap();
292    /// let names = v.query("items[*].name");
293    /// assert_eq!(names.len(), 2);
294    /// ```
295    #[must_use]
296    pub fn query(&self, path: &str) -> Vec<&Self> {
297        let segments = parse_query_path(path);
298        let mut results = Vec::new();
299        borrowed_query_recursive(self, &segments, 0, &mut results);
300        results
301    }
302
303    /// Access a nested value through a prevalidated [`QueryPath`].
304    #[must_use]
305    pub fn get_query_path(&self, path: &QueryPath) -> Option<&Self> {
306        let mut current = self;
307        for seg in path.segments() {
308            current = match seg {
309                QuerySegment::Key(key) => {
310                    if let Self::Mapping(m) = current {
311                        m.get(key.as_str())?
312                    } else {
313                        return None;
314                    }
315                }
316                QuerySegment::Index(idx) => {
317                    if let Self::Sequence(s) = current {
318                        s.get(*idx)?
319                    } else {
320                        return None;
321                    }
322                }
323                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => {
324                    return self.query_path(path).into_iter().next();
325                }
326            };
327        }
328        Some(current)
329    }
330
331    /// Access a nested value via a dotted path.
332    ///
333    /// # Examples
334    ///
335    /// ```
336    /// use noyalib::borrowed::from_str_borrowed;
337    /// let v = from_str_borrowed("a:\n  b: 2\n").unwrap();
338    /// assert_eq!(v.get_path("a.b").unwrap().as_i64(), Some(2));
339    /// ```
340    #[must_use]
341    pub fn get_path(&self, path: &str) -> Option<&Self> {
342        let segments = parse_query_path(path);
343        let mut current = self;
344        for seg in &segments {
345            current = match seg {
346                QuerySegment::Key(key) => {
347                    if let Self::Mapping(m) = current {
348                        m.get(key.as_str())?
349                    } else {
350                        return None;
351                    }
352                }
353                QuerySegment::Index(idx) => {
354                    if let Self::Sequence(s) = current {
355                        s.get(*idx)?
356                    } else {
357                        return None;
358                    }
359                }
360                QuerySegment::Wildcard | QuerySegment::RecursiveDescent => {
361                    return self.query(path).into_iter().next();
362                }
363            };
364        }
365        Some(current)
366    }
367
368    /// Convert to an owned `Value`, cloning all borrowed strings.
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use noyalib::borrowed::from_str_borrowed;
374    /// let v = from_str_borrowed("k: 1\n").unwrap();
375    /// let owned = v.into_owned();
376    /// assert!(owned.as_mapping().is_some());
377    /// ```
378    #[must_use]
379    pub fn into_owned(self) -> crate::Value {
380        match self {
381            Self::Null => crate::Value::Null,
382            Self::Bool(b) => crate::Value::Bool(b),
383            Self::Number(n) => crate::Value::Number(n),
384            Self::String(s) => crate::Value::String(s.into_owned()),
385            Self::Sequence(seq) => {
386                crate::Value::Sequence(seq.into_iter().map(|v| v.into_owned()).collect())
387            }
388            Self::Mapping(map) => {
389                let mut m = crate::Mapping::with_capacity(map.len());
390                for (k, v) in map {
391                    let _ = m.insert(k.into_owned(), v.into_owned());
392                }
393                crate::Value::Mapping(m)
394            }
395        }
396    }
397}
398
399impl Hash for BorrowedValue<'_> {
400    fn hash<H: Hasher>(&self, state: &mut H) {
401        core::mem::discriminant(self).hash(state);
402        match self {
403            Self::Null => {}
404            Self::Bool(b) => b.hash(state),
405            Self::Number(n) => n.hash(state),
406            Self::String(s) => s.hash(state),
407            Self::Sequence(seq) => seq.hash(state),
408            Self::Mapping(map) => {
409                state.write_usize(map.len());
410                for (k, v) in map {
411                    k.hash(state);
412                    v.hash(state);
413                }
414            }
415        }
416    }
417}
418
419impl serde_core::Serialize for BorrowedValue<'_> {
420    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
421    where
422        S: serde_core::Serializer,
423    {
424        match self {
425            Self::Null => serializer.serialize_none(),
426            Self::Bool(b) => serializer.serialize_bool(*b),
427            Self::Number(n) => match n {
428                crate::value::Number::Integer(i) => serializer.serialize_i64(*i),
429                #[cfg(feature = "lossless-u64")]
430                crate::value::Number::Unsigned(u) => serializer.serialize_u64(*u),
431                crate::value::Number::Float(f) => serializer.serialize_f64(*f),
432            },
433            Self::String(s) => serializer.serialize_str(s),
434            Self::Sequence(seq) => seq.serialize(serializer),
435            Self::Mapping(map) => {
436                use serde_core::ser::SerializeMap as _;
437                let mut m = serializer.serialize_map(Some(map.len()))?;
438                for (k, v) in map {
439                    m.serialize_entry(k.as_ref(), v)?;
440                }
441                m.end()
442            }
443        }
444    }
445}
446
447impl PartialOrd for BorrowedValue<'_> {
448    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
449        Some(self.cmp(other))
450    }
451}
452
453impl Ord for BorrowedValue<'_> {
454    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
455        use core::cmp::Ordering;
456        let rank = |v: &Self| -> u8 {
457            match v {
458                Self::Null => 0,
459                Self::Bool(_) => 1,
460                Self::Number(_) => 2,
461                Self::String(_) => 3,
462                Self::Sequence(_) => 4,
463                Self::Mapping(_) => 5,
464            }
465        };
466        let r = rank(self).cmp(&rank(other));
467        if r != Ordering::Equal {
468            return r;
469        }
470        match (self, other) {
471            (Self::Null, Self::Null) => Ordering::Equal,
472            (Self::Bool(a), Self::Bool(b)) => a.cmp(b),
473            (Self::Number(a), Self::Number(b)) => a.cmp(b),
474            (Self::String(a), Self::String(b)) => a.cmp(b),
475            (Self::Sequence(a), Self::Sequence(b)) => a.cmp(b),
476            (Self::Mapping(a), Self::Mapping(b)) => a.len().cmp(&b.len()),
477            _ => Ordering::Equal,
478        }
479    }
480}
481
482/// Recursively query a `BorrowedValue` tree.
483fn borrowed_query_recursive<'a, 'b>(
484    value: &'b BorrowedValue<'a>,
485    segments: &[QuerySegment],
486    depth: usize,
487    results: &mut Vec<&'b BorrowedValue<'a>>,
488) {
489    if depth >= segments.len() {
490        results.push(value);
491        return;
492    }
493    match &segments[depth] {
494        QuerySegment::Key(key) => {
495            if let BorrowedValue::Mapping(m) = value
496                && let Some(child) = m.get(key.as_str())
497            {
498                borrowed_query_recursive(child, segments, depth + 1, results);
499            }
500        }
501        QuerySegment::Index(idx) => {
502            if let BorrowedValue::Sequence(s) = value
503                && let Some(child) = s.get(*idx)
504            {
505                borrowed_query_recursive(child, segments, depth + 1, results);
506            }
507        }
508        QuerySegment::Wildcard => match value {
509            BorrowedValue::Sequence(seq) => {
510                for item in seq {
511                    borrowed_query_recursive(item, segments, depth + 1, results);
512                }
513            }
514            BorrowedValue::Mapping(map) => {
515                for (_, v) in map {
516                    borrowed_query_recursive(v, segments, depth + 1, results);
517                }
518            }
519            _ => {}
520        },
521        QuerySegment::RecursiveDescent => {
522            let remaining = &segments[depth + 1..];
523            if !remaining.is_empty() {
524                borrowed_query_recursive(value, segments, depth + 1, results);
525                match value {
526                    BorrowedValue::Sequence(seq) => {
527                        for item in seq {
528                            borrowed_query_recursive(item, segments, depth, results);
529                        }
530                    }
531                    BorrowedValue::Mapping(map) => {
532                        for (_, v) in map {
533                            borrowed_query_recursive(v, segments, depth, results);
534                        }
535                    }
536                    _ => {}
537                }
538            }
539        }
540    }
541}
542
543/// Parse YAML into a zero-copy `BorrowedValue` that borrows from the input.
544///
545/// This is significantly faster than `from_str::<Value>` for large documents
546/// because string scalars and mapping keys borrow directly from the input
547/// buffer instead of allocating on the heap.
548///
549/// # Examples
550///
551/// ```rust
552/// use noyalib::borrowed::{from_str_borrowed, BorrowedValue};
553///
554/// let yaml = "host: localhost\nport: 8080\n";
555/// let value = from_str_borrowed(yaml).unwrap();
556/// assert_eq!(value.as_mapping().unwrap().get("host").unwrap().as_str(), Some("localhost"));
557/// ```
558pub fn from_str_borrowed(input: &str) -> Result<BorrowedValue<'_>> {
559    from_str_borrowed_with_config(input, &crate::ParserConfig::default())
560}
561
562/// Parse YAML into a zero-copy `BorrowedValue` with custom security limits.
563///
564/// Same as [`from_str_borrowed`] but accepts a [`crate::ParserConfig`]
565/// so callers can tighten `max_document_length`, `max_depth`, and other
566/// limits for untrusted input.
567///
568/// # Errors
569///
570/// Returns an error when the input exceeds `max_document_length`, when
571/// the parser encounters invalid YAML, or when alias expansion exceeds
572/// [`ParserConfig::max_alias_expansions`](crate::ParserConfig::max_alias_expansions).
573///
574/// # Aliases
575///
576/// Anchors (`&name`) and aliases (`*name`) are eagerly resolved on the
577/// borrowed path. The anchored value is stored in a side-table keyed
578/// by name; each alias clones the value into the tree (string fields
579/// stay `Cow::Borrowed`, so the clone is mostly free — only sequences
580/// and mappings actually duplicate). Total expansions are bounded by
581/// `max_alias_expansions` to neutralise YAML bombs.
582///
583/// # Examples
584///
585/// ```
586/// use noyalib::{borrowed::from_str_borrowed_with_config, ParserConfig};
587/// let cfg = ParserConfig::strict();
588/// let v = from_str_borrowed_with_config("k: 1\n", &cfg).unwrap();
589/// assert!(v.as_mapping().is_some());
590/// ```
591pub fn from_str_borrowed_with_config<'a>(
592    input: &'a str,
593    user_config: &crate::ParserConfig,
594) -> Result<BorrowedValue<'a>> {
595    let config = ParseConfig::from(user_config);
596    if input.len() > config.max_document_length {
597        return Err(Error::Parse(format!(
598            "document exceeds maximum length of {} bytes",
599            config.max_document_length
600        )));
601    }
602
603    let mut parser = Parser::with_max_events(input, config.max_events);
604    let mut builder = BorrowedBuilder::new(&config);
605
606    loop {
607        let event = parser
608            .next_event()
609            .map_err(|e| e.into_error(input, config.max_events))?;
610        if matches!(event, Event::StreamEnd) {
611            break;
612        }
613        builder.process(event)?;
614    }
615
616    let value = builder.into_value();
617    if !user_config.policies.is_empty() {
618        crate::policy::check_document(&user_config.policies, &value.clone().into_owned())?;
619    }
620    Ok(value)
621}
622
623enum Frame<'a> {
624    Sequence(Vec<BorrowedValue<'a>>, Option<String>),
625    MappingKey(
626        IndexMap<Cow<'a, str>, BorrowedValue<'a>, FxBuildHasher>,
627        Option<String>,
628    ),
629    MappingValue(
630        IndexMap<Cow<'a, str>, BorrowedValue<'a>, FxBuildHasher>,
631        Cow<'a, str>,
632        Option<String>,
633    ),
634}
635
636/// The plain `<<` key that the owned loaders read as a merge.
637const MERGE_KEY: &str = "<<";
638
639struct BorrowedBuilder<'a, 'c> {
640    stack: Vec<Frame<'a>>,
641    result: Option<BorrowedValue<'a>>,
642    config: &'c ParseConfig,
643    depth: usize,
644    /// Anchor → value table, with the measured cost of expanding each.
645    /// Eager resolution: an `Alias` event clones the anchored value into
646    /// the tree (string fields stay `Cow::Borrowed`, so only sequences
647    /// and mappings duplicate), after the cost is charged.
648    anchors: FxHashMap<String, (BorrowedValue<'a>, AliasCost)>,
649    /// The budget meter the owned loaders and the streaming
650    /// deserializer charge, so every limit holds here too.
651    meter: Meter,
652}
653
654impl<'a, 'c> BorrowedBuilder<'a, 'c> {
655    fn new(config: &'c ParseConfig) -> Self {
656        Self {
657            stack: Vec::new(),
658            result: None,
659            config,
660            depth: 0,
661            anchors: FxHashMap::default(),
662            meter: Meter::new(config),
663        }
664    }
665
666    fn into_value(self) -> BorrowedValue<'a> {
667        self.result.unwrap_or(BorrowedValue::Null)
668    }
669
670    fn resolve_scalar(
671        &self,
672        value: Cow<'a, str>,
673        style: ScalarStyle,
674        tag: Option<&(String, String)>,
675    ) -> BorrowedValue<'a> {
676        // `!!str` is a resolution tag, not a decoration: it says the
677        // scalar *is* a string, so plain resolution must not run and
678        // turn `!!str 1` into the integer 1. The owned graph honours it
679        // (`resolve_tagged_scalar`'s "str" arm); this one used to
680        // discard the tag entirely and disagree.
681        //
682        // The other core tags (`int`, `float`, `bool`, `null`) already
683        // land on the same answer through plain resolution for every
684        // well-formed payload, and `BorrowedValue` has no `Tagged`
685        // variant for a custom tag to live in — both are recorded in
686        // `differential_readers.rs` rather than silently differing.
687        if is_core_string_tag(tag) || style != ScalarStyle::Plain {
688            return BorrowedValue::String(value);
689        }
690        let c = self.config;
691        match crate::streaming::resolve_plain_ext(
692            &value,
693            c.strict_booleans,
694            c.legacy_booleans,
695            c.no_schema,
696            c.legacy_octal_numbers,
697            c.legacy_sexagesimal,
698            c.lossless_u64_integers(),
699        ) {
700            crate::streaming::Scalar::Null => BorrowedValue::Null,
701            crate::streaming::Scalar::Bool(b) => BorrowedValue::Bool(b),
702            crate::streaming::Scalar::Int(i) => {
703                BorrowedValue::Number(crate::value::Number::Integer(i))
704            }
705            #[cfg(feature = "lossless-u64")]
706            crate::streaming::Scalar::Uint(u) => {
707                BorrowedValue::Number(crate::value::Number::Unsigned(u))
708            }
709            crate::streaming::Scalar::Float(f) => {
710                BorrowedValue::Number(crate::value::Number::Float(f))
711            }
712            crate::streaming::Scalar::Str(_) => BorrowedValue::String(value),
713        }
714    }
715
716    /// Place a finished value: into the open sequence or mapping, under
717    /// the same length, key-count and duplicate-key rules the owned
718    /// loaders apply, or as the document's root.
719    fn push_value(&mut self, value: BorrowedValue<'a>) -> Result<()> {
720        match self.stack.pop() {
721            Some(Frame::Sequence(mut seq, anchor)) => {
722                if seq.len() >= self.config.max_sequence_length {
723                    return Err(Error::Budget(crate::BudgetBreach::MaxSequenceLength {
724                        limit: self.config.max_sequence_length,
725                        observed: seq.len() + 1,
726                    }));
727                }
728                seq.push(value);
729                self.stack.push(Frame::Sequence(seq, anchor));
730            }
731            Some(Frame::MappingValue(mut map, key, anchor)) => {
732                self.insert_entry(&mut map, key, value)?;
733                self.stack.push(Frame::MappingKey(map, anchor));
734            }
735            // A key arrives through `set_key`, never here; keep the frame.
736            Some(frame @ Frame::MappingKey(..)) => self.stack.push(frame),
737            None => self.result = Some(value),
738        }
739        Ok(())
740    }
741
742    fn insert_entry(
743        &self,
744        map: &mut IndexMap<Cow<'a, str>, BorrowedValue<'a>, FxBuildHasher>,
745        key: Cow<'a, str>,
746        value: BorrowedValue<'a>,
747    ) -> Result<()> {
748        if map.len() >= self.config.max_mapping_keys {
749            return Err(Error::Budget(crate::BudgetBreach::MaxMappingKeys {
750                limit: self.config.max_mapping_keys,
751                observed: map.len() + 1,
752            }));
753        }
754        match self.config.duplicate_key_policy {
755            InternalDuplicateKeyPolicy::First if map.contains_key(&key) => {}
756            InternalDuplicateKeyPolicy::Error if map.contains_key(&key) => {
757                return Err(Error::DuplicateKey(key.into_owned()));
758            }
759            _ => {
760                let _ = map.insert(key, value);
761            }
762        }
763        Ok(())
764    }
765
766    /// Turn the open mapping's key state into its value state.
767    fn set_key(&mut self, key: Cow<'a, str>) {
768        if let Some(Frame::MappingKey(map, anchor)) = self.stack.pop() {
769            self.stack.push(Frame::MappingValue(map, key, anchor));
770        }
771    }
772
773    /// Register `value` under `anchor` so a later `*anchor` event can
774    /// resolve to a clone of it. No-op when `anchor` is `None`.
775    fn record_anchor(&mut self, anchor: Option<String>, value: &BorrowedValue<'a>) {
776        if let Some(name) = anchor {
777            let cost = borrowed_cost(value);
778            let _ = self.anchors.insert(name, (value.clone(), cost));
779        }
780    }
781
782    fn process(&mut self, event: Event<'a>) -> Result<()> {
783        self.meter.charge_event(&event, self.config)?;
784        match event {
785            Event::DocumentEnd => {
786                // Per YAML spec each document has its own anchor
787                // namespace. Reset between documents to match.
788                self.anchors.clear();
789                Ok(())
790            }
791            Event::Scalar {
792                value,
793                style,
794                anchor,
795                tag,
796                ..
797            } => self.on_scalar(value, style, anchor, tag.as_ref()),
798            Event::SequenceStart { anchor, .. } => {
799                self.open_collection()?;
800                self.stack
801                    .push(Frame::Sequence(Vec::with_capacity(4), anchor));
802                Ok(())
803            }
804            Event::MappingStart { anchor, .. } => {
805                self.open_collection()?;
806                self.stack.push(Frame::MappingKey(
807                    IndexMap::with_capacity_and_hasher(4, FxBuildHasher),
808                    anchor,
809                ));
810                Ok(())
811            }
812            Event::SequenceEnd { .. } | Event::MappingEnd { .. } => self.close_collection(),
813            Event::Alias { anchor, .. } => self.on_alias(&anchor),
814            Event::StreamStart | Event::StreamEnd | Event::DocumentStart => Ok(()),
815        }
816    }
817
818    fn on_scalar(
819        &mut self,
820        value: Cow<'a, str>,
821        style: ScalarStyle,
822        anchor: Option<String>,
823        tag: Option<&(String, String)>,
824    ) -> Result<()> {
825        if let Some(Frame::MappingKey(..)) = self.stack.last() {
826            if style == ScalarStyle::Plain && value == MERGE_KEY {
827                self.charge_merge_key()?;
828            }
829            self.set_key(value);
830            return Ok(());
831        }
832        let resolved = self.resolve_scalar(value, style, tag);
833        self.record_anchor(anchor, &resolved);
834        self.push_value(resolved)
835    }
836
837    /// A plain `<<` key is a merge on the owned paths; the borrowed tree
838    /// keeps it as an ordinary key, but it is charged and refused there
839    /// exactly as the owned loaders do so the budget means one thing.
840    fn charge_merge_key(&mut self) -> Result<()> {
841        match self.config.merge_key_policy {
842            InternalMergeKeyPolicy::AsOrdinary => Ok(()),
843            InternalMergeKeyPolicy::Error => Err(Error::Custom(
844                "merge key `<<` rejected by MergeKeyPolicy::Error".to_owned(),
845            )),
846            InternalMergeKeyPolicy::Auto => self.meter.charge_merge_key(self.config),
847        }
848    }
849
850    fn open_collection(&mut self) -> Result<()> {
851        self.depth += 1;
852        if budget::depth_exceeded(self.depth, self.config.max_depth) {
853            return Err(Error::RecursionLimitExceeded { depth: self.depth });
854        }
855        Ok(())
856    }
857
858    fn close_collection(&mut self) -> Result<()> {
859        self.depth = self.depth.saturating_sub(1);
860        let (value, anchor) = match self.stack.pop() {
861            Some(Frame::Sequence(s, a)) => (BorrowedValue::Sequence(s), a),
862            Some(Frame::MappingKey(m, a) | Frame::MappingValue(m, _, a)) => {
863                (BorrowedValue::Mapping(m), a)
864            }
865            None => return Err(Error::Invalid("unexpected collection end".to_string())),
866        };
867        self.record_anchor(anchor, &value);
868        self.push_value(value)
869    }
870
871    fn on_alias(&mut self, anchor: &str) -> Result<()> {
872        self.meter.charge_alias(self.config)?;
873        let (referent, cost) = self
874            .anchors
875            .get(anchor)
876            .ok_or_else(|| Error::Parse(format!("unknown anchor: '{anchor}'")))?;
877        self.meter.charge_expansion(cost, self.depth, self.config)?;
878        let referent = referent.clone();
879        // Special-case: alias used as a mapping key. We need the
880        // alias's resolved value to be a string for it to function as
881        // one, mirroring how YAML 1.2 treats key aliases on the owned
882        // path.
883        if let Some(Frame::MappingKey(..)) = self.stack.last() {
884            let key = alias_key(referent)?;
885            self.set_key(key);
886            return Ok(());
887        }
888        self.push_value(referent)
889    }
890}
891
892/// The key an alias stands for when it is used as a mapping key. A
893/// scalar renders as its text, matching the owned path's mapping-key
894/// coercion; a collection cannot be a key here.
895fn alias_key(referent: BorrowedValue<'_>) -> Result<Cow<'_, str>> {
896    match referent {
897        BorrowedValue::String(s) => Ok(s),
898        BorrowedValue::Bool(b) => Ok(Cow::Owned(b.to_string())),
899        BorrowedValue::Number(n) => Ok(Cow::Owned(n.to_string())),
900        BorrowedValue::Null => Ok(Cow::Borrowed("null")),
901        BorrowedValue::Sequence(_) | BorrowedValue::Mapping(_) => Err(Error::Invalid(
902            "alias resolved to a non-scalar cannot be used as a mapping key".to_string(),
903        )),
904    }
905}
906
907/// The expansion cost of an anchored borrowed value, by the estimator
908/// the owned loaders use. Walks without recursion.
909fn borrowed_cost(root: &BorrowedValue<'_>) -> AliasCost {
910    enum Step<'v, 'a> {
911        Visit(&'v BorrowedValue<'a>),
912        Close,
913    }
914    let mut tally = CostTally::default();
915    let mut pending = vec![Step::Visit(root)];
916    while let Some(step) = pending.pop() {
917        let value = match step {
918            Step::Visit(value) => value,
919            Step::Close => {
920                tally.close();
921                continue;
922            }
923        };
924        match value {
925            BorrowedValue::String(s) => tally.scalar(s.len()),
926            BorrowedValue::Sequence(items) => {
927                tally.open();
928                pending.push(Step::Close);
929                pending.extend(items.iter().map(Step::Visit));
930            }
931            BorrowedValue::Mapping(map) => {
932                tally.open();
933                pending.push(Step::Close);
934                for (key, item) in map {
935                    tally.scalar(key.len());
936                    pending.push(Step::Visit(item));
937                }
938            }
939            BorrowedValue::Null | BorrowedValue::Bool(_) | BorrowedValue::Number(_) => {
940                tally.scalar(0);
941            }
942        }
943    }
944    tally.finish()
945}
946
947/// Whether `tag` is YAML's core-schema string tag, in any of its
948/// spellings: `!!str`, the verbatim `tag:yaml.org,2002:str`, or the
949/// primary `!str`. Matches the owned loader's `resolve_tagged_scalar`.
950fn is_core_string_tag(tag: Option<&(String, String)>) -> bool {
951    tag.is_some_and(|(handle, suffix)| {
952        suffix == "str" && (handle == "!!" || handle == "tag:yaml.org,2002:" || handle == "!")
953    })
954}