Skip to main content

rac_engine/
frontmatter.rs

1//! Frontmatter parsing — port of `src/rac/core/frontmatter.py` plus the
2//! bounded PyYAML-1.1 SafeLoader subset it rides on (PORT-CONTRACT.d/02).
3//!
4//! This is parity landmine #1: the oracle is PyYAML 6.0.3's pure-Python
5//! `SafeLoader` (full YAML 1.1) subclassed with three guards — duplicate-key
6//! rejection, alias rejection, and a 32-level node-count depth cap. Byte
7//! parity requires reproducing PyYAML's implicit resolution, its error
8//! *problem* strings, and CPython `repr()` formatting inside issue messages.
9//! The scanner/parser/composer/constructor below are direct ports of the
10//! corresponding PyYAML modules (message strings verbatim).
11//!
12//! Known oracle crashes (PORT-CONTRACT decision 3): several inputs crash the
13//! oracle with uncaught non-YAML exceptions (unhashable mapping keys,
14//! explicit-tag/value mismatches like `!!int ''`, out-of-range dates such as
15//! `2026-13-01`, `!!map` on a non-empty scalar/sequence, and CPython's
16//! 4300-digit int<->str conversion limit). This port does NOT
17//! crash: every such path returns a distinguishable internal issue (code
18//! `internal-oracle-divergence`) whose message mirrors the Python exception
19//! (`"TypeError: unhashable type: 'list'"`, ...). The marker is intentional
20//! and the parity harness treats it as the documented divergence class.
21//!
22//! Integers are unbounded like Python's `int`: values
23//! beyond i64 construct `Yaml::BigInt` (sign + decimal digits) instead of
24//! overflowing, and duplicate-key equality / `repr()` / validator messages
25//! follow CPython semantics for them exactly.
26
27use std::collections::HashMap;
28
29use crate::pycompat::{py_float_repr, py_repr_str, py_strip};
30
31// ---------------------------------------------------------------------------
32// Limits (src/rac/core/limits.py)
33// ---------------------------------------------------------------------------
34
35pub const DEFAULT_MAX_FILE_BYTES: u64 = 1 << 20; // 1 MiB
36pub const MAX_FRONTMATTER_BYTES: usize = 64 << 10; // 64 KiB
37pub const MAX_FRONTMATTER_DEPTH: usize = 32;
38pub const SUPPORTED_SCHEMA_VERSIONS: &[i64] = &[1];
39
40const SUPPORTED_FIELDS: [&str; 5] = ["schema_version", "id", "type", "relationships", "tags"];
41
42/// `exceeds_byte_cap(text, cap)`: true when `text` exceeds `cap` UTF-8 bytes.
43/// (The oracle's char-count shortcuts are a pure optimization; Rust `len()`
44/// is already the UTF-8 byte length.)
45pub fn exceeds_byte_cap(text: &str, cap: usize) -> bool {
46    text.len() > cap
47}
48
49/// The per-file byte cap at the READ stage.
50///
51/// The oracle's `parse_file` runs `fh.read(cap + 1)`, which CRASHES the
52/// oracle uncaught for huge caps (PORT-CONTRACT decision-3 marker class),
53/// with the boundary verified empirically against CPython 3.11:
54///   - cap >= 2^63 - 1  (`cap + 1 > sys.maxsize`):
55///     `OverflowError: cannot fit 'int' into an index-sized integer`
56///   - 2^63 - 34 <= cap <= 2^63 - 2  (`cap + 1` exceeds the bytes-object
57///     size limit `PY_SSIZE_T_MAX - 33`):
58///     `OverflowError: byte string is too large`
59///   - below that, down to roughly the machine's allocatable memory, the
60///     oracle raises `MemoryError` — an ENVIRONMENT-DEPENDENT crash that
61///     cannot be mirrored deterministically and is deliberately NOT
62///     mirrored (the Rust engine reads incrementally and never
63///     preallocates the cap).
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum FileCap {
66    Cap(u64),
67    /// Every file READ crashes the oracle with this exception line.
68    OracleCrash(&'static str),
69}
70
71/// `cap + 1 > sys.maxsize`, i.e. cap >= 2^63 - 1.
72const ORACLE_READ_OVERFLOW_MIN: i128 = i64::MAX as i128;
73/// `cap + 1` over the CPython bytes allocation limit (PY_SSIZE_T_MAX - 33).
74const ORACLE_READ_TOOLARGE_MIN: i128 = i64::MAX as i128 - 33;
75
76/// The per-file byte cap, honoring `DECIDED_MAX_FILE_BYTES` — Python `int()`
77/// semantics (Unicode digits, underscores, unbounded magnitude; unparseable
78/// or non-positive overrides fall back to the default). Shared parser with
79/// `markdown::max_file_bytes_from` so the read and parse stages agree.
80pub fn file_cap() -> FileCap {
81    match std::env::var("DECIDED_MAX_FILE_BYTES") {
82        Ok(raw) => file_cap_from(Some(&raw)),
83        Err(_) => FileCap::Cap(DEFAULT_MAX_FILE_BYTES),
84    }
85}
86
87pub fn file_cap_from(raw: Option<&str>) -> FileCap {
88    if let Some(raw) = raw {
89        if let Some(v) = crate::markdown::py_parse_int(raw) {
90            if v >= ORACLE_READ_OVERFLOW_MIN {
91                return FileCap::OracleCrash(
92                    "OverflowError: cannot fit 'int' into an index-sized integer",
93                );
94            }
95            if v >= ORACLE_READ_TOOLARGE_MIN {
96                return FileCap::OracleCrash("OverflowError: byte string is too large");
97            }
98            if v > 0 {
99                return FileCap::Cap(v as u64);
100            }
101        }
102    }
103    FileCap::Cap(DEFAULT_MAX_FILE_BYTES)
104}
105
106// ---------------------------------------------------------------------------
107// Issue / metadata models (src/rac/core/models.py, metadata.py)
108// ---------------------------------------------------------------------------
109
110#[derive(Clone, Debug, PartialEq)]
111pub struct Issue {
112    pub severity: &'static str,
113    pub code: String,
114    pub message: String,
115    pub line: Option<i64>,
116}
117
118impl Issue {
119    fn error(code: &str, message: String) -> Issue {
120        Issue {
121            severity: "error",
122            code: code.to_string(),
123            message,
124            line: None,
125        }
126    }
127}
128
129/// `ArtifactMetadata.schema_version`: Python keeps the parsed int as-is,
130/// which can exceed i64 (an unsupported-but-integer version is stored with
131/// only an issue recorded). `Display` matches Python `str(int)`.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum SchemaVersion {
134    Int(i64),
135    Big(BigInt),
136}
137
138impl std::fmt::Display for SchemaVersion {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            SchemaVersion::Int(v) => write!(f, "{v}"),
142            SchemaVersion::Big(b) => write!(f, "{b}"),
143        }
144    }
145}
146
147#[derive(Clone, Debug, PartialEq)]
148pub struct ArtifactMetadata {
149    pub schema_version: SchemaVersion,
150    pub id: Option<String>,
151    pub artifact_type: Option<String>,
152    /// Ordered (kind -> targets), preserving YAML document order.
153    pub relationships: Vec<(String, Vec<String>)>,
154    pub tags: Vec<String>,
155    pub provenance: &'static str,
156}
157
158/// Canonical (uppercase) form of an artifact ID (Python `strip().upper()`).
159pub fn normalize_id(value: &str) -> String {
160    py_strip(value).to_uppercase()
161}
162
163fn is_crockford(c: char) -> bool {
164    matches!(c, '0'..='9' | 'A'..='H' | 'J' | 'K' | 'M' | 'N' | 'P'..='T' | 'V'..='Z')
165}
166
167/// `^[A-Z][A-Z0-9]{1,9}-[0-9A-HJKMNP-TV-Z]{12}$` over the normalized id.
168pub fn is_valid_id(value: &str) -> bool {
169    let n = normalize_id(value);
170    let chars: Vec<char> = n.chars().collect();
171    if chars.is_empty() || !chars[0].is_ascii_uppercase() {
172        return false;
173    }
174    // Backtrack over the {1,9} key tail exactly as the regex engine would.
175    for keylen in 1..=9usize {
176        let dash = 1 + keylen;
177        if chars.len() != dash + 13 {
178            continue;
179        }
180        if !chars[1..dash]
181            .iter()
182            .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
183        {
184            continue;
185        }
186        if chars[dash] != '-' {
187            continue;
188        }
189        if chars[dash + 1..].iter().all(|c| is_crockford(*c)) {
190            return true;
191        }
192    }
193    false
194}
195
196// ---------------------------------------------------------------------------
197// Value model — what PyYAML SafeLoader construction can yield here
198// ---------------------------------------------------------------------------
199
200/// Arbitrary-precision integer (sign + decimal digits), mirroring Python's
201/// unbounded `int` for values outside i64 (PORT-CONTRACT 02 §4: the YAML 1.1
202/// int constructor never overflows).
203///
204/// Invariant: the magnitude never fits i64 (smaller values construct
205/// `Yaml::Int`), and `digits` has no leading zeros, so cross-class equality
206/// with `Int`/`Bool` is always false and `BigInt` equality is digit equality.
207#[derive(Clone, Debug, PartialEq, Eq)]
208pub struct BigInt {
209    pub neg: bool,
210    /// Decimal magnitude digits, most significant first.
211    pub digits: String,
212}
213
214impl std::fmt::Display for BigInt {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        if self.neg {
217            f.write_str("-")?;
218        }
219        f.write_str(&self.digits)
220    }
221}
222
223#[derive(Clone, Debug, PartialEq)]
224pub enum Yaml {
225    Null,
226    Bool(bool),
227    Int(i64),
228    /// Python bignum — an int whose value is outside the i64 range.
229    BigInt(BigInt),
230    Float(f64),
231    Str(String),
232    Date {
233        year: i64,
234        month: u32,
235        day: u32,
236    },
237    DateTime {
238        year: i64,
239        month: u32,
240        day: u32,
241        hour: u32,
242        minute: u32,
243        second: u32,
244        micro: u32,
245        /// UTC offset in seconds; None = naive.
246        tz: Option<i64>,
247    },
248    Bytes(Vec<u8>),
249    List(Vec<Yaml>),
250    /// Python tuples (from `!!omap` / `!!pairs` entries).
251    Tuple(Vec<Yaml>),
252    /// Insertion-ordered mapping (Python dict preserves document order).
253    Map(Vec<(Yaml, Yaml)>),
254    /// From `!!set`; stored in first-occurrence order.
255    Set(Vec<Yaml>),
256}
257
258/// Python `==` over the constructed values: numeric cross-class equality
259/// (`1 == True == 1.0`), and NaN keys compare equal to each other because
260/// PyYAML returns the one shared `nan_value` object (identity hit in the
261/// oracle's duplicate-key `set`).
262pub fn py_eq(a: &Yaml, b: &Yaml) -> bool {
263    fn num(v: &Yaml) -> Option<f64> {
264        match v {
265            Yaml::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
266            Yaml::Int(i) => Some(*i as f64),
267            Yaml::Float(f) => Some(*f),
268            _ => None,
269        }
270    }
271    match (a, b) {
272        (Yaml::Null, Yaml::Null) => true,
273        (Yaml::Str(x), Yaml::Str(y)) => x == y,
274        (Yaml::Bytes(x), Yaml::Bytes(y)) => x == y,
275        (
276            Yaml::Date { year, month, day },
277            Yaml::Date {
278                year: y2,
279                month: m2,
280                day: d2,
281            },
282        ) => year == y2 && month == m2 && day == d2,
283        (Yaml::DateTime { .. }, Yaml::DateTime { .. }) => datetime_eq(a, b),
284        (Yaml::Tuple(x), Yaml::Tuple(y)) => {
285            x.len() == y.len() && x.iter().zip(y).all(|(p, q)| py_eq(p, q))
286        }
287        (Yaml::List(x), Yaml::List(y)) => {
288            x.len() == y.len() && x.iter().zip(y).all(|(p, q)| py_eq(p, q))
289        }
290        (Yaml::BigInt(x), Yaml::BigInt(y)) => x == y,
291        // Invariant: a BigInt never fits i64, so it can never equal an
292        // Int or Bool (Python compares the mathematical values).
293        (Yaml::BigInt(_), Yaml::Int(_) | Yaml::Bool(_))
294        | (Yaml::Int(_) | Yaml::Bool(_), Yaml::BigInt(_)) => false,
295        // Python bignum == float compares mathematically (exactly).
296        (Yaml::BigInt(x), Yaml::Float(f)) | (Yaml::Float(f), Yaml::BigInt(x)) => {
297            bigint_eq_float(x, *f)
298        }
299        (Yaml::BigInt(_), _) | (_, Yaml::BigInt(_)) => false,
300        _ => match (num(a), num(b)) {
301            (Some(x), Some(y)) => {
302                if x.is_nan() && y.is_nan() {
303                    // PyYAML nan_value identity (verified: `.nan:` twice is a dup).
304                    matches!((a, b), (Yaml::Float(_), Yaml::Float(_)))
305                } else {
306                    exact_num_eq(a, b, x, y)
307                }
308            }
309            _ => false,
310        },
311    }
312}
313
314/// Exact Python numeric equality across bool/int/float without f64 precision
315/// loss on large i64s.
316fn exact_num_eq(a: &Yaml, b: &Yaml, x: f64, y: f64) -> bool {
317    fn as_int(v: &Yaml) -> Option<i64> {
318        match v {
319            Yaml::Bool(b) => Some(*b as i64),
320            Yaml::Int(i) => Some(*i),
321            _ => None,
322        }
323    }
324    match (as_int(a), as_int(b)) {
325        (Some(i), Some(j)) => i == j,
326        (Some(i), None) => float_eq_int(y, i),
327        (None, Some(j)) => float_eq_int(x, j),
328        (None, None) => x == y,
329    }
330}
331
332fn float_eq_int(f: f64, i: i64) -> bool {
333    if !f.is_finite() || f != f.trunc() {
334        return false;
335    }
336    if !(-9.223_372_036_854_776E18..9.223_372_036_854_776E18).contains(&f) {
337        return false;
338    }
339    (f as i64) == i && (f as i64) as f64 == f
340}
341
342// ---------------------------------------------------------------------------
343// Arbitrary-precision decimal magnitude — just enough bignum for PyYAML int
344// construction and Python's exact cross-class numeric equality.
345// ---------------------------------------------------------------------------
346
347/// CPython's default `sys.get_int_max_str_digits()` (Python 3.11+): int<->str
348/// conversions beyond this many decimal digits raise `ValueError` — uncaught
349/// by the oracle, so decision-3 internal markers mirror them.
350const INT_MAX_STR_DIGITS: usize = 4300;
351
352/// `str(int)` / `repr(int)` over the limit (message has no digit count).
353fn int_to_str_limit_err() -> YErr {
354    YErr::Internal(
355        "ValueError: Exceeds the limit (4300 digits) for integer string conversion; \
356         use sys.set_int_max_str_digits() to increase the limit"
357            .to_string(),
358    )
359}
360
361/// `int(str)` (base 10 only) over the limit (message carries the count).
362fn int_parse_limit_err(ndigits: usize) -> YErr {
363    YErr::Internal(format!(
364        "ValueError: Exceeds the limit (4300 digits) for integer string conversion: \
365         value has {ndigits} digits; use sys.set_int_max_str_digits() to increase the limit"
366    ))
367}
368
369/// Unsigned magnitude in base 1e9 limbs, least significant first. Base 1e9
370/// keeps the arithmetic O(digits^2/9) worst case and makes the decimal
371/// rendering a straight concatenation.
372#[derive(Clone, Debug)]
373struct Mag(Vec<u32>);
374
375const MAG_BASE: u64 = 1_000_000_000;
376
377impl Mag {
378    fn zero() -> Mag {
379        Mag(vec![0])
380    }
381
382    fn from_u64(mut v: u64) -> Mag {
383        let mut limbs = Vec::new();
384        loop {
385            limbs.push((v % MAG_BASE) as u32);
386            v /= MAG_BASE;
387            if v == 0 {
388                break;
389            }
390        }
391        Mag(limbs)
392    }
393
394    fn is_zero(&self) -> bool {
395        self.0.iter().all(|&l| l == 0)
396    }
397
398    fn trim(&mut self) {
399        while self.0.len() > 1 && *self.0.last().unwrap() == 0 {
400            self.0.pop();
401        }
402    }
403
404    /// self = self * m + a  (m, a < 2^32).
405    fn mul_add_small(&mut self, m: u64, a: u64) {
406        let mut carry = a;
407        for limb in &mut self.0 {
408            let v = *limb as u64 * m + carry;
409            *limb = (v % MAG_BASE) as u32;
410            carry = v / MAG_BASE;
411        }
412        while carry > 0 {
413            self.0.push((carry % MAG_BASE) as u32);
414            carry /= MAG_BASE;
415        }
416        self.trim();
417    }
418
419    fn add(&mut self, other: &Mag) {
420        if other.0.len() > self.0.len() {
421            self.0.resize(other.0.len(), 0);
422        }
423        let mut carry = 0u64;
424        for (i, limb) in self.0.iter_mut().enumerate() {
425            let v = *limb as u64 + other.0.get(i).map_or(0, |&l| l as u64) + carry;
426            *limb = (v % MAG_BASE) as u32;
427            carry = v / MAG_BASE;
428        }
429        if carry > 0 {
430            self.0.push(carry as u32);
431        }
432    }
433
434    /// self - other; requires self >= other.
435    fn sub(&mut self, other: &Mag) {
436        let mut borrow = 0i64;
437        for (i, limb) in self.0.iter_mut().enumerate() {
438            let mut v = *limb as i64 - other.0.get(i).map_or(0, |&l| l as i64) - borrow;
439            if v < 0 {
440                v += MAG_BASE as i64;
441                borrow = 1;
442            } else {
443                borrow = 0;
444            }
445            *limb = v as u32;
446        }
447        debug_assert_eq!(borrow, 0, "Mag::sub underflow");
448        self.trim();
449    }
450
451    fn cmp_mag(&self, other: &Mag) -> std::cmp::Ordering {
452        let (mut a, mut b) = (self.clone(), other.clone());
453        a.trim();
454        b.trim();
455        if a.0.len() != b.0.len() {
456            return a.0.len().cmp(&b.0.len());
457        }
458        for (x, y) in a.0.iter().rev().zip(b.0.iter().rev()) {
459            if x != y {
460                return x.cmp(y);
461            }
462        }
463        std::cmp::Ordering::Equal
464    }
465
466    /// Decimal digits, most significant first, no leading zeros.
467    fn to_decimal(&self) -> String {
468        let mut limbs = self.0.clone();
469        while limbs.len() > 1 && *limbs.last().unwrap() == 0 {
470            limbs.pop();
471        }
472        let mut out = format!("{}", limbs.last().unwrap());
473        for limb in limbs.iter().rev().skip(1) {
474            out.push_str(&format!("{limb:09}"));
475        }
476        out
477    }
478
479    /// The signed value when it fits i64 (magnitude up to 2^63 when `neg`).
480    fn to_i64(&self, neg: bool) -> Option<i64> {
481        let mut acc: i64 = 0;
482        for &limb in self.0.iter().rev() {
483            acc = acc.checked_mul(MAG_BASE as i64)?;
484            acc = if neg {
485                acc.checked_sub(limb as i64)?
486            } else {
487                acc.checked_add(limb as i64)?
488            };
489        }
490        Some(acc)
491    }
492}
493
494/// The `Yaml` value for a signed magnitude: `Int` when it fits i64, else the
495/// exact Python bignum.
496fn yaml_int(neg: bool, mag: &Mag) -> Yaml {
497    match mag.to_i64(neg) {
498        Some(v) => Yaml::Int(v),
499        None => Yaml::BigInt(BigInt {
500            neg,
501            digits: mag.to_decimal(),
502        }),
503    }
504}
505
506/// Python `bignum == float` (exact): true iff the float is finite, integral,
507/// and its exact value has the same sign and decimal digits.
508fn bigint_eq_float(x: &BigInt, f: f64) -> bool {
509    if !f.is_finite() || f != f.trunc() || f == 0.0 {
510        return false;
511    }
512    if (f < 0.0) != x.neg {
513        return false;
514    }
515    f64_integral_magnitude(f.abs()) == x.digits
516}
517
518/// Exact decimal digits of a positive integral f64 (mantissa * 2^exp).
519fn f64_integral_magnitude(f: f64) -> String {
520    let bits = f.to_bits();
521    let exp = ((bits >> 52) & 0x7ff) as i64;
522    let frac = bits & ((1u64 << 52) - 1);
523    let (mut m, mut e) = if exp == 0 {
524        (frac, -1074i64)
525    } else {
526        (frac | (1 << 52), exp - 1075)
527    };
528    // Integral input: any negative exponent shifts out exactly.
529    while e < 0 {
530        m >>= 1;
531        e += 1;
532    }
533    let mut mag = Mag::from_u64(m);
534    for _ in 0..e {
535        mag.mul_add_small(2, 0);
536    }
537    mag.to_decimal()
538}
539
540fn datetime_eq(a: &Yaml, b: &Yaml) -> bool {
541    if let (
542        Yaml::DateTime {
543            year,
544            month,
545            day,
546            hour,
547            minute,
548            second,
549            micro,
550            tz,
551        },
552        Yaml::DateTime {
553            year: y2,
554            month: m2,
555            day: d2,
556            hour: h2,
557            minute: mi2,
558            second: s2,
559            micro: us2,
560            tz: tz2,
561        },
562    ) = (a, b)
563    {
564        match (tz, tz2) {
565            (None, None) => {
566                (year, month, day, hour, minute, second, micro)
567                    == (y2, m2, d2, h2, mi2, s2, us2)
568            }
569            (Some(o1), Some(o2)) => {
570                let u1 = utc_micros(*year, *month, *day, *hour, *minute, *second, *micro) - o1 * 1_000_000;
571                let u2 = utc_micros(*y2, *m2, *d2, *h2, *mi2, *s2, *us2) - o2 * 1_000_000;
572                u1 == u2
573            }
574            // Python 3: mixed naive/aware equality is False.
575            _ => false,
576        }
577    } else {
578        false
579    }
580}
581
582fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
583    // Howard Hinnant's algorithm; proleptic Gregorian.
584    let y = if m <= 2 { y - 1 } else { y };
585    let era = if y >= 0 { y } else { y - 399 } / 400;
586    let yoe = y - era * 400;
587    let mp = (m + 9) % 12;
588    let doy = (153 * mp + 2) / 5 + d - 1;
589    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
590    era * 146_097 + doe - 719_468
591}
592
593fn utc_micros(y: i64, mo: u32, d: u32, h: u32, mi: u32, s: u32, us: u32) -> i64 {
594    let days = days_from_civil(y, mo as i64, d as i64);
595    ((days * 86_400) + h as i64 * 3600 + mi as i64 * 60 + s as i64) * 1_000_000 + us as i64
596}
597
598/// If hashing `v` would raise `TypeError` in CPython, the offending type
599/// name (`list` / `dict` / `set`), recursing through tuples.
600fn unhashable_type_name(v: &Yaml) -> Option<&'static str> {
601    match v {
602        Yaml::List(_) => Some("list"),
603        Yaml::Map(_) => Some("dict"),
604        Yaml::Set(_) => Some("set"),
605        Yaml::Tuple(items) => items.iter().find_map(unhashable_type_name),
606        _ => None,
607    }
608}
609
610// ---------------------------------------------------------------------------
611// Python repr() of constructed values (drives `{key!r}` in issue messages)
612// ---------------------------------------------------------------------------
613
614fn py_repr_bytes(b: &[u8]) -> String {
615    let has_sq = b.contains(&b'\'');
616    let has_dq = b.contains(&b'"');
617    let quote = if has_sq && !has_dq { b'"' } else { b'\'' };
618    let mut out = String::from("b");
619    out.push(quote as char);
620    for &c in b {
621        if c == quote || c == b'\\' {
622            out.push('\\');
623            out.push(c as char);
624        } else if c == b'\t' {
625            out.push_str("\\t");
626        } else if c == b'\n' {
627            out.push_str("\\n");
628        } else if c == b'\r' {
629            out.push_str("\\r");
630        } else if (0x20..0x7f).contains(&c) {
631            out.push(c as char);
632        } else {
633            out.push_str(&format!("\\x{c:02x}"));
634        }
635    }
636    out.push(quote as char);
637    out
638}
639
640fn py_repr_timedelta(offset_seconds: i64) -> String {
641    // datetime.timedelta normalizes to 0 <= seconds < 86400.
642    let days = offset_seconds.div_euclid(86_400);
643    let secs = offset_seconds.rem_euclid(86_400);
644    let mut parts: Vec<String> = Vec::new();
645    if days != 0 {
646        parts.push(format!("days={days}"));
647    }
648    if secs != 0 {
649        parts.push(format!("seconds={secs}"));
650    }
651    if parts.is_empty() {
652        "datetime.timedelta(0)".to_string()
653    } else {
654        format!("datetime.timedelta({})", parts.join(", "))
655    }
656}
657
658fn py_repr_tzinfo(offset_seconds: i64) -> String {
659    if offset_seconds == 0 {
660        // timezone(timedelta(0)) is the utc singleton (verified).
661        "datetime.timezone.utc".to_string()
662    } else {
663        format!("datetime.timezone({})", py_repr_timedelta(offset_seconds))
664    }
665}
666
667/// CPython `repr()` for every value the loader can produce. Fallible: a
668/// bignum beyond the 4300-digit conversion limit raises `ValueError` in the
669/// oracle (uncaught — decision-3 internal marker), at any nesting depth.
670fn py_repr(v: &Yaml) -> Result<String, YErr> {
671    fn join(items: &[Yaml]) -> Result<String, YErr> {
672        Ok(items
673            .iter()
674            .map(py_repr)
675            .collect::<Result<Vec<_>, _>>()?
676            .join(", "))
677    }
678    Ok(match v {
679        Yaml::Null => "None".to_string(),
680        Yaml::Bool(true) => "True".to_string(),
681        Yaml::Bool(false) => "False".to_string(),
682        Yaml::Int(i) => i.to_string(),
683        Yaml::BigInt(b) => {
684            if b.digits.len() > INT_MAX_STR_DIGITS {
685                return Err(int_to_str_limit_err());
686            }
687            b.to_string()
688        }
689        Yaml::Float(f) => py_float_repr(*f),
690        Yaml::Str(s) => py_repr_str(s),
691        Yaml::Date { year, month, day } => {
692            format!("datetime.date({year}, {month}, {day})")
693        }
694        Yaml::DateTime {
695            year,
696            month,
697            day,
698            hour,
699            minute,
700            second,
701            micro,
702            tz,
703        } => {
704            let mut out = format!("datetime.datetime({year}, {month}, {day}, {hour}, {minute}");
705            if *second != 0 || *micro != 0 {
706                out.push_str(&format!(", {second}"));
707            }
708            if *micro != 0 {
709                out.push_str(&format!(", {micro}"));
710            }
711            if let Some(off) = tz {
712                out.push_str(&format!(", tzinfo={}", py_repr_tzinfo(*off)));
713            }
714            out.push(')');
715            out
716        }
717        Yaml::Bytes(b) => py_repr_bytes(b),
718        Yaml::Tuple(items) => match items.len() {
719            0 => "()".to_string(),
720            1 => format!("({},)", py_repr(&items[0])?),
721            _ => format!("({})", join(items)?),
722        },
723        Yaml::List(items) => format!("[{}]", join(items)?),
724        Yaml::Map(pairs) => format!(
725            "{{{}}}",
726            pairs
727                .iter()
728                .map(|(k, v)| Ok(format!("{}: {}", py_repr(k)?, py_repr(v)?)))
729                .collect::<Result<Vec<_>, YErr>>()?
730                .join(", ")
731        ),
732        Yaml::Set(items) => {
733            if items.is_empty() {
734                "set()".to_string()
735            } else {
736                format!("{{{}}}", join(items)?)
737            }
738        }
739    })
740}
741
742// ---------------------------------------------------------------------------
743// split_frontmatter
744// ---------------------------------------------------------------------------
745
746#[derive(Clone, Debug, PartialEq)]
747pub struct FrontmatterSplit {
748    pub raw: Option<String>,
749    pub body: String,
750    pub line_offset: usize,
751    pub unterminated: bool,
752}
753
754/// Split a leading `---` frontmatter block from `text` — LF-only line split
755/// (CRLF leaves `\r` in `raw`), Python-whitespace `.strip()` on delimiter
756/// lines (BOM and U+200B are NOT whitespace and defeat the delimiter).
757pub fn split_frontmatter(text: &str) -> FrontmatterSplit {
758    let lines: Vec<&str> = text.split('\n').collect();
759    if py_strip(lines[0]) != "---" {
760        return FrontmatterSplit {
761            raw: None,
762            body: text.to_string(),
763            line_offset: 0,
764            unterminated: false,
765        };
766    }
767    for i in 1..lines.len() {
768        let stripped = py_strip(lines[i]);
769        if stripped == "---" || stripped == "..." {
770            return FrontmatterSplit {
771                raw: Some(lines[1..i].join("\n")),
772                body: lines[i + 1..].join("\n"),
773                line_offset: i + 1,
774                unterminated: false,
775            };
776        }
777    }
778    FrontmatterSplit {
779        raw: None,
780        body: text.to_string(),
781        line_offset: 0,
782        unterminated: true,
783    }
784}
785
786// ---------------------------------------------------------------------------
787// YAML engine error type
788// ---------------------------------------------------------------------------
789
790#[derive(Clone, Debug)]
791enum YErr {
792    /// MarkedYAMLError — only the `problem` string reaches output.
793    Marked(String),
794    /// yaml.reader.ReaderError — the full multi-line `str(exc)`.
795    Reader(String),
796    /// The oracle would crash with an uncaught non-YAML exception here
797    /// (PORT-CONTRACT decision 3). Message mirrors `f"{type}: {exc}"`.
798    Internal(String),
799}
800
801// ---------------------------------------------------------------------------
802// YAML 1.1 implicit resolver (yaml/resolver.py, regexes matched by hand —
803// no regex crate in the workspace)
804// ---------------------------------------------------------------------------
805
806const TAG_STR: &str = "tag:yaml.org,2002:str";
807const TAG_SEQ: &str = "tag:yaml.org,2002:seq";
808const TAG_MAP: &str = "tag:yaml.org,2002:map";
809const TAG_BOOL: &str = "tag:yaml.org,2002:bool";
810const TAG_INT: &str = "tag:yaml.org,2002:int";
811const TAG_FLOAT: &str = "tag:yaml.org,2002:float";
812const TAG_MERGE: &str = "tag:yaml.org,2002:merge";
813const TAG_NULL: &str = "tag:yaml.org,2002:null";
814const TAG_TIMESTAMP: &str = "tag:yaml.org,2002:timestamp";
815const TAG_VALUE: &str = "tag:yaml.org,2002:value";
816
817fn match_bool(v: &str) -> bool {
818    matches!(
819        v,
820        "yes" | "Yes" | "YES" | "no" | "No" | "NO" | "true" | "True" | "TRUE" | "false"
821            | "False" | "FALSE" | "on" | "On" | "ON" | "off" | "Off" | "OFF"
822    )
823}
824
825fn eat_sign(b: &[u8]) -> &[u8] {
826    if !b.is_empty() && (b[0] == b'-' || b[0] == b'+') {
827        &b[1..]
828    } else {
829        b
830    }
831}
832
833fn all_in(b: &[u8], pred: impl Fn(u8) -> bool) -> bool {
834    b.iter().all(|&c| pred(c))
835}
836
837/// `(?:[eE][-+][0-9]+)?` — returns rest after consuming an exponent (which
838/// must have a sign), or None if a malformed exponent-like tail is present.
839fn strip_signed_exponent(b: &[u8]) -> Option<&[u8]> {
840    if b.is_empty() {
841        return Some(b);
842    }
843    if b[0] == b'e' || b[0] == b'E' {
844        if b.len() >= 3 && (b[1] == b'-' || b[1] == b'+') && b[2..].iter().all(u8::is_ascii_digit)
845        {
846            Some(&b[..0])
847        } else {
848            None
849        }
850    } else {
851        Some(b)
852    }
853}
854
855fn match_float(v: &str) -> bool {
856    let b = v.as_bytes();
857    if !b.is_ascii() {
858        return false;
859    }
860    // [-+]?\.(?:inf|Inf|INF)
861    {
862        let t = eat_sign(b);
863        if t == b".inf" || t == b".Inf" || t == b".INF" {
864            return true;
865        }
866    }
867    // \.(?:nan|NaN|NAN)  (no sign)
868    if b == b".nan" || b == b".NaN" || b == b".NAN" {
869        return true;
870    }
871    // \.[0-9][0-9_]*(?:[eE][-+][0-9]+)?  (no sign)
872    if b.first() == Some(&b'.') {
873        let t = &b[1..];
874        if !t.is_empty() && t[0].is_ascii_digit() {
875            let end = t
876                .iter()
877                .position(|&c| !(c.is_ascii_digit() || c == b'_'))
878                .unwrap_or(t.len());
879            if let Some(rest) = strip_signed_exponent(&t[end..]) {
880                if rest.is_empty() {
881                    return true;
882                }
883            }
884        }
885    }
886    let t = eat_sign(b);
887    if t.is_empty() || !t[0].is_ascii_digit() {
888        return false;
889    }
890    // [-+]?[0-9][0-9_]*\.[0-9_]*(?:[eE][-+][0-9]+)?
891    // [-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
892    let int_end = t
893        .iter()
894        .position(|&c| !(c.is_ascii_digit() || c == b'_'))
895        .unwrap_or(t.len());
896    let rest = &t[int_end..];
897    if rest.first() == Some(&b'.') {
898        let frac = &rest[1..];
899        let frac_end = frac
900            .iter()
901            .position(|&c| !(c.is_ascii_digit() || c == b'_'))
902            .unwrap_or(frac.len());
903        if let Some(after) = strip_signed_exponent(&frac[frac_end..]) {
904            return after.is_empty();
905        }
906        return false;
907    }
908    if rest.first() == Some(&b':') {
909        // (?::[0-5]?[0-9])+\.[0-9_]*  — sexagesimal float, no exponent.
910        let mut r = rest;
911        loop {
912            if r.first() != Some(&b':') {
913                break;
914            }
915            r = &r[1..];
916            let mut ndig = 0;
917            if r.first().is_some_and(|c| (b'0'..=b'5').contains(c))
918                && r.get(1).is_some_and(u8::is_ascii_digit)
919            {
920                ndig = 2;
921            } else if r.first().is_some_and(u8::is_ascii_digit) {
922                ndig = 1;
923            }
924            if ndig == 0 {
925                return false;
926            }
927            r = &r[ndig..];
928        }
929        if r.first() != Some(&b'.') {
930            return false;
931        }
932        return all_in(&r[1..], |c| c.is_ascii_digit() || c == b'_');
933    }
934    false
935}
936
937fn match_int(v: &str) -> bool {
938    let b = v.as_bytes();
939    if !b.is_ascii() {
940        return false;
941    }
942    let t = eat_sign(b);
943    if t.is_empty() {
944        return false;
945    }
946    // 0b[0-1_]+
947    if t.len() > 2 && t.starts_with(b"0b") && all_in(&t[2..], |c| c == b'0' || c == b'1' || c == b'_') {
948        return true;
949    }
950    // 0x[0-9a-fA-F_]+
951    if t.len() > 2 && t.starts_with(b"0x") && all_in(&t[2..], |c| c.is_ascii_hexdigit() || c == b'_')
952    {
953        return true;
954    }
955    // 0[0-7_]+
956    if t.len() > 1 && t[0] == b'0' && all_in(&t[1..], |c| (b'0'..=b'7').contains(&c) || c == b'_') {
957        return true;
958    }
959    // 0 | [1-9][0-9_]*
960    if t == b"0" {
961        return true;
962    }
963    if t[0].is_ascii_digit() && t[0] != b'0' {
964        let end = t
965            .iter()
966            .position(|&c| !(c.is_ascii_digit() || c == b'_'))
967            .unwrap_or(t.len());
968        if end == t.len() {
969            return true;
970        }
971        // [1-9][0-9_]*(?::[0-5]?[0-9])+  — sexagesimal int
972        let mut r = &t[end..];
973        if r.first() != Some(&b':') {
974            return false;
975        }
976        while r.first() == Some(&b':') {
977            r = &r[1..];
978            let ndig = if r.first().is_some_and(|c| (b'0'..=b'5').contains(c))
979                && r.get(1).is_some_and(u8::is_ascii_digit)
980            {
981                2
982            } else if r.first().is_some_and(u8::is_ascii_digit) {
983                1
984            } else {
985                return false;
986            };
987            r = &r[ndig..];
988        }
989        return r.is_empty();
990    }
991    false
992}
993
994fn match_null(v: &str) -> bool {
995    matches!(v, "~" | "null" | "Null" | "NULL" | "")
996}
997
998/// The timestamp *resolver* regex (stricter than the constructor's).
999fn match_timestamp(v: &str) -> bool {
1000    let b = v.as_bytes();
1001    if !b.is_ascii() {
1002        return false;
1003    }
1004    let d = |i: usize| b.get(i).is_some_and(u8::is_ascii_digit);
1005    // [0-9]{4}-[0-9]{2}-[0-9]{2}  (date-only, fixed width)
1006    if b.len() == 10
1007        && d(0)
1008        && d(1)
1009        && d(2)
1010        && d(3)
1011        && b[4] == b'-'
1012        && d(5)
1013        && d(6)
1014        && b[7] == b'-'
1015        && d(8)
1016        && d(9)
1017    {
1018        return true;
1019    }
1020    // Full form: [0-9]{4}-[0-9]{1,2}-[0-9]{1,2}([Tt]|[ \t]+)[0-9]{1,2}:[0-9]{2}:[0-9]{2}(\.[0-9]*)?([ \t]*(Z|[-+][0-9]{1,2}(:[0-9]{2})?))?
1021    let mut i = 0;
1022    if !(d(0) && d(1) && d(2) && d(3)) {
1023        return false;
1024    }
1025    i += 4;
1026    if b.get(i) != Some(&b'-') {
1027        return false;
1028    }
1029    i += 1;
1030    if !d(i) {
1031        return false;
1032    }
1033    i += 1;
1034    if d(i) {
1035        i += 1;
1036    }
1037    if b.get(i) != Some(&b'-') {
1038        return false;
1039    }
1040    i += 1;
1041    if !d(i) {
1042        return false;
1043    }
1044    i += 1;
1045    if d(i) {
1046        i += 1;
1047    }
1048    // separator
1049    match b.get(i) {
1050        Some(b'T') | Some(b't') => i += 1,
1051        Some(b' ') | Some(b'\t') => {
1052            while matches!(b.get(i), Some(b' ') | Some(b'\t')) {
1053                i += 1;
1054            }
1055        }
1056        _ => return false,
1057    }
1058    if !d(i) {
1059        return false;
1060    }
1061    i += 1;
1062    if d(i) {
1063        i += 1;
1064    }
1065    if b.get(i) != Some(&b':') || !(d(i + 1) && d(i + 2)) {
1066        return false;
1067    }
1068    i += 3;
1069    if b.get(i) != Some(&b':') || !(d(i + 1) && d(i + 2)) {
1070        return false;
1071    }
1072    i += 3;
1073    if b.get(i) == Some(&b'.') {
1074        i += 1;
1075        while d(i) {
1076            i += 1;
1077        }
1078    }
1079    if i == b.len() {
1080        return true;
1081    }
1082    while matches!(b.get(i), Some(b' ') | Some(b'\t')) {
1083        i += 1;
1084    }
1085    match b.get(i) {
1086        Some(b'Z') => i += 1,
1087        Some(b'-') | Some(b'+') => {
1088            i += 1;
1089            if !d(i) {
1090                return false;
1091            }
1092            i += 1;
1093            if d(i) {
1094                i += 1;
1095            }
1096            if b.get(i) == Some(&b':') {
1097                if !(d(i + 1) && d(i + 2)) {
1098                    return false;
1099                }
1100                i += 3;
1101            }
1102        }
1103        _ => return false,
1104    }
1105    i == b.len()
1106}
1107
1108/// PyYAML implicit resolution for a plain scalar (registration order per
1109/// first-char trigger set; falls through to `!!str`).
1110fn resolve_plain(value: &str) -> &'static str {
1111    let first = value.chars().next();
1112    let candidates: &[&str] = match first {
1113        None => &[TAG_NULL],
1114        Some(c) => match c {
1115            'y' | 'Y' | 't' | 'T' | 'f' | 'F' | 'o' | 'O' => &[TAG_BOOL],
1116            'n' | 'N' => &[TAG_BOOL, TAG_NULL],
1117            '-' | '+' => &[TAG_FLOAT, TAG_INT],
1118            '0'..='9' => &[TAG_FLOAT, TAG_INT, TAG_TIMESTAMP],
1119            '.' => &[TAG_FLOAT],
1120            '<' => &[TAG_MERGE],
1121            '~' => &[TAG_NULL],
1122            '=' => &[TAG_VALUE],
1123            _ => &[],
1124        },
1125    };
1126    for tag in candidates {
1127        let hit = match *tag {
1128            TAG_BOOL => match_bool(value),
1129            TAG_FLOAT => match_float(value),
1130            TAG_INT => match_int(value),
1131            TAG_MERGE => value == "<<",
1132            TAG_NULL => match_null(value),
1133            TAG_TIMESTAMP => match_timestamp(value),
1134            TAG_VALUE => value == "=",
1135            _ => false,
1136        };
1137        if hit {
1138            return tag;
1139        }
1140    }
1141    TAG_STR
1142}
1143
1144// ---------------------------------------------------------------------------
1145// Reader (yaml/reader.py) — stream of chars with '\0' sentinel
1146// ---------------------------------------------------------------------------
1147
1148fn yaml_printable(c: char) -> bool {
1149    matches!(c,
1150        '\t' | '\n' | '\r' | '\x20'..='\x7e' | '\u{85}'
1151        | '\u{a0}'..='\u{d7ff}' | '\u{e000}'..='\u{fffd}' | '\u{10000}'..='\u{10ffff}')
1152}
1153
1154fn check_printable(data: &str) -> Result<(), YErr> {
1155    for (pos, c) in data.chars().enumerate() {
1156        // stdin surrogateescape sentinel: the oracle holds a lone surrogate
1157        // here (never yaml-printable) and reports ITS code point.
1158        if let Some(sur) = crate::pycompat::sentinel_surrogate(c) {
1159            return Err(YErr::Reader(format!(
1160                "unacceptable character #x{sur:04x}: special characters are not allowed\n  in \"<unicode string>\", position {pos}",
1161            )));
1162        }
1163        if !yaml_printable(c) {
1164            return Err(YErr::Reader(format!(
1165                "unacceptable character #x{:04x}: special characters are not allowed\n  in \"<unicode string>\", position {}",
1166                c as u32, pos
1167            )));
1168        }
1169    }
1170    Ok(())
1171}
1172
1173// ---------------------------------------------------------------------------
1174// Tokens (yaml/tokens.py)
1175// ---------------------------------------------------------------------------
1176
1177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1178enum TK {
1179    StreamStart,
1180    StreamEnd,
1181    Directive,
1182    DocumentStart,
1183    DocumentEnd,
1184    BlockSequenceStart,
1185    BlockMappingStart,
1186    BlockEnd,
1187    FlowSequenceStart,
1188    FlowMappingStart,
1189    FlowSequenceEnd,
1190    FlowMappingEnd,
1191    BlockEntry,
1192    FlowEntry,
1193    Key,
1194    Value,
1195    Alias,
1196    Anchor,
1197    Tag,
1198    Scalar,
1199}
1200
1201#[derive(Clone, Debug)]
1202enum DirectiveVal {
1203    Yaml { major_is_1: bool },
1204    Tag { handle: String, prefix: String },
1205    Other,
1206}
1207
1208#[derive(Clone, Debug)]
1209struct Tok {
1210    kind: TK,
1211    /// Scalar value / anchor / alias name.
1212    value: String,
1213    plain: bool,
1214    /// Tag token: (handle, suffix).
1215    tag: Option<(Option<String>, String)>,
1216    directive: Option<DirectiveVal>,
1217}
1218
1219impl Tok {
1220    fn simple(kind: TK) -> Tok {
1221        Tok {
1222            kind,
1223            value: String::new(),
1224            plain: false,
1225            tag: None,
1226            directive: None,
1227        }
1228    }
1229
1230    /// `token.id` strings used inside parser error messages.
1231    fn id(&self) -> &'static str {
1232        match self.kind {
1233            TK::StreamStart => "<stream start>",
1234            TK::StreamEnd => "<stream end>",
1235            TK::Directive => "<directive>",
1236            TK::DocumentStart => "<document start>",
1237            TK::DocumentEnd => "<document end>",
1238            TK::BlockSequenceStart => "<block sequence start>",
1239            TK::BlockMappingStart => "<block mapping start>",
1240            TK::BlockEnd => "<block end>",
1241            TK::FlowSequenceStart => "[",
1242            TK::FlowMappingStart => "{",
1243            TK::FlowSequenceEnd => "]",
1244            TK::FlowMappingEnd => "}",
1245            TK::BlockEntry => "-",
1246            TK::FlowEntry => ",",
1247            TK::Key => "?",
1248            TK::Value => ":",
1249            TK::Alias => "<alias>",
1250            TK::Anchor => "<anchor>",
1251            TK::Tag => "<tag>",
1252            TK::Scalar => "<scalar>",
1253        }
1254    }
1255}
1256
1257// ---------------------------------------------------------------------------
1258// Scanner (yaml/scanner.py) — faithful port; error strings verbatim
1259// ---------------------------------------------------------------------------
1260
1261#[derive(Clone, Copy)]
1262struct SimpleKey {
1263    token_number: usize,
1264    required: bool,
1265    index: usize,
1266    line: usize,
1267    column: i64,
1268}
1269
1270struct Scanner {
1271    buf: Vec<char>, // input + '\0' sentinel
1272    pointer: usize,
1273    index: usize,
1274    line: usize,
1275    column: i64,
1276    done: bool,
1277    flow_level: usize,
1278    tokens: std::collections::VecDeque<Tok>,
1279    tokens_taken: usize,
1280    indent: i64,
1281    indents: Vec<i64>,
1282    allow_simple_key: bool,
1283    possible_simple_keys: HashMap<usize, SimpleKey>,
1284}
1285
1286fn is_break(c: char) -> bool {
1287    matches!(c, '\r' | '\n' | '\u{85}' | '\u{2028}' | '\u{2029}')
1288}
1289
1290fn is_z_ws_break(c: char) -> bool {
1291    matches!(c, '\0' | ' ' | '\t') || is_break(c)
1292}
1293
1294fn is_z_break(c: char) -> bool {
1295    c == '\0' || is_break(c)
1296}
1297
1298fn is_word_char(c: char) -> bool {
1299    c.is_ascii_alphanumeric() || c == '-' || c == '_'
1300}
1301
1302fn repr_char(c: char) -> String {
1303    py_repr_str(&c.to_string())
1304}
1305
1306impl Scanner {
1307    fn new(data: &str) -> Scanner {
1308        let mut buf: Vec<char> = data.chars().collect();
1309        buf.push('\0');
1310        let mut s = Scanner {
1311            buf,
1312            pointer: 0,
1313            index: 0,
1314            line: 0,
1315            column: 0,
1316            done: false,
1317            flow_level: 0,
1318            tokens: std::collections::VecDeque::new(),
1319            tokens_taken: 0,
1320            indent: -1,
1321            indents: Vec::new(),
1322            allow_simple_key: true,
1323            possible_simple_keys: HashMap::new(),
1324        };
1325        s.tokens.push_back(Tok::simple(TK::StreamStart));
1326        s
1327    }
1328
1329    fn peek(&self, k: usize) -> char {
1330        self.buf.get(self.pointer + k).copied().unwrap_or('\0')
1331    }
1332
1333    fn prefix(&self, l: usize) -> String {
1334        let end = (self.pointer + l).min(self.buf.len());
1335        self.buf[self.pointer..end].iter().collect()
1336    }
1337
1338    fn forward(&mut self, l: usize) {
1339        for _ in 0..l {
1340            let ch = self.buf.get(self.pointer).copied().unwrap_or('\0');
1341            self.pointer += 1;
1342            self.index += 1;
1343            let next = self.buf.get(self.pointer).copied().unwrap_or('\0');
1344            if matches!(ch, '\n' | '\u{85}' | '\u{2028}' | '\u{2029}')
1345                || (ch == '\r' && next != '\n')
1346            {
1347                self.line += 1;
1348                self.column = 0;
1349            } else if ch != '\u{feff}' {
1350                self.column += 1;
1351            }
1352        }
1353    }
1354
1355    fn skip_spaces(&mut self) {
1356        while self.peek(0) == ' ' {
1357            self.forward(1);
1358        }
1359    }
1360
1361    // --- public token interface -------------------------------------------
1362
1363    fn need_more_tokens(&mut self) -> Result<bool, YErr> {
1364        if self.done {
1365            return Ok(false);
1366        }
1367        if self.tokens.is_empty() {
1368            return Ok(true);
1369        }
1370        self.stale_possible_simple_keys()?;
1371        if self.next_possible_simple_key() == Some(self.tokens_taken) {
1372            return Ok(true);
1373        }
1374        Ok(false)
1375    }
1376
1377    fn ensure(&mut self) -> Result<(), YErr> {
1378        while self.need_more_tokens()? {
1379            self.fetch_more_tokens()?;
1380        }
1381        Ok(())
1382    }
1383
1384    fn check_token(&mut self, choices: &[TK]) -> Result<bool, YErr> {
1385        self.ensure()?;
1386        if let Some(t) = self.tokens.front() {
1387            if choices.is_empty() {
1388                return Ok(true);
1389            }
1390            return Ok(choices.contains(&t.kind));
1391        }
1392        Ok(false)
1393    }
1394
1395    fn peek_token(&mut self) -> Result<Tok, YErr> {
1396        self.ensure()?;
1397        self.tokens
1398            .front()
1399            .cloned()
1400            .ok_or_else(|| YErr::Internal("IndexError: peek past stream end".to_string()))
1401    }
1402
1403    fn get_token(&mut self) -> Result<Tok, YErr> {
1404        self.ensure()?;
1405        match self.tokens.pop_front() {
1406            Some(t) => {
1407                self.tokens_taken += 1;
1408                Ok(t)
1409            }
1410            None => Err(YErr::Internal("IndexError: get past stream end".to_string())),
1411        }
1412    }
1413
1414    // --- simple keys --------------------------------------------------------
1415
1416    fn next_possible_simple_key(&self) -> Option<usize> {
1417        self.possible_simple_keys
1418            .values()
1419            .map(|k| k.token_number)
1420            .min()
1421    }
1422
1423    fn stale_possible_simple_keys(&mut self) -> Result<(), YErr> {
1424        let levels: Vec<usize> = self.possible_simple_keys.keys().copied().collect();
1425        for level in levels {
1426            let key = self.possible_simple_keys[&level];
1427            if key.line != self.line || self.index - key.index > 1024 {
1428                if key.required {
1429                    return Err(YErr::Marked("could not find expected ':'".to_string()));
1430                }
1431                self.possible_simple_keys.remove(&level);
1432            }
1433        }
1434        Ok(())
1435    }
1436
1437    fn save_possible_simple_key(&mut self) -> Result<(), YErr> {
1438        let required = self.flow_level == 0 && self.indent == self.column;
1439        if self.allow_simple_key {
1440            self.remove_possible_simple_key()?;
1441            let token_number = self.tokens_taken + self.tokens.len();
1442            self.possible_simple_keys.insert(
1443                self.flow_level,
1444                SimpleKey {
1445                    token_number,
1446                    required,
1447                    index: self.index,
1448                    line: self.line,
1449                    column: self.column,
1450                },
1451            );
1452        }
1453        Ok(())
1454    }
1455
1456    fn remove_possible_simple_key(&mut self) -> Result<(), YErr> {
1457        if let Some(key) = self.possible_simple_keys.get(&self.flow_level) {
1458            if key.required {
1459                return Err(YErr::Marked("could not find expected ':'".to_string()));
1460            }
1461            self.possible_simple_keys.remove(&self.flow_level);
1462        }
1463        Ok(())
1464    }
1465
1466    // --- indentation --------------------------------------------------------
1467
1468    fn unwind_indent(&mut self, column: i64) {
1469        if self.flow_level != 0 {
1470            return;
1471        }
1472        while self.indent > column {
1473            self.indent = self.indents.pop().unwrap_or(-1);
1474            self.tokens.push_back(Tok::simple(TK::BlockEnd));
1475        }
1476    }
1477
1478    fn add_indent(&mut self, column: i64) -> bool {
1479        if self.indent < column {
1480            self.indents.push(self.indent);
1481            self.indent = column;
1482            true
1483        } else {
1484            false
1485        }
1486    }
1487
1488    // --- fetchers -----------------------------------------------------------
1489
1490    fn fetch_more_tokens(&mut self) -> Result<(), YErr> {
1491        self.scan_to_next_token();
1492        self.stale_possible_simple_keys()?;
1493        self.unwind_indent(self.column);
1494        let ch = self.peek(0);
1495        if ch == '\0' {
1496            return self.fetch_stream_end();
1497        }
1498        if ch == '%' && self.check_directive() {
1499            return self.fetch_directive();
1500        }
1501        if ch == '-' && self.check_document_indicator("---") {
1502            return self.fetch_document_indicator(TK::DocumentStart);
1503        }
1504        if ch == '.' && self.check_document_indicator("...") {
1505            return self.fetch_document_indicator(TK::DocumentEnd);
1506        }
1507        match ch {
1508            '[' => return self.fetch_flow_collection_start(TK::FlowSequenceStart),
1509            '{' => return self.fetch_flow_collection_start(TK::FlowMappingStart),
1510            ']' => return self.fetch_flow_collection_end(TK::FlowSequenceEnd),
1511            '}' => return self.fetch_flow_collection_end(TK::FlowMappingEnd),
1512            ',' => return self.fetch_flow_entry(),
1513            _ => {}
1514        }
1515        if ch == '-' && is_z_ws_break(self.peek(1)) {
1516            return self.fetch_block_entry();
1517        }
1518        if ch == '?' && (self.flow_level != 0 || is_z_ws_break(self.peek(1))) {
1519            return self.fetch_key();
1520        }
1521        if ch == ':' && (self.flow_level != 0 || is_z_ws_break(self.peek(1))) {
1522            return self.fetch_value();
1523        }
1524        if ch == '*' {
1525            return self.fetch_anchor_or_alias(TK::Alias);
1526        }
1527        if ch == '&' {
1528            return self.fetch_anchor_or_alias(TK::Anchor);
1529        }
1530        if ch == '!' {
1531            return self.fetch_tag();
1532        }
1533        if ch == '|' && self.flow_level == 0 {
1534            return self.fetch_block_scalar('|');
1535        }
1536        if ch == '>' && self.flow_level == 0 {
1537            return self.fetch_block_scalar('>');
1538        }
1539        if ch == '\'' {
1540            return self.fetch_flow_scalar('\'');
1541        }
1542        if ch == '"' {
1543            return self.fetch_flow_scalar('"');
1544        }
1545        if self.check_plain() {
1546            return self.fetch_plain();
1547        }
1548        Err(YErr::Marked(format!(
1549            "found character {} that cannot start any token",
1550            repr_char(ch)
1551        )))
1552    }
1553
1554    fn check_directive(&self) -> bool {
1555        self.column == 0
1556    }
1557
1558    fn check_document_indicator(&self, marker: &str) -> bool {
1559        self.column == 0 && self.prefix(3) == marker && is_z_ws_break(self.peek(3))
1560    }
1561
1562    fn check_plain(&self) -> bool {
1563        let ch = self.peek(0);
1564        let starters = "-?:,[]{}#&*!|>'\"%@`";
1565        (!is_z_ws_break(ch) && !starters.contains(ch))
1566            || (!is_z_ws_break(self.peek(1))
1567                && (ch == '-' || (self.flow_level == 0 && (ch == '?' || ch == ':'))))
1568    }
1569
1570    fn fetch_stream_end(&mut self) -> Result<(), YErr> {
1571        self.unwind_indent(-1);
1572        self.remove_possible_simple_key()?;
1573        self.allow_simple_key = false;
1574        self.possible_simple_keys.clear();
1575        self.tokens.push_back(Tok::simple(TK::StreamEnd));
1576        self.done = true;
1577        Ok(())
1578    }
1579
1580    fn fetch_directive(&mut self) -> Result<(), YErr> {
1581        self.unwind_indent(-1);
1582        self.remove_possible_simple_key()?;
1583        self.allow_simple_key = false;
1584        let tok = self.scan_directive()?;
1585        self.tokens.push_back(tok);
1586        Ok(())
1587    }
1588
1589    fn fetch_document_indicator(&mut self, kind: TK) -> Result<(), YErr> {
1590        self.unwind_indent(-1);
1591        self.remove_possible_simple_key()?;
1592        self.allow_simple_key = false;
1593        self.forward(3);
1594        self.tokens.push_back(Tok::simple(kind));
1595        Ok(())
1596    }
1597
1598    fn fetch_flow_collection_start(&mut self, kind: TK) -> Result<(), YErr> {
1599        self.save_possible_simple_key()?;
1600        self.flow_level += 1;
1601        self.allow_simple_key = true;
1602        self.forward(1);
1603        self.tokens.push_back(Tok::simple(kind));
1604        Ok(())
1605    }
1606
1607    fn fetch_flow_collection_end(&mut self, kind: TK) -> Result<(), YErr> {
1608        self.remove_possible_simple_key()?;
1609        self.flow_level = self.flow_level.saturating_sub(1);
1610        self.allow_simple_key = false;
1611        self.forward(1);
1612        self.tokens.push_back(Tok::simple(kind));
1613        Ok(())
1614    }
1615
1616    fn fetch_flow_entry(&mut self) -> Result<(), YErr> {
1617        self.allow_simple_key = true;
1618        self.remove_possible_simple_key()?;
1619        self.forward(1);
1620        self.tokens.push_back(Tok::simple(TK::FlowEntry));
1621        Ok(())
1622    }
1623
1624    fn fetch_block_entry(&mut self) -> Result<(), YErr> {
1625        if self.flow_level == 0 {
1626            if !self.allow_simple_key {
1627                return Err(YErr::Marked(
1628                    "sequence entries are not allowed here".to_string(),
1629                ));
1630            }
1631            if self.add_indent(self.column) {
1632                self.tokens.push_back(Tok::simple(TK::BlockSequenceStart));
1633            }
1634        }
1635        self.allow_simple_key = true;
1636        self.remove_possible_simple_key()?;
1637        self.forward(1);
1638        self.tokens.push_back(Tok::simple(TK::BlockEntry));
1639        Ok(())
1640    }
1641
1642    fn fetch_key(&mut self) -> Result<(), YErr> {
1643        if self.flow_level == 0 {
1644            if !self.allow_simple_key {
1645                return Err(YErr::Marked(
1646                    "mapping keys are not allowed here".to_string(),
1647                ));
1648            }
1649            if self.add_indent(self.column) {
1650                self.tokens.push_back(Tok::simple(TK::BlockMappingStart));
1651            }
1652        }
1653        self.allow_simple_key = self.flow_level == 0;
1654        self.remove_possible_simple_key()?;
1655        self.forward(1);
1656        self.tokens.push_back(Tok::simple(TK::Key));
1657        Ok(())
1658    }
1659
1660    fn fetch_value(&mut self) -> Result<(), YErr> {
1661        if let Some(key) = self.possible_simple_keys.remove(&self.flow_level) {
1662            let insert_at = key.token_number - self.tokens_taken;
1663            self.tokens.insert(insert_at, Tok::simple(TK::Key));
1664            if self.flow_level == 0 && self.add_indent(key.column) {
1665                self.tokens
1666                    .insert(insert_at, Tok::simple(TK::BlockMappingStart));
1667            }
1668            self.allow_simple_key = false;
1669        } else {
1670            if self.flow_level == 0 {
1671                if !self.allow_simple_key {
1672                    return Err(YErr::Marked(
1673                        "mapping values are not allowed here".to_string(),
1674                    ));
1675                }
1676                if self.add_indent(self.column) {
1677                    self.tokens.push_back(Tok::simple(TK::BlockMappingStart));
1678                }
1679            }
1680            self.allow_simple_key = self.flow_level == 0;
1681            self.remove_possible_simple_key()?;
1682        }
1683        self.forward(1);
1684        self.tokens.push_back(Tok::simple(TK::Value));
1685        Ok(())
1686    }
1687
1688    fn fetch_anchor_or_alias(&mut self, kind: TK) -> Result<(), YErr> {
1689        self.save_possible_simple_key()?;
1690        self.allow_simple_key = false;
1691        let tok = self.scan_anchor(kind)?;
1692        self.tokens.push_back(tok);
1693        Ok(())
1694    }
1695
1696    fn fetch_tag(&mut self) -> Result<(), YErr> {
1697        self.save_possible_simple_key()?;
1698        self.allow_simple_key = false;
1699        let tok = self.scan_tag()?;
1700        self.tokens.push_back(tok);
1701        Ok(())
1702    }
1703
1704    fn fetch_block_scalar(&mut self, style: char) -> Result<(), YErr> {
1705        self.allow_simple_key = true;
1706        self.remove_possible_simple_key()?;
1707        let tok = self.scan_block_scalar(style)?;
1708        self.tokens.push_back(tok);
1709        Ok(())
1710    }
1711
1712    fn fetch_flow_scalar(&mut self, style: char) -> Result<(), YErr> {
1713        self.save_possible_simple_key()?;
1714        self.allow_simple_key = false;
1715        let tok = self.scan_flow_scalar(style)?;
1716        self.tokens.push_back(tok);
1717        Ok(())
1718    }
1719
1720    fn fetch_plain(&mut self) -> Result<(), YErr> {
1721        self.save_possible_simple_key()?;
1722        self.allow_simple_key = false;
1723        let tok = self.scan_plain()?;
1724        self.tokens.push_back(tok);
1725        Ok(())
1726    }
1727
1728    // --- scanners -----------------------------------------------------------
1729
1730    fn scan_to_next_token(&mut self) {
1731        if self.index == 0 && self.peek(0) == '\u{feff}' {
1732            self.forward(1);
1733        }
1734        let mut found = false;
1735        while !found {
1736            self.skip_spaces();
1737            if self.peek(0) == '#' {
1738                while !is_z_break(self.peek(0)) {
1739                    self.forward(1);
1740                }
1741            }
1742            if !self.scan_line_break().is_empty() {
1743                if self.flow_level == 0 {
1744                    self.allow_simple_key = true;
1745                }
1746            } else {
1747                found = true;
1748            }
1749        }
1750    }
1751
1752    fn scan_line_break(&mut self) -> String {
1753        let ch = self.peek(0);
1754        if matches!(ch, '\r' | '\n' | '\u{85}') {
1755            if self.prefix(2) == "\r\n" {
1756                self.forward(2);
1757            } else {
1758                self.forward(1);
1759            }
1760            return "\n".to_string();
1761        } else if matches!(ch, '\u{2028}' | '\u{2029}') {
1762            self.forward(1);
1763            return ch.to_string();
1764        }
1765        String::new()
1766    }
1767
1768    fn scan_directive(&mut self) -> Result<Tok, YErr> {
1769        self.forward(1);
1770        let name = self.scan_directive_name()?;
1771        let value = if name == "YAML" {
1772            let v = self.scan_yaml_directive_value()?;
1773            Some(DirectiveVal::Yaml { major_is_1: v })
1774        } else if name == "TAG" {
1775            let (handle, prefix) = self.scan_tag_directive_value()?;
1776            Some(DirectiveVal::Tag { handle, prefix })
1777        } else {
1778            while !is_z_break(self.peek(0)) {
1779                self.forward(1);
1780            }
1781            Some(DirectiveVal::Other)
1782        };
1783        self.scan_ignored_line()?;
1784        Ok(Tok {
1785            kind: TK::Directive,
1786            value: name,
1787            plain: false,
1788            tag: None,
1789            directive: value,
1790        })
1791    }
1792
1793    fn scan_directive_name(&mut self) -> Result<String, YErr> {
1794        let mut length = 0;
1795        let mut ch = self.peek(length);
1796        while is_word_char(ch) {
1797            length += 1;
1798            ch = self.peek(length);
1799        }
1800        if length == 0 {
1801            return Err(YErr::Marked(format!(
1802                "expected alphabetic or numeric character, but found {}",
1803                repr_char(ch)
1804            )));
1805        }
1806        let value = self.prefix(length);
1807        self.forward(length);
1808        let ch = self.peek(0);
1809        if !(ch == '\0' || ch == ' ' || is_break(ch)) {
1810            return Err(YErr::Marked(format!(
1811                "expected alphabetic or numeric character, but found {}",
1812                repr_char(ch)
1813            )));
1814        }
1815        Ok(value)
1816    }
1817
1818    fn scan_yaml_directive_value(&mut self) -> Result<bool, YErr> {
1819        self.skip_spaces();
1820        let major = self.scan_yaml_directive_number()?;
1821        if self.peek(0) != '.' {
1822            return Err(YErr::Marked(format!(
1823                "expected a digit or '.', but found {}",
1824                repr_char(self.peek(0))
1825            )));
1826        }
1827        self.forward(1);
1828        let _minor = self.scan_yaml_directive_number()?;
1829        let ch = self.peek(0);
1830        if !(ch == '\0' || ch == ' ' || is_break(ch)) {
1831            return Err(YErr::Marked(format!(
1832                "expected a digit or ' ', but found {}",
1833                repr_char(ch)
1834            )));
1835        }
1836        Ok(major == Some(1))
1837    }
1838
1839    fn scan_yaml_directive_number(&mut self) -> Result<Option<u64>, YErr> {
1840        let ch = self.peek(0);
1841        if !ch.is_ascii_digit() {
1842            return Err(YErr::Marked(format!(
1843                "expected a digit, but found {}",
1844                repr_char(ch)
1845            )));
1846        }
1847        let mut length = 0;
1848        while self.peek(length).is_ascii_digit() {
1849            length += 1;
1850        }
1851        let text = self.prefix(length);
1852        self.forward(length);
1853        Ok(text.parse::<u64>().ok()) // None = larger than u64 (still != 1)
1854    }
1855
1856    fn scan_tag_directive_value(&mut self) -> Result<(String, String), YErr> {
1857        self.skip_spaces();
1858        let handle = self.scan_tag_handle("directive")?;
1859        let ch = self.peek(0);
1860        if ch != ' ' {
1861            return Err(YErr::Marked(format!(
1862                "expected ' ', but found {}",
1863                repr_char(ch)
1864            )));
1865        }
1866        self.skip_spaces();
1867        let prefix = self.scan_tag_uri("directive")?;
1868        let ch = self.peek(0);
1869        if !(ch == '\0' || ch == ' ' || is_break(ch)) {
1870            return Err(YErr::Marked(format!(
1871                "expected ' ', but found {}",
1872                repr_char(ch)
1873            )));
1874        }
1875        Ok((handle, prefix))
1876    }
1877
1878    fn scan_ignored_line(&mut self) -> Result<(), YErr> {
1879        self.skip_spaces();
1880        if self.peek(0) == '#' {
1881            while !is_z_break(self.peek(0)) {
1882                self.forward(1);
1883            }
1884        }
1885        let ch = self.peek(0);
1886        if !is_z_break(ch) {
1887            return Err(YErr::Marked(format!(
1888                "expected a comment or a line break, but found {}",
1889                repr_char(ch)
1890            )));
1891        }
1892        self.scan_line_break();
1893        Ok(())
1894    }
1895
1896    fn scan_anchor(&mut self, kind: TK) -> Result<Tok, YErr> {
1897        self.forward(1);
1898        let mut length = 0;
1899        let mut ch = self.peek(length);
1900        while is_word_char(ch) {
1901            length += 1;
1902            ch = self.peek(length);
1903        }
1904        if length == 0 {
1905            return Err(YErr::Marked(format!(
1906                "expected alphabetic or numeric character, but found {}",
1907                repr_char(ch)
1908            )));
1909        }
1910        let value = self.prefix(length);
1911        self.forward(length);
1912        let ch = self.peek(0);
1913        if !(is_z_ws_break(ch) || "?:,]}%@`".contains(ch)) {
1914            return Err(YErr::Marked(format!(
1915                "expected alphabetic or numeric character, but found {}",
1916                repr_char(ch)
1917            )));
1918        }
1919        Ok(Tok {
1920            kind,
1921            value,
1922            plain: false,
1923            tag: None,
1924            directive: None,
1925        })
1926    }
1927
1928    fn scan_tag(&mut self) -> Result<Tok, YErr> {
1929        let ch = self.peek(1);
1930        let (handle, suffix): (Option<String>, String);
1931        if ch == '<' {
1932            self.forward(2);
1933            let s = self.scan_tag_uri("tag")?;
1934            if self.peek(0) != '>' {
1935                return Err(YErr::Marked(format!(
1936                    "expected '>', but found {}",
1937                    repr_char(self.peek(0))
1938                )));
1939            }
1940            self.forward(1);
1941            handle = None;
1942            suffix = s;
1943        } else if is_z_ws_break(ch) {
1944            handle = None;
1945            suffix = "!".to_string();
1946            self.forward(1);
1947        } else {
1948            let mut length = 1;
1949            let mut c = ch;
1950            let mut use_handle = false;
1951            while !(c == '\0' || c == ' ' || is_break(c)) {
1952                if c == '!' {
1953                    use_handle = true;
1954                    break;
1955                }
1956                length += 1;
1957                c = self.peek(length);
1958            }
1959            if use_handle {
1960                handle = Some(self.scan_tag_handle("tag")?);
1961            } else {
1962                handle = Some("!".to_string());
1963                self.forward(1);
1964            }
1965            suffix = self.scan_tag_uri("tag")?;
1966        }
1967        let ch = self.peek(0);
1968        if !(ch == '\0' || ch == ' ' || is_break(ch)) {
1969            return Err(YErr::Marked(format!(
1970                "expected ' ', but found {}",
1971                repr_char(ch)
1972            )));
1973        }
1974        Ok(Tok {
1975            kind: TK::Tag,
1976            value: String::new(),
1977            plain: false,
1978            tag: Some((handle, suffix)),
1979            directive: None,
1980        })
1981    }
1982
1983    fn scan_tag_handle(&mut self, _name: &str) -> Result<String, YErr> {
1984        let ch = self.peek(0);
1985        if ch != '!' {
1986            return Err(YErr::Marked(format!(
1987                "expected '!', but found {}",
1988                repr_char(ch)
1989            )));
1990        }
1991        let mut length = 1;
1992        let mut c = self.peek(length);
1993        if c != ' ' {
1994            while is_word_char(c) {
1995                length += 1;
1996                c = self.peek(length);
1997            }
1998            if c != '!' {
1999                self.forward(length);
2000                return Err(YErr::Marked(format!(
2001                    "expected '!', but found {}",
2002                    repr_char(c)
2003                )));
2004            }
2005            length += 1;
2006        }
2007        let value = self.prefix(length);
2008        self.forward(length);
2009        Ok(value)
2010    }
2011
2012    fn scan_tag_uri(&mut self, name: &str) -> Result<String, YErr> {
2013        let mut chunks = String::new();
2014        let mut length = 0;
2015        let mut ch = self.peek(length);
2016        while ch.is_ascii_alphanumeric() || "-;/?:@&=+$,_.!~*'()[]%".contains(ch) {
2017            if ch == '%' {
2018                chunks.push_str(&self.prefix(length));
2019                self.forward(length);
2020                length = 0;
2021                chunks.push_str(&self.scan_uri_escapes(name)?);
2022            } else {
2023                length += 1;
2024            }
2025            ch = self.peek(length);
2026        }
2027        if length > 0 {
2028            chunks.push_str(&self.prefix(length));
2029            self.forward(length);
2030        }
2031        if chunks.is_empty() {
2032            return Err(YErr::Marked(format!(
2033                "expected URI, but found {}",
2034                repr_char(ch)
2035            )));
2036        }
2037        Ok(chunks)
2038    }
2039
2040    fn scan_uri_escapes(&mut self, _name: &str) -> Result<String, YErr> {
2041        let mut codes: Vec<u8> = Vec::new();
2042        while self.peek(0) == '%' {
2043            self.forward(1);
2044            for k in 0..2 {
2045                if !self.peek(k).is_ascii_hexdigit() {
2046                    return Err(YErr::Marked(format!(
2047                        "expected URI escape sequence of 2 hexadecimal numbers, but found {}",
2048                        repr_char(self.peek(k))
2049                    )));
2050                }
2051            }
2052            let hex = self.prefix(2);
2053            codes.push(u8::from_str_radix(&hex, 16).unwrap_or(0));
2054            self.forward(2);
2055        }
2056        match String::from_utf8(codes.clone()) {
2057            Ok(s) => Ok(s),
2058            Err(e) => {
2059                // Python embeds str(UnicodeDecodeError). Reproduce the common
2060                // single-byte form.
2061                let pos = e.utf8_error().valid_up_to();
2062                let byte = codes.get(pos).copied().unwrap_or(0);
2063                let reason = if pos + 1 >= codes.len() && e.utf8_error().error_len().is_none() {
2064                    "unexpected end of data"
2065                } else if (0x80..0xc0).contains(&byte) {
2066                    "invalid start byte"
2067                } else {
2068                    "invalid continuation byte"
2069                };
2070                Err(YErr::Marked(format!(
2071                    "'utf-8' codec can't decode byte 0x{byte:02x} in position {pos}: {reason}"
2072                )))
2073            }
2074        }
2075    }
2076
2077    fn scan_block_scalar(&mut self, style: char) -> Result<Tok, YErr> {
2078        let folded = style == '>';
2079        let mut chunks = String::new();
2080        self.forward(1);
2081        let (chomping, increment) = self.scan_block_scalar_indicators()?;
2082        self.scan_ignored_line()?;
2083
2084        let mut min_indent = self.indent + 1;
2085        if min_indent < 1 {
2086            min_indent = 1;
2087        }
2088        let (mut breaks, indent) = if let Some(inc) = increment {
2089            let indent = min_indent + inc - 1;
2090            (self.scan_block_scalar_breaks(indent), indent)
2091        } else {
2092            let (breaks, max_indent) = self.scan_block_scalar_indentation();
2093            (breaks, min_indent.max(max_indent))
2094        };
2095        let mut line_break = String::new();
2096
2097        while self.column == indent && self.peek(0) != '\0' {
2098            chunks.push_str(&breaks);
2099            let leading_non_space = !matches!(self.peek(0), ' ' | '\t');
2100            let mut length = 0;
2101            while !is_z_break(self.peek(length)) {
2102                length += 1;
2103            }
2104            chunks.push_str(&self.prefix(length));
2105            self.forward(length);
2106            line_break = self.scan_line_break();
2107            breaks = self.scan_block_scalar_breaks(indent);
2108            if self.column == indent && self.peek(0) != '\0' {
2109                if folded
2110                    && line_break == "\n"
2111                    && leading_non_space
2112                    && !matches!(self.peek(0), ' ' | '\t')
2113                {
2114                    if breaks.is_empty() {
2115                        chunks.push(' ');
2116                    }
2117                } else {
2118                    chunks.push_str(&line_break);
2119                }
2120            } else {
2121                break;
2122            }
2123        }
2124
2125        if chomping != Some(false) {
2126            chunks.push_str(&line_break);
2127        }
2128        if chomping == Some(true) {
2129            chunks.push_str(&breaks);
2130        }
2131        Ok(Tok {
2132            kind: TK::Scalar,
2133            value: chunks,
2134            plain: false,
2135            tag: None,
2136            directive: None,
2137        })
2138    }
2139
2140    fn scan_block_scalar_indicators(&mut self) -> Result<(Option<bool>, Option<i64>), YErr> {
2141        let mut chomping: Option<bool> = None;
2142        let mut increment: Option<i64> = None;
2143        let mut ch = self.peek(0);
2144        if ch == '+' || ch == '-' {
2145            chomping = Some(ch == '+');
2146            self.forward(1);
2147            ch = self.peek(0);
2148            if ch.is_ascii_digit() {
2149                let inc = ch.to_digit(10).unwrap() as i64;
2150                if inc == 0 {
2151                    return Err(YErr::Marked(
2152                        "expected indentation indicator in the range 1-9, but found 0"
2153                            .to_string(),
2154                    ));
2155                }
2156                increment = Some(inc);
2157                self.forward(1);
2158            }
2159        } else if ch.is_ascii_digit() {
2160            let inc = ch.to_digit(10).unwrap() as i64;
2161            if inc == 0 {
2162                return Err(YErr::Marked(
2163                    "expected indentation indicator in the range 1-9, but found 0".to_string(),
2164                ));
2165            }
2166            increment = Some(inc);
2167            self.forward(1);
2168            ch = self.peek(0);
2169            if ch == '+' || ch == '-' {
2170                chomping = Some(ch == '+');
2171                self.forward(1);
2172            }
2173        }
2174        let ch = self.peek(0);
2175        if !(ch == '\0' || ch == ' ' || is_break(ch)) {
2176            return Err(YErr::Marked(format!(
2177                "expected chomping or indentation indicators, but found {}",
2178                repr_char(ch)
2179            )));
2180        }
2181        Ok((chomping, increment))
2182    }
2183
2184    fn scan_block_scalar_indentation(&mut self) -> (String, i64) {
2185        let mut chunks = String::new();
2186        let mut max_indent = 0i64;
2187        loop {
2188            let ch = self.peek(0);
2189            if !(ch == ' ' || is_break(ch)) {
2190                break;
2191            }
2192            if ch != ' ' {
2193                chunks.push_str(&self.scan_line_break());
2194            } else {
2195                self.forward(1);
2196                if self.column > max_indent {
2197                    max_indent = self.column;
2198                }
2199            }
2200        }
2201        (chunks, max_indent)
2202    }
2203
2204    fn scan_block_scalar_breaks(&mut self, indent: i64) -> String {
2205        let mut chunks = String::new();
2206        while self.column < indent && self.peek(0) == ' ' {
2207            self.forward(1);
2208        }
2209        while is_break(self.peek(0)) {
2210            chunks.push_str(&self.scan_line_break());
2211            while self.column < indent && self.peek(0) == ' ' {
2212                self.forward(1);
2213            }
2214        }
2215        chunks
2216    }
2217
2218    fn scan_flow_scalar(&mut self, style: char) -> Result<Tok, YErr> {
2219        let double = style == '"';
2220        let mut chunks = String::new();
2221        let quote = self.peek(0);
2222        self.forward(1);
2223        chunks.push_str(&self.scan_flow_scalar_non_spaces(double)?);
2224        while self.peek(0) != quote {
2225            chunks.push_str(&self.scan_flow_scalar_spaces()?);
2226            chunks.push_str(&self.scan_flow_scalar_non_spaces(double)?);
2227        }
2228        self.forward(1);
2229        Ok(Tok {
2230            kind: TK::Scalar,
2231            value: chunks,
2232            plain: false,
2233            tag: None,
2234            directive: None,
2235        })
2236    }
2237
2238    fn scan_flow_scalar_non_spaces(&mut self, double: bool) -> Result<String, YErr> {
2239        let mut chunks = String::new();
2240        loop {
2241            let mut length = 0;
2242            while !matches!(self.peek(length), '\'' | '"' | '\\')
2243                && !is_z_ws_break(self.peek(length))
2244            {
2245                length += 1;
2246            }
2247            if length > 0 {
2248                chunks.push_str(&self.prefix(length));
2249                self.forward(length);
2250            }
2251            let ch = self.peek(0);
2252            if !double && ch == '\'' && self.peek(1) == '\'' {
2253                chunks.push('\'');
2254                self.forward(2);
2255            } else if (double && ch == '\'') || (!double && (ch == '"' || ch == '\\')) {
2256                chunks.push(ch);
2257                self.forward(1);
2258            } else if double && ch == '\\' {
2259                self.forward(1);
2260                let ch = self.peek(0);
2261                let simple: Option<&str> = match ch {
2262                    '0' => Some("\0"),
2263                    'a' => Some("\x07"),
2264                    'b' => Some("\x08"),
2265                    't' | '\t' => Some("\t"),
2266                    'n' => Some("\n"),
2267                    'v' => Some("\x0b"),
2268                    'f' => Some("\x0c"),
2269                    'r' => Some("\r"),
2270                    'e' => Some("\x1b"),
2271                    ' ' => Some(" "),
2272                    '"' => Some("\""),
2273                    '\\' => Some("\\"),
2274                    '/' => Some("/"),
2275                    'N' => Some("\u{85}"),
2276                    '_' => Some("\u{a0}"),
2277                    'L' => Some("\u{2028}"),
2278                    'P' => Some("\u{2029}"),
2279                    _ => None,
2280                };
2281                if let Some(s) = simple {
2282                    chunks.push_str(s);
2283                    self.forward(1);
2284                } else if matches!(ch, 'x' | 'u' | 'U') {
2285                    let length = match ch {
2286                        'x' => 2,
2287                        'u' => 4,
2288                        _ => 8,
2289                    };
2290                    self.forward(1);
2291                    for k in 0..length {
2292                        if !self.peek(k).is_ascii_hexdigit() {
2293                            return Err(YErr::Marked(format!(
2294                                "expected escape sequence of {} hexadecimal numbers, but found {}",
2295                                length,
2296                                repr_char(self.peek(k))
2297                            )));
2298                        }
2299                    }
2300                    let code = u32::from_str_radix(&self.prefix(length), 16).unwrap_or(0);
2301                    match char::from_u32(code) {
2302                        Some(c) => chunks.push(c),
2303                        None => {
2304                            if code > 0x10ffff {
2305                                // Python chr() raises ValueError -> crash.
2306                                return Err(YErr::Internal(
2307                                    "ValueError: chr() arg not in range(0x110000)".to_string(),
2308                                ));
2309                            }
2310                            // Lone surrogate: representable in a Python str,
2311                            // not in Rust. ORACLE DIVERGENCE.
2312                            return Err(YErr::Internal(format!(
2313                                "surrogate escape \\u{code:04x} not representable"
2314                            )));
2315                        }
2316                    }
2317                    self.forward(length);
2318                } else if is_break(ch) {
2319                    self.scan_line_break();
2320                    chunks.push_str(&self.scan_flow_scalar_breaks()?);
2321                } else {
2322                    return Err(YErr::Marked(format!(
2323                        "found unknown escape character {}",
2324                        repr_char(ch)
2325                    )));
2326                }
2327            } else {
2328                return Ok(chunks);
2329            }
2330        }
2331    }
2332
2333    fn scan_flow_scalar_spaces(&mut self) -> Result<String, YErr> {
2334        let mut chunks = String::new();
2335        let mut length = 0;
2336        while matches!(self.peek(length), ' ' | '\t') {
2337            length += 1;
2338        }
2339        let whitespaces = self.prefix(length);
2340        self.forward(length);
2341        let ch = self.peek(0);
2342        if ch == '\0' {
2343            return Err(YErr::Marked("found unexpected end of stream".to_string()));
2344        } else if is_break(ch) {
2345            let line_break = self.scan_line_break();
2346            let breaks = self.scan_flow_scalar_breaks()?;
2347            if line_break != "\n" {
2348                chunks.push_str(&line_break);
2349            } else if breaks.is_empty() {
2350                chunks.push(' ');
2351            }
2352            chunks.push_str(&breaks);
2353        } else {
2354            chunks.push_str(&whitespaces);
2355        }
2356        Ok(chunks)
2357    }
2358
2359    fn scan_flow_scalar_breaks(&mut self) -> Result<String, YErr> {
2360        let mut chunks = String::new();
2361        loop {
2362            let prefix = self.prefix(3);
2363            if (prefix == "---" || prefix == "...") && is_z_ws_break(self.peek(3)) {
2364                return Err(YErr::Marked(
2365                    "found unexpected document separator".to_string(),
2366                ));
2367            }
2368            while matches!(self.peek(0), ' ' | '\t') {
2369                self.forward(1);
2370            }
2371            if is_break(self.peek(0)) {
2372                chunks.push_str(&self.scan_line_break());
2373            } else {
2374                return Ok(chunks);
2375            }
2376        }
2377    }
2378
2379    fn scan_plain(&mut self) -> Result<Tok, YErr> {
2380        let mut chunks = String::new();
2381        let indent = self.indent + 1;
2382        let mut spaces = String::new();
2383        loop {
2384            let mut length = 0;
2385            if self.peek(0) == '#' {
2386                break;
2387            }
2388            loop {
2389                let ch = self.peek(length);
2390                let stop = is_z_ws_break(ch)
2391                    || (ch == ':'
2392                        && (is_z_ws_break(self.peek(length + 1))
2393                            || (self.flow_level != 0
2394                                && ",[]{}".contains(self.peek(length + 1)))))
2395                    || (self.flow_level != 0 && ",?[]{}".contains(ch));
2396                if stop {
2397                    break;
2398                }
2399                length += 1;
2400            }
2401            if length == 0 {
2402                break;
2403            }
2404            self.allow_simple_key = false;
2405            chunks.push_str(&spaces);
2406            chunks.push_str(&self.prefix(length));
2407            self.forward(length);
2408            spaces = match self.scan_plain_spaces()? {
2409                Some(s) => s,
2410                None => break,
2411            };
2412            if spaces.is_empty()
2413                || self.peek(0) == '#'
2414                || (self.flow_level == 0 && self.column < indent)
2415            {
2416                break;
2417            }
2418        }
2419        Ok(Tok {
2420            kind: TK::Scalar,
2421            value: chunks,
2422            plain: true,
2423            tag: None,
2424            directive: None,
2425        })
2426    }
2427
2428    /// Returns None where Python's `scan_plain_spaces` returns None (document
2429    /// separator ahead), Some(chunks) otherwise.
2430    fn scan_plain_spaces(&mut self) -> Result<Option<String>, YErr> {
2431        let mut chunks = String::new();
2432        let mut length = 0;
2433        while self.peek(length) == ' ' {
2434            length += 1;
2435        }
2436        let whitespaces = self.prefix(length);
2437        self.forward(length);
2438        let ch = self.peek(0);
2439        if is_break(ch) {
2440            let line_break = self.scan_line_break();
2441            self.allow_simple_key = true;
2442            let prefix = self.prefix(3);
2443            if (prefix == "---" || prefix == "...") && is_z_ws_break(self.peek(3)) {
2444                return Ok(None);
2445            }
2446            let mut breaks = String::new();
2447            loop {
2448                let c = self.peek(0);
2449                if !(c == ' ' || is_break(c)) {
2450                    break;
2451                }
2452                if c == ' ' {
2453                    self.forward(1);
2454                } else {
2455                    breaks.push_str(&self.scan_line_break());
2456                    let prefix = self.prefix(3);
2457                    if (prefix == "---" || prefix == "...") && is_z_ws_break(self.peek(3)) {
2458                        return Ok(None);
2459                    }
2460                }
2461            }
2462            if line_break != "\n" {
2463                chunks.push_str(&line_break);
2464            } else if breaks.is_empty() {
2465                chunks.push(' ');
2466            }
2467            chunks.push_str(&breaks);
2468        } else if !whitespaces.is_empty() {
2469            chunks.push_str(&whitespaces);
2470        }
2471        Ok(Some(chunks))
2472    }
2473}
2474
2475// ---------------------------------------------------------------------------
2476// Parser (yaml/parser.py) — LL(1) state machine; error strings verbatim
2477// ---------------------------------------------------------------------------
2478
2479#[derive(Clone, Debug)]
2480enum Ev {
2481    StreamStart,
2482    StreamEnd,
2483    DocStart,
2484    DocEnd,
2485    Alias,
2486    Scalar {
2487        tag: Option<String>,
2488        implicit: (bool, bool),
2489        value: String,
2490        anchor: Option<String>,
2491    },
2492    SeqStart {
2493        tag: Option<String>,
2494        anchor: Option<String>,
2495    },
2496    SeqEnd,
2497    MapStart {
2498        tag: Option<String>,
2499        anchor: Option<String>,
2500    },
2501    MapEnd,
2502}
2503
2504#[derive(Clone, Copy, Debug)]
2505enum St {
2506    StreamStart,
2507    ImplicitDocumentStart,
2508    DocumentStart,
2509    DocumentContent,
2510    DocumentEnd,
2511    BlockNode,
2512    BlockSequenceFirstEntry,
2513    BlockSequenceEntry,
2514    IndentlessSequenceEntry,
2515    BlockMappingFirstKey,
2516    BlockMappingKey,
2517    BlockMappingValue,
2518    FlowSequenceFirstEntry,
2519    FlowSequenceEntry { first: bool },
2520    FlowSequenceEntryMappingKey,
2521    FlowSequenceEntryMappingValue,
2522    FlowSequenceEntryMappingEnd,
2523    FlowMappingFirstKey,
2524    FlowMappingKey { first: bool },
2525    FlowMappingValue,
2526    FlowMappingEmptyValue,
2527}
2528
2529struct Parser {
2530    sc: Scanner,
2531    current_event: Option<Ev>,
2532    yaml_version_seen: bool,
2533    tag_handles: HashMap<String, String>,
2534    states: Vec<St>,
2535    state: Option<St>,
2536}
2537
2538fn default_tag_handles() -> HashMap<String, String> {
2539    let mut m = HashMap::new();
2540    m.insert("!".to_string(), "!".to_string());
2541    m.insert("!!".to_string(), "tag:yaml.org,2002:".to_string());
2542    m
2543}
2544
2545impl Parser {
2546    fn new(sc: Scanner) -> Parser {
2547        Parser {
2548            sc,
2549            current_event: None,
2550            yaml_version_seen: false,
2551            tag_handles: HashMap::new(),
2552            states: Vec::new(),
2553            state: Some(St::StreamStart),
2554        }
2555    }
2556
2557    fn produce(&mut self) -> Result<(), YErr> {
2558        if self.current_event.is_none() {
2559            if let Some(st) = self.state {
2560                let ev = self.step(st)?;
2561                self.current_event = Some(ev);
2562            }
2563        }
2564        Ok(())
2565    }
2566
2567    fn peek_event(&mut self) -> Result<Option<&Ev>, YErr> {
2568        self.produce()?;
2569        Ok(self.current_event.as_ref())
2570    }
2571
2572    fn get_event(&mut self) -> Result<Ev, YErr> {
2573        self.produce()?;
2574        self.current_event
2575            .take()
2576            .ok_or_else(|| YErr::Internal("StopIteration: no more events".to_string()))
2577    }
2578
2579    fn step(&mut self, st: St) -> Result<Ev, YErr> {
2580        match st {
2581            St::StreamStart => {
2582                self.sc.get_token()?;
2583                self.state = Some(St::ImplicitDocumentStart);
2584                Ok(Ev::StreamStart)
2585            }
2586            St::ImplicitDocumentStart => {
2587                if !self
2588                    .sc
2589                    .check_token(&[TK::Directive, TK::DocumentStart, TK::StreamEnd])?
2590                {
2591                    self.tag_handles = default_tag_handles();
2592                    self.states.push(St::DocumentEnd);
2593                    self.state = Some(St::BlockNode);
2594                    Ok(Ev::DocStart)
2595                } else {
2596                    self.step(St::DocumentStart)
2597                }
2598            }
2599            St::DocumentStart => {
2600                while self.sc.check_token(&[TK::DocumentEnd])? {
2601                    self.sc.get_token()?;
2602                }
2603                if !self.sc.check_token(&[TK::StreamEnd])? {
2604                    self.process_directives()?;
2605                    if !self.sc.check_token(&[TK::DocumentStart])? {
2606                        let tok = self.sc.peek_token()?;
2607                        return Err(YErr::Marked(format!(
2608                            "expected '<document start>', but found {}",
2609                            py_repr_str(tok.id())
2610                        )));
2611                    }
2612                    self.sc.get_token()?;
2613                    self.states.push(St::DocumentEnd);
2614                    self.state = Some(St::DocumentContent);
2615                    Ok(Ev::DocStart)
2616                } else {
2617                    self.sc.get_token()?;
2618                    self.state = None;
2619                    Ok(Ev::StreamEnd)
2620                }
2621            }
2622            St::DocumentEnd => {
2623                if self.sc.check_token(&[TK::DocumentEnd])? {
2624                    self.sc.get_token()?;
2625                }
2626                self.state = Some(St::DocumentStart);
2627                Ok(Ev::DocEnd)
2628            }
2629            St::DocumentContent => {
2630                if self.sc.check_token(&[
2631                    TK::Directive,
2632                    TK::DocumentStart,
2633                    TK::DocumentEnd,
2634                    TK::StreamEnd,
2635                ])? {
2636                    self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2637                    Ok(empty_scalar())
2638                } else {
2639                    self.parse_node(true, false)
2640                }
2641            }
2642            St::BlockNode => self.parse_node(true, false),
2643            St::BlockSequenceFirstEntry => {
2644                self.sc.get_token()?;
2645                self.block_sequence_entry()
2646            }
2647            St::BlockSequenceEntry => self.block_sequence_entry(),
2648            St::IndentlessSequenceEntry => {
2649                if self.sc.check_token(&[TK::BlockEntry])? {
2650                    self.sc.get_token()?;
2651                    if !self.sc.check_token(&[
2652                        TK::BlockEntry,
2653                        TK::Key,
2654                        TK::Value,
2655                        TK::BlockEnd,
2656                    ])? {
2657                        self.states.push(St::IndentlessSequenceEntry);
2658                        return self.parse_node(true, false);
2659                    }
2660                    self.state = Some(St::IndentlessSequenceEntry);
2661                    return Ok(empty_scalar());
2662                }
2663                self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2664                Ok(Ev::SeqEnd)
2665            }
2666            St::BlockMappingFirstKey => {
2667                self.sc.get_token()?;
2668                self.block_mapping_key()
2669            }
2670            St::BlockMappingKey => self.block_mapping_key(),
2671            St::BlockMappingValue => {
2672                if self.sc.check_token(&[TK::Value])? {
2673                    self.sc.get_token()?;
2674                    if !self.sc.check_token(&[TK::Key, TK::Value, TK::BlockEnd])? {
2675                        self.states.push(St::BlockMappingKey);
2676                        return self.parse_node(true, true);
2677                    }
2678                    self.state = Some(St::BlockMappingKey);
2679                    return Ok(empty_scalar());
2680                }
2681                self.state = Some(St::BlockMappingKey);
2682                Ok(empty_scalar())
2683            }
2684            St::FlowSequenceFirstEntry => {
2685                self.sc.get_token()?;
2686                self.flow_sequence_entry(true)
2687            }
2688            St::FlowSequenceEntry { first } => self.flow_sequence_entry(first),
2689            St::FlowSequenceEntryMappingKey => {
2690                self.sc.get_token()?;
2691                if !self
2692                    .sc
2693                    .check_token(&[TK::Value, TK::FlowEntry, TK::FlowSequenceEnd])?
2694                {
2695                    self.states.push(St::FlowSequenceEntryMappingValue);
2696                    return self.parse_node(false, false);
2697                }
2698                self.state = Some(St::FlowSequenceEntryMappingValue);
2699                Ok(empty_scalar())
2700            }
2701            St::FlowSequenceEntryMappingValue => {
2702                if self.sc.check_token(&[TK::Value])? {
2703                    self.sc.get_token()?;
2704                    if !self.sc.check_token(&[TK::FlowEntry, TK::FlowSequenceEnd])? {
2705                        self.states.push(St::FlowSequenceEntryMappingEnd);
2706                        return self.parse_node(false, false);
2707                    }
2708                    self.state = Some(St::FlowSequenceEntryMappingEnd);
2709                    return Ok(empty_scalar());
2710                }
2711                self.state = Some(St::FlowSequenceEntryMappingEnd);
2712                Ok(empty_scalar())
2713            }
2714            St::FlowSequenceEntryMappingEnd => {
2715                self.state = Some(St::FlowSequenceEntry { first: false });
2716                Ok(Ev::MapEnd)
2717            }
2718            St::FlowMappingFirstKey => {
2719                self.sc.get_token()?;
2720                self.flow_mapping_key(true)
2721            }
2722            St::FlowMappingKey { first } => self.flow_mapping_key(first),
2723            St::FlowMappingValue => {
2724                if self.sc.check_token(&[TK::Value])? {
2725                    self.sc.get_token()?;
2726                    if !self.sc.check_token(&[TK::FlowEntry, TK::FlowMappingEnd])? {
2727                        self.states.push(St::FlowMappingKey { first: false });
2728                        return self.parse_node(false, false);
2729                    }
2730                    self.state = Some(St::FlowMappingKey { first: false });
2731                    return Ok(empty_scalar());
2732                }
2733                self.state = Some(St::FlowMappingKey { first: false });
2734                Ok(empty_scalar())
2735            }
2736            St::FlowMappingEmptyValue => {
2737                self.state = Some(St::FlowMappingKey { first: false });
2738                Ok(empty_scalar())
2739            }
2740        }
2741    }
2742
2743    fn block_sequence_entry(&mut self) -> Result<Ev, YErr> {
2744        if self.sc.check_token(&[TK::BlockEntry])? {
2745            self.sc.get_token()?;
2746            if !self.sc.check_token(&[TK::BlockEntry, TK::BlockEnd])? {
2747                self.states.push(St::BlockSequenceEntry);
2748                return self.parse_node(true, false);
2749            }
2750            self.state = Some(St::BlockSequenceEntry);
2751            return Ok(empty_scalar());
2752        }
2753        if !self.sc.check_token(&[TK::BlockEnd])? {
2754            let tok = self.sc.peek_token()?;
2755            return Err(YErr::Marked(format!(
2756                "expected <block end>, but found {}",
2757                py_repr_str(tok.id())
2758            )));
2759        }
2760        self.sc.get_token()?;
2761        self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2762        Ok(Ev::SeqEnd)
2763    }
2764
2765    fn block_mapping_key(&mut self) -> Result<Ev, YErr> {
2766        if self.sc.check_token(&[TK::Key])? {
2767            self.sc.get_token()?;
2768            if !self.sc.check_token(&[TK::Key, TK::Value, TK::BlockEnd])? {
2769                self.states.push(St::BlockMappingValue);
2770                return self.parse_node(true, true);
2771            }
2772            self.state = Some(St::BlockMappingValue);
2773            return Ok(empty_scalar());
2774        }
2775        if !self.sc.check_token(&[TK::BlockEnd])? {
2776            let tok = self.sc.peek_token()?;
2777            return Err(YErr::Marked(format!(
2778                "expected <block end>, but found {}",
2779                py_repr_str(tok.id())
2780            )));
2781        }
2782        self.sc.get_token()?;
2783        self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2784        Ok(Ev::MapEnd)
2785    }
2786
2787    fn flow_sequence_entry(&mut self, first: bool) -> Result<Ev, YErr> {
2788        if !self.sc.check_token(&[TK::FlowSequenceEnd])? {
2789            if !first {
2790                if self.sc.check_token(&[TK::FlowEntry])? {
2791                    self.sc.get_token()?;
2792                } else {
2793                    let tok = self.sc.peek_token()?;
2794                    return Err(YErr::Marked(format!(
2795                        "expected ',' or ']', but got {}",
2796                        py_repr_str(tok.id())
2797                    )));
2798                }
2799            }
2800            if self.sc.check_token(&[TK::Key])? {
2801                self.state = Some(St::FlowSequenceEntryMappingKey);
2802                return Ok(Ev::MapStart {
2803                    tag: None,
2804                    anchor: None,
2805                });
2806            } else if !self.sc.check_token(&[TK::FlowSequenceEnd])? {
2807                self.states.push(St::FlowSequenceEntry { first: false });
2808                return self.parse_node(false, false);
2809            }
2810        }
2811        self.sc.get_token()?;
2812        self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2813        Ok(Ev::SeqEnd)
2814    }
2815
2816    fn flow_mapping_key(&mut self, first: bool) -> Result<Ev, YErr> {
2817        if !self.sc.check_token(&[TK::FlowMappingEnd])? {
2818            if !first {
2819                if self.sc.check_token(&[TK::FlowEntry])? {
2820                    self.sc.get_token()?;
2821                } else {
2822                    let tok = self.sc.peek_token()?;
2823                    return Err(YErr::Marked(format!(
2824                        "expected ',' or '}}', but got {}",
2825                        py_repr_str(tok.id())
2826                    )));
2827                }
2828            }
2829            if self.sc.check_token(&[TK::Key])? {
2830                self.sc.get_token()?;
2831                if !self
2832                    .sc
2833                    .check_token(&[TK::Value, TK::FlowEntry, TK::FlowMappingEnd])?
2834                {
2835                    self.states.push(St::FlowMappingValue);
2836                    return self.parse_node(false, false);
2837                }
2838                self.state = Some(St::FlowMappingValue);
2839                return Ok(empty_scalar());
2840            } else if !self.sc.check_token(&[TK::FlowMappingEnd])? {
2841                self.states.push(St::FlowMappingEmptyValue);
2842                return self.parse_node(false, false);
2843            }
2844        }
2845        self.sc.get_token()?;
2846        self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2847        Ok(Ev::MapEnd)
2848    }
2849
2850    fn process_directives(&mut self) -> Result<(), YErr> {
2851        self.yaml_version_seen = false;
2852        self.tag_handles = HashMap::new();
2853        while self.sc.check_token(&[TK::Directive])? {
2854            let tok = self.sc.get_token()?;
2855            match tok.directive {
2856                Some(DirectiveVal::Yaml { major_is_1 }) => {
2857                    if self.yaml_version_seen {
2858                        return Err(YErr::Marked("found duplicate YAML directive".to_string()));
2859                    }
2860                    if !major_is_1 {
2861                        return Err(YErr::Marked(
2862                            "found incompatible YAML document (version 1.* is required)"
2863                                .to_string(),
2864                        ));
2865                    }
2866                    self.yaml_version_seen = true;
2867                }
2868                Some(DirectiveVal::Tag { handle, prefix }) => {
2869                    if self.tag_handles.contains_key(&handle) {
2870                        return Err(YErr::Marked(format!(
2871                            "duplicate tag handle {}",
2872                            py_repr_str(&handle)
2873                        )));
2874                    }
2875                    self.tag_handles.insert(handle, prefix);
2876                }
2877                _ => {}
2878            }
2879        }
2880        for (k, v) in default_tag_handles() {
2881            self.tag_handles.entry(k).or_insert(v);
2882        }
2883        Ok(())
2884    }
2885
2886    fn parse_node(&mut self, block: bool, indentless_sequence: bool) -> Result<Ev, YErr> {
2887        if self.sc.check_token(&[TK::Alias])? {
2888            self.sc.get_token()?;
2889            self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2890            return Ok(Ev::Alias);
2891        }
2892        let mut anchor: Option<String> = None;
2893        let mut tag: Option<(Option<String>, String)> = None;
2894        let mut saw_properties = false;
2895        if self.sc.check_token(&[TK::Anchor])? {
2896            let tok = self.sc.get_token()?;
2897            anchor = Some(tok.value);
2898            saw_properties = true;
2899            if self.sc.check_token(&[TK::Tag])? {
2900                let tok = self.sc.get_token()?;
2901                tag = tok.tag;
2902            }
2903        } else if self.sc.check_token(&[TK::Tag])? {
2904            let tok = self.sc.get_token()?;
2905            tag = tok.tag;
2906            saw_properties = true;
2907            if self.sc.check_token(&[TK::Anchor])? {
2908                let tok = self.sc.get_token()?;
2909                anchor = Some(tok.value);
2910            }
2911        }
2912        let resolved_tag: Option<String> = match tag {
2913            Some((Some(handle), suffix)) => match self.tag_handles.get(&handle) {
2914                Some(prefix) => Some(format!("{prefix}{suffix}")),
2915                None => {
2916                    return Err(YErr::Marked(format!(
2917                        "found undefined tag handle {}",
2918                        py_repr_str(&handle)
2919                    )));
2920                }
2921            },
2922            Some((None, suffix)) => Some(suffix),
2923            None => None,
2924        };
2925        let implicit = resolved_tag.is_none() || resolved_tag.as_deref() == Some("!");
2926        if indentless_sequence && self.sc.check_token(&[TK::BlockEntry])? {
2927            self.state = Some(St::IndentlessSequenceEntry);
2928            return Ok(Ev::SeqStart {
2929                tag: resolved_tag,
2930                anchor,
2931            });
2932        }
2933        if self.sc.check_token(&[TK::Scalar])? {
2934            let tok = self.sc.get_token()?;
2935            let implicit_pair = if (tok.plain && resolved_tag.is_none())
2936                || resolved_tag.as_deref() == Some("!")
2937            {
2938                (true, false)
2939            } else if resolved_tag.is_none() {
2940                (false, true)
2941            } else {
2942                (false, false)
2943            };
2944            self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2945            return Ok(Ev::Scalar {
2946                tag: resolved_tag,
2947                implicit: implicit_pair,
2948                value: tok.value,
2949                anchor,
2950            });
2951        }
2952        if self.sc.check_token(&[TK::FlowSequenceStart])? {
2953            self.state = Some(St::FlowSequenceFirstEntry);
2954            return Ok(Ev::SeqStart {
2955                tag: resolved_tag,
2956                anchor,
2957            });
2958        }
2959        if self.sc.check_token(&[TK::FlowMappingStart])? {
2960            self.state = Some(St::FlowMappingFirstKey);
2961            return Ok(Ev::MapStart {
2962                tag: resolved_tag,
2963                anchor,
2964            });
2965        }
2966        if block && self.sc.check_token(&[TK::BlockSequenceStart])? {
2967            self.state = Some(St::BlockSequenceFirstEntry);
2968            return Ok(Ev::SeqStart {
2969                tag: resolved_tag,
2970                anchor,
2971            });
2972        }
2973        if block && self.sc.check_token(&[TK::BlockMappingStart])? {
2974            self.state = Some(St::BlockMappingFirstKey);
2975            return Ok(Ev::MapStart {
2976                tag: resolved_tag,
2977                anchor,
2978            });
2979        }
2980        if saw_properties {
2981            self.state = Some(self.states.pop().ok_or_else(state_underflow)?);
2982            return Ok(Ev::Scalar {
2983                tag: resolved_tag,
2984                implicit: (implicit, false),
2985                value: String::new(),
2986                anchor,
2987            });
2988        }
2989        // Context string ("while parsing a block/flow node") unused — only
2990        // the `problem` field reaches output.
2991        let tok = self.sc.peek_token()?;
2992        Err(YErr::Marked(format!(
2993            "expected the node content, but found {}",
2994            py_repr_str(tok.id())
2995        )))
2996    }
2997}
2998
2999fn empty_scalar() -> Ev {
3000    Ev::Scalar {
3001        tag: None,
3002        implicit: (true, false),
3003        value: String::new(),
3004        anchor: None,
3005    }
3006}
3007
3008fn state_underflow() -> YErr {
3009    YErr::Internal("IndexError: pop from empty parser state stack".to_string())
3010}
3011
3012// ---------------------------------------------------------------------------
3013// Composer (yaml/composer.py) with the _BoundedLoader guards
3014// ---------------------------------------------------------------------------
3015
3016#[derive(Clone, Debug)]
3017enum Node {
3018    Scalar { tag: String, value: String },
3019    Seq { tag: String, items: Vec<Node> },
3020    Map { tag: String, pairs: Vec<(Node, Node)> },
3021}
3022
3023impl Node {
3024    fn tag(&self) -> &str {
3025        match self {
3026            Node::Scalar { tag, .. } | Node::Seq { tag, .. } | Node::Map { tag, .. } => tag,
3027        }
3028    }
3029
3030    fn id(&self) -> &'static str {
3031        match self {
3032            Node::Scalar { .. } => "scalar",
3033            Node::Seq { .. } => "sequence",
3034            Node::Map { .. } => "mapping",
3035        }
3036    }
3037}
3038
3039/// PyYAML node class names, as CPython prints them in unpack TypeErrors.
3040fn py_node_class(n: &Node) -> &'static str {
3041    match n {
3042        Node::Scalar { .. } => "ScalarNode",
3043        Node::Seq { .. } => "SequenceNode",
3044        Node::Map { .. } => "MappingNode",
3045    }
3046}
3047
3048struct Composer {
3049    p: Parser,
3050    anchors: std::collections::HashSet<String>,
3051    depth: usize,
3052}
3053
3054impl Composer {
3055    fn get_single_node(&mut self) -> Result<Option<Node>, YErr> {
3056        // Drop STREAM-START.
3057        self.p.get_event()?;
3058        let mut document = None;
3059        if !matches!(self.p.peek_event()?, Some(Ev::StreamEnd)) {
3060            document = Some(self.compose_document()?);
3061        }
3062        if !matches!(self.p.peek_event()?, Some(Ev::StreamEnd)) {
3063            return Err(YErr::Marked("but found another document".to_string()));
3064        }
3065        self.p.get_event()?;
3066        Ok(document)
3067    }
3068
3069    fn compose_document(&mut self) -> Result<Node, YErr> {
3070        self.p.get_event()?; // DOCUMENT-START
3071        let node = self.compose_node()?;
3072        self.p.get_event()?; // DOCUMENT-END
3073        self.anchors.clear();
3074        Ok(node)
3075    }
3076
3077    /// `_BoundedLoader.compose_node`: alias rejection, then the node-count
3078    /// depth cap (root = 1; every scalar/sequence/mapping counts one level).
3079    fn compose_node(&mut self) -> Result<Node, YErr> {
3080        if matches!(self.p.peek_event()?, Some(Ev::Alias)) {
3081            return Err(YErr::Marked(
3082                "YAML aliases are not permitted in frontmatter".to_string(),
3083            ));
3084        }
3085        self.depth += 1;
3086        if self.depth > MAX_FRONTMATTER_DEPTH {
3087            self.depth -= 1;
3088            return Err(YErr::Marked(format!(
3089                "frontmatter nesting exceeds the {MAX_FRONTMATTER_DEPTH}-level cap"
3090            )));
3091        }
3092        let result = self.compose_node_inner();
3093        self.depth -= 1;
3094        result
3095    }
3096
3097    fn compose_node_inner(&mut self) -> Result<Node, YErr> {
3098        // Duplicate-anchor check (aliases already rejected above).
3099        let anchor: Option<String> = match self.p.peek_event()? {
3100            Some(Ev::Scalar { anchor, .. })
3101            | Some(Ev::SeqStart { anchor, .. })
3102            | Some(Ev::MapStart { anchor, .. }) => anchor.clone(),
3103            _ => None,
3104        };
3105        if let Some(a) = &anchor {
3106            if self.anchors.contains(a) {
3107                return Err(YErr::Marked("second occurrence".to_string()));
3108            }
3109            self.anchors.insert(a.clone());
3110        }
3111        match self.p.peek_event()? {
3112            Some(Ev::Scalar { .. }) => {
3113                if let Ev::Scalar {
3114                    tag,
3115                    implicit,
3116                    value,
3117                    ..
3118                } = self.p.get_event()?
3119                {
3120                    let tag = match tag.as_deref() {
3121                        None | Some("!") => {
3122                            if implicit.0 {
3123                                resolve_plain(&value).to_string()
3124                            } else {
3125                                TAG_STR.to_string()
3126                            }
3127                        }
3128                        Some(t) => t.to_string(),
3129                    };
3130                    Ok(Node::Scalar { tag, value })
3131                } else {
3132                    unreachable!()
3133                }
3134            }
3135            Some(Ev::SeqStart { .. }) => {
3136                let tag = if let Ev::SeqStart { tag, .. } = self.p.get_event()? {
3137                    match tag.as_deref() {
3138                        None | Some("!") => TAG_SEQ.to_string(),
3139                        Some(t) => t.to_string(),
3140                    }
3141                } else {
3142                    unreachable!()
3143                };
3144                let mut items = Vec::new();
3145                while !matches!(self.p.peek_event()?, Some(Ev::SeqEnd)) {
3146                    items.push(self.compose_node()?);
3147                }
3148                self.p.get_event()?;
3149                Ok(Node::Seq { tag, items })
3150            }
3151            Some(Ev::MapStart { .. }) => {
3152                let tag = if let Ev::MapStart { tag, .. } = self.p.get_event()? {
3153                    match tag.as_deref() {
3154                        None | Some("!") => TAG_MAP.to_string(),
3155                        Some(t) => t.to_string(),
3156                    }
3157                } else {
3158                    unreachable!()
3159                };
3160                let mut pairs = Vec::new();
3161                while !matches!(self.p.peek_event()?, Some(Ev::MapEnd)) {
3162                    let key = self.compose_node()?;
3163                    let value = self.compose_node()?;
3164                    pairs.push((key, value));
3165                }
3166                self.p.get_event()?;
3167                Ok(Node::Map { tag, pairs })
3168            }
3169            _ => Err(YErr::Internal(
3170                "UnboundLocalError: compose_node on non-node event".to_string(),
3171            )),
3172        }
3173    }
3174}
3175
3176// ---------------------------------------------------------------------------
3177// Constructor (yaml/constructor.py SafeConstructor + _no_duplicates)
3178// ---------------------------------------------------------------------------
3179
3180fn construct_scalar_value(node: &Node) -> Result<String, YErr> {
3181    if let Node::Map { pairs, .. } = node {
3182        for (k, v) in pairs {
3183            if k.tag() == TAG_VALUE {
3184                return construct_scalar_value(v);
3185            }
3186        }
3187    }
3188    match node {
3189        Node::Scalar { value, .. } => Ok(value.clone()),
3190        _ => Err(YErr::Marked(format!(
3191            "expected a scalar node, but found {}",
3192            node.id()
3193        ))),
3194    }
3195}
3196
3197fn construct_yaml_bool(node: &Node) -> Result<Yaml, YErr> {
3198    let value = construct_scalar_value(node)?;
3199    match value.to_lowercase().as_str() {
3200        "yes" | "true" | "on" => Ok(Yaml::Bool(true)),
3201        "no" | "false" | "off" => Ok(Yaml::Bool(false)),
3202        // Oracle: KeyError out of bool_values (uncaught crash).
3203        other => Err(YErr::Internal(format!(
3204            "KeyError: {}",
3205            py_repr_str(other)
3206        ))),
3207    }
3208}
3209
3210/// CPython `int(s, base)` for base 2/8/10/16 over the strings PyYAML's int
3211/// constructor produces: Unicode strip, one optional sign, an optional
3212/// matching base prefix (`0b`/`0B`, `0o`/`0O`, `0x`/`0X` — never for base
3213/// 10), single underscores strictly between digits (or right after the
3214/// prefix), and — base 10 only — the 4300-digit conversion limit, checked on
3215/// the leading digit span before trailing junk is diagnosed (matches CPython
3216/// order: `int('9'*4301 + 'z')` reports the limit, not the literal).
3217/// Returns (negative, magnitude). Invalid input mirrors the oracle's uncaught
3218/// `ValueError` (decision 3).
3219/// CPython also accepts non-ASCII `Nd` decimal digits
3220/// (`int('٥') == 5`); those still report invalid-literal here.
3221fn py_int_parse(s: &str, base: u32) -> Result<(bool, Mag), YErr> {
3222    let invalid = || {
3223        YErr::Internal(format!(
3224            "ValueError: invalid literal for int() with base {}: {}",
3225            base,
3226            py_repr_str(s)
3227        ))
3228    };
3229    let t = py_strip(s);
3230    let chars: Vec<char> = t.chars().collect();
3231    let mut i = 0;
3232    let neg = match chars.first() {
3233        Some('-') => {
3234            i = 1;
3235            true
3236        }
3237        Some('+') => {
3238            i = 1;
3239            false
3240        }
3241        _ => false,
3242    };
3243    // Optional base prefix (only the one matching `base`).
3244    let prefix = match base {
3245        2 => Some(('b', 'B')),
3246        8 => Some(('o', 'O')),
3247        16 => Some(('x', 'X')),
3248        _ => None,
3249    };
3250    if let Some((lo, hi)) = prefix {
3251        if chars.get(i) == Some(&'0') && matches!(chars.get(i + 1), Some(c) if *c == lo || *c == hi)
3252        {
3253            i += 2;
3254            // A single underscore may follow the prefix.
3255            if chars.get(i) == Some(&'_') && chars.get(i + 1).is_some_and(|c| c.is_digit(base)) {
3256                i += 1;
3257            }
3258        }
3259    }
3260    // The 4300-digit limit is checked on the leading digit/underscore span.
3261    if base == 10 {
3262        let span_digits = chars[i..]
3263            .iter()
3264            .take_while(|c| c.is_ascii_digit() || **c == '_')
3265            .filter(|c| c.is_ascii_digit())
3266            .count();
3267        if span_digits > INT_MAX_STR_DIGITS {
3268            return Err(int_parse_limit_err(span_digits));
3269        }
3270    }
3271    let mut mag = Mag::zero();
3272    let mut prev_digit = false;
3273    let mut ndigits = 0usize;
3274    while i < chars.len() {
3275        let c = chars[i];
3276        if c == '_' {
3277            if !prev_digit || !chars.get(i + 1).is_some_and(|c| c.is_digit(base)) {
3278                return Err(invalid());
3279            }
3280            prev_digit = false;
3281        } else if let Some(d) = c.to_digit(base) {
3282            if !c.is_ascii() {
3283                // Unicode digits: SEAM above (to_digit is ASCII-only anyway).
3284                return Err(invalid());
3285            }
3286            mag.mul_add_small(base as u64, d as u64);
3287            prev_digit = true;
3288            ndigits += 1;
3289        } else {
3290            return Err(invalid());
3291        }
3292        i += 1;
3293    }
3294    if ndigits == 0 {
3295        return Err(invalid());
3296    }
3297    Ok((neg, mag))
3298}
3299
3300fn construct_yaml_int(node: &Node) -> Result<Yaml, YErr> {
3301    let raw = construct_scalar_value(node)?;
3302    let value = raw.replace('_', "");
3303    if value.is_empty() {
3304        // Oracle: IndexError on value[0] (uncaught crash).
3305        return Err(YErr::Internal(
3306            "IndexError: string index out of range".to_string(),
3307        ));
3308    }
3309    let mut neg = false;
3310    let mut v = value.as_str();
3311    if v.starts_with('-') {
3312        neg = true;
3313        v = &v[1..];
3314    } else if v.starts_with('+') {
3315        v = &v[1..];
3316    }
3317    if v == "0" {
3318        return Ok(Yaml::Int(0));
3319    }
3320    if let Some(rest) = v.strip_prefix("0b") {
3321        let (pneg, mag) = py_int_parse(rest, 2)?;
3322        return Ok(yaml_int(neg ^ pneg, &mag));
3323    }
3324    if let Some(rest) = v.strip_prefix("0x") {
3325        let (pneg, mag) = py_int_parse(rest, 16)?;
3326        return Ok(yaml_int(neg ^ pneg, &mag));
3327    }
3328    if v.starts_with('0') {
3329        let (pneg, mag) = py_int_parse(v, 8)?;
3330        return Ok(yaml_int(neg ^ pneg, &mag));
3331    }
3332    if v.contains(':') {
3333        // digits.reverse() + base*=60 in PyYAML == Horner in document order.
3334        // Parts go through full int(part), so each may carry its own sign.
3335        let mut acc_neg = false;
3336        let mut acc = Mag::zero();
3337        for part in v.split(':') {
3338            let (pneg, pmag) = py_int_parse(part, 10)?;
3339            acc.mul_add_small(60, 0);
3340            if acc_neg == pneg || pmag.is_zero() {
3341                acc.add(&pmag);
3342            } else {
3343                match acc.cmp_mag(&pmag) {
3344                    std::cmp::Ordering::Less => {
3345                        let mut m = pmag.clone();
3346                        m.sub(&acc);
3347                        acc = m;
3348                        acc_neg = pneg;
3349                    }
3350                    std::cmp::Ordering::Equal => {
3351                        acc = Mag::zero();
3352                        acc_neg = false;
3353                    }
3354                    std::cmp::Ordering::Greater => acc.sub(&pmag),
3355                }
3356            }
3357            if acc.is_zero() {
3358                acc_neg = false;
3359            }
3360        }
3361        return Ok(yaml_int(neg ^ acc_neg, &acc));
3362    }
3363    let (pneg, mag) = py_int_parse(v, 10)?;
3364    Ok(yaml_int(neg ^ pneg, &mag))
3365}
3366
3367fn construct_yaml_float(node: &Node) -> Result<Yaml, YErr> {
3368    let raw = construct_scalar_value(node)?;
3369    let value = raw.replace('_', "").to_lowercase();
3370    if value.is_empty() {
3371        return Err(YErr::Internal(
3372            "IndexError: string index out of range".to_string(),
3373        ));
3374    }
3375    let mut sign = 1.0f64;
3376    let mut v = value.as_str();
3377    if v.starts_with('-') {
3378        sign = -1.0;
3379        v = &v[1..];
3380    } else if v.starts_with('+') {
3381        v = &v[1..];
3382    }
3383    if v == ".inf" {
3384        return Ok(Yaml::Float(sign * f64::INFINITY));
3385    }
3386    if v == ".nan" {
3387        // PyYAML returns the shared nan_value object; sign ignored.
3388        return Ok(Yaml::Float(f64::NAN));
3389    }
3390    if v.contains(':') {
3391        let mut digits: Vec<f64> = Vec::new();
3392        for part in v.split(':') {
3393            digits.push(py_float_parse(part)?);
3394        }
3395        digits.reverse();
3396        let mut base = 1.0f64;
3397        let mut acc = 0.0f64;
3398        for d in digits {
3399            acc += d * base;
3400            base *= 60.0;
3401        }
3402        return Ok(Yaml::Float(sign * acc));
3403    }
3404    Ok(Yaml::Float(sign * py_float_parse(v)?))
3405}
3406
3407/// Python `float(str)`: Unicode-strip, then the usual grammar incl.
3408/// `inf`/`infinity`/`nan` (input is already lowercased by the caller).
3409fn py_float_parse(s: &str) -> Result<f64, YErr> {
3410    let t = py_strip(s);
3411    match t.parse::<f64>() {
3412        Ok(v) => Ok(v),
3413        Err(_) => Err(YErr::Internal(format!(
3414            "ValueError: could not convert string to float: {}",
3415            py_repr_str(s)
3416        ))),
3417    }
3418}
3419
3420fn construct_yaml_binary(node: &Node) -> Result<Yaml, YErr> {
3421    let value = construct_scalar_value(node)?;
3422    if let Some((pos, c)) = value.chars().enumerate().find(|(_, c)| !c.is_ascii()) {
3423        let cp = c as u32;
3424        let esc = if cp < 0x100 {
3425            format!("\\x{cp:02x}")
3426        } else if cp < 0x10000 {
3427            format!("\\u{cp:04x}")
3428        } else {
3429            format!("\\U{cp:08x}")
3430        };
3431        return Err(YErr::Marked(format!(
3432            "failed to convert base64 data into ascii: 'ascii' codec can't encode character '{esc}' in position {pos}: ordinal not in range(128)"
3433        )));
3434    }
3435    match decode_base64_lenient(value.as_bytes()) {
3436        Ok(bytes) => Ok(Yaml::Bytes(bytes)),
3437        Err(msg) => Err(YErr::Marked(format!(
3438            "failed to decode base64 data: {msg}"
3439        ))),
3440    }
3441}
3442
3443/// `base64.decodebytes` (binascii a2b_base64, non-strict): non-alphabet
3444/// characters are skipped; padding after >=2 quad chars terminates decode.
3445/// Error-message coverage is limited to the common forms.
3446fn decode_base64_lenient(data: &[u8]) -> Result<Vec<u8>, String> {
3447    fn val(b: u8) -> Option<u32> {
3448        match b {
3449            b'A'..=b'Z' => Some((b - b'A') as u32),
3450            b'a'..=b'z' => Some((b - b'a' + 26) as u32),
3451            b'0'..=b'9' => Some((b - b'0' + 52) as u32),
3452            b'+' => Some(62),
3453            b'/' => Some(63),
3454            _ => None,
3455        }
3456    }
3457    let mut out = Vec::new();
3458    let mut acc: u32 = 0;
3459    let mut quad = 0usize;
3460    let mut ndata = 0usize;
3461    for &b in data {
3462        if b == b'=' && quad >= 2 {
3463            match quad {
3464                2 => out.push((acc >> 4) as u8),
3465                3 => {
3466                    out.push((acc >> 10) as u8);
3467                    out.push(((acc >> 2) & 0xff) as u8);
3468                }
3469                _ => {}
3470            }
3471            return Ok(out);
3472        }
3473        if let Some(v) = val(b) {
3474            acc = (acc << 6) | v;
3475            quad += 1;
3476            ndata += 1;
3477            if quad == 4 {
3478                out.push((acc >> 16) as u8);
3479                out.push(((acc >> 8) & 0xff) as u8);
3480                out.push((acc & 0xff) as u8);
3481                acc = 0;
3482                quad = 0;
3483            }
3484        }
3485    }
3486    match quad {
3487        0 => Ok(out),
3488        1 => Err(format!(
3489            "Invalid base64-encoded string: number of data characters ({ndata}) cannot be 1 more than a multiple of 4"
3490        )),
3491        _ => Err("Incorrect padding".to_string()),
3492    }
3493}
3494
3495fn days_in_month(year: i64, month: u32) -> u32 {
3496    match month {
3497        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3498        4 | 6 | 9 | 11 => 30,
3499        2 => {
3500            if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
3501                29
3502            } else {
3503                28
3504            }
3505        }
3506        _ => 0,
3507    }
3508}
3509
3510/// The *constructor's* timestamp regexp (more lenient than the resolver's:
3511/// 1-2 digit month/day in the date-only form too).
3512struct TsParts {
3513    year: i64,
3514    month: u32,
3515    day: u32,
3516    time: Option<TsTime>,
3517    // time = (hour, minute, second, fraction_digits, tz_sign_hour_minute, tz_z)
3518}
3519
3520type TsTime = (u32, u32, u32, String, Option<(i64, u32, u32)>, bool);
3521
3522fn match_ts_constructor(v: &str) -> Option<TsParts> {
3523    let b = v.as_bytes();
3524    if !b.is_ascii() {
3525        return None;
3526    }
3527    let d = |i: usize| b.get(i).is_some_and(u8::is_ascii_digit);
3528    if !(d(0) && d(1) && d(2) && d(3)) {
3529        return None;
3530    }
3531    let year: i64 = v[0..4].parse().ok()?;
3532    let mut i = 4;
3533    if b.get(i) != Some(&b'-') || !d(i + 1) {
3534        return None;
3535    }
3536    i += 1;
3537    let m_start = i;
3538    i += 1;
3539    if d(i) {
3540        i += 1;
3541    }
3542    let month: u32 = v[m_start..i].parse().ok()?;
3543    if b.get(i) != Some(&b'-') || !d(i + 1) {
3544        return None;
3545    }
3546    i += 1;
3547    let d_start = i;
3548    i += 1;
3549    if d(i) {
3550        i += 1;
3551    }
3552    let day: u32 = v[d_start..i].parse().ok()?;
3553    if i == b.len() {
3554        return Some(TsParts {
3555            year,
3556            month,
3557            day,
3558            time: None,
3559        });
3560    }
3561    match b.get(i) {
3562        Some(b'T') | Some(b't') => i += 1,
3563        Some(b' ') | Some(b'\t') => {
3564            while matches!(b.get(i), Some(b' ') | Some(b'\t')) {
3565                i += 1;
3566            }
3567        }
3568        _ => return None,
3569    }
3570    if !d(i) {
3571        return None;
3572    }
3573    let h_start = i;
3574    i += 1;
3575    if d(i) {
3576        i += 1;
3577    }
3578    let hour: u32 = v[h_start..i].parse().ok()?;
3579    if b.get(i) != Some(&b':') || !(d(i + 1) && d(i + 2)) {
3580        return None;
3581    }
3582    let minute: u32 = v[i + 1..i + 3].parse().ok()?;
3583    i += 3;
3584    if b.get(i) != Some(&b':') || !(d(i + 1) && d(i + 2)) {
3585        return None;
3586    }
3587    let second: u32 = v[i + 1..i + 3].parse().ok()?;
3588    i += 3;
3589    let mut fraction = String::new();
3590    if b.get(i) == Some(&b'.') {
3591        i += 1;
3592        while d(i) {
3593            fraction.push(b[i] as char);
3594            i += 1;
3595        }
3596    }
3597    let mut tz: Option<(i64, u32, u32)> = None;
3598    let mut tz_z = false;
3599    if i < b.len() {
3600        while matches!(b.get(i), Some(b' ') | Some(b'\t')) {
3601            i += 1;
3602        }
3603        match b.get(i) {
3604            Some(b'Z') => {
3605                tz_z = true;
3606                i += 1;
3607            }
3608            Some(&s @ (b'-' | b'+')) => {
3609                i += 1;
3610                if !d(i) {
3611                    return None;
3612                }
3613                let th_start = i;
3614                i += 1;
3615                if d(i) {
3616                    i += 1;
3617                }
3618                let tz_hour: u32 = v[th_start..i].parse().ok()?;
3619                let mut tz_minute = 0u32;
3620                if b.get(i) == Some(&b':') {
3621                    if !(d(i + 1) && d(i + 2)) {
3622                        return None;
3623                    }
3624                    tz_minute = v[i + 1..i + 3].parse().ok()?;
3625                    i += 3;
3626                }
3627                tz = Some((if s == b'-' { -1 } else { 1 }, tz_hour, tz_minute));
3628            }
3629            _ => return None,
3630        }
3631        if i != b.len() {
3632            return None;
3633        }
3634    }
3635    Some(TsParts {
3636        year,
3637        month,
3638        day,
3639        time: Some((hour, minute, second, fraction, tz, tz_z)),
3640    })
3641}
3642
3643fn construct_yaml_timestamp(node: &Node) -> Result<Yaml, YErr> {
3644    let value = match node {
3645        Node::Scalar { value, .. } => value.clone(),
3646        // Python calls construct_scalar first (errors on non-scalar); if that
3647        // somehow succeeds (value-key map), the regexp match on `node.value`
3648        // then crashes the oracle with a TypeError.
3649        // Reachable only via a value-key mapping (`!!timestamp {=: ...}`):
3650        // `construct_scalar` succeeds, then `re.match` runs on `node.value`
3651        // — a list for a MappingNode (verified against Python 3.11.15).
3652        _ => {
3653            construct_scalar_value(node)?;
3654            return Err(YErr::Internal(
3655                "TypeError: expected string or bytes-like object, got 'list'".to_string(),
3656            ));
3657        }
3658    };
3659    let parts = match match_ts_constructor(&value) {
3660        Some(p) => p,
3661        // Oracle: AttributeError on match.groupdict() (uncaught crash).
3662        None => {
3663            return Err(YErr::Internal(
3664                "AttributeError: 'NoneType' object has no attribute 'groupdict'".to_string(),
3665            ))
3666        }
3667    };
3668    // datetime.date / datetime.datetime range validation (ValueError crashes
3669    // in the oracle — PORT-CONTRACT decision 3).
3670    if parts.year < 1 {
3671        return Err(YErr::Internal(format!(
3672            "ValueError: year {} is out of range",
3673            parts.year
3674        )));
3675    }
3676    if !(1..=12).contains(&parts.month) {
3677        return Err(YErr::Internal(
3678            "ValueError: month must be in 1..12".to_string(),
3679        ));
3680    }
3681    if parts.day < 1 || parts.day > days_in_month(parts.year, parts.month) {
3682        return Err(YErr::Internal(
3683            "ValueError: day is out of range for month".to_string(),
3684        ));
3685    }
3686    let Some((hour, minute, second, fraction, tz, tz_z)) = parts.time else {
3687        return Ok(Yaml::Date {
3688            year: parts.year,
3689            month: parts.month,
3690            day: parts.day,
3691        });
3692    };
3693    if hour > 23 {
3694        return Err(YErr::Internal(
3695            "ValueError: hour must be in 0..23".to_string(),
3696        ));
3697    }
3698    if minute > 59 {
3699        return Err(YErr::Internal(
3700            "ValueError: minute must be in 0..59".to_string(),
3701        ));
3702    }
3703    if second > 59 {
3704        return Err(YErr::Internal(
3705            "ValueError: second must be in 0..59".to_string(),
3706        ));
3707    }
3708    let micro = if fraction.is_empty() {
3709        0
3710    } else {
3711        let mut f: String = fraction.chars().take(6).collect();
3712        while f.len() < 6 {
3713            f.push('0');
3714        }
3715        f.parse::<u32>().unwrap_or(0)
3716    };
3717    let tzinfo: Option<i64> = if let Some((sign, th, tm)) = tz {
3718        let offset = sign * (th as i64 * 3600 + tm as i64 * 60);
3719        if offset.abs() >= 24 * 3600 {
3720            return Err(YErr::Internal(format!(
3721                "ValueError: offset must be a timedelta strictly between -timedelta(hours=24) and timedelta(hours=24), not {}.",
3722                py_repr_timedelta(offset)
3723            )));
3724        }
3725        Some(offset)
3726    } else if tz_z {
3727        Some(0)
3728    } else {
3729        None
3730    };
3731    Ok(Yaml::DateTime {
3732        year: parts.year,
3733        month: parts.month,
3734        day: parts.day,
3735        hour,
3736        minute,
3737        second,
3738        micro,
3739        tz: tzinfo,
3740    })
3741}
3742
3743/// `SafeConstructor.flatten_mapping` — merge-key flattening (reachable only
3744/// via `!!set` here, since strict-map key construction rejects the merge tag
3745/// before flattening ever runs on the default map tag).
3746fn flatten_pairs(pairs: &[(Node, Node)]) -> Result<Vec<(Node, Node)>, YErr> {
3747    let mut merge: Vec<(Node, Node)> = Vec::new();
3748    let mut rest: Vec<(Node, Node)> = Vec::new();
3749    for (k, v) in pairs {
3750        if k.tag() == TAG_MERGE {
3751            match v {
3752                Node::Map {
3753                    pairs: sub_pairs, ..
3754                } => {
3755                    merge.extend(flatten_pairs(sub_pairs)?);
3756                }
3757                Node::Seq { items, .. } => {
3758                    let mut submerge = Vec::new();
3759                    for sub in items {
3760                        let Node::Map {
3761                            pairs: sub_pairs, ..
3762                        } = sub
3763                        else {
3764                            return Err(YErr::Marked(format!(
3765                                "expected a mapping for merging, but found {}",
3766                                sub.id()
3767                            )));
3768                        };
3769                        submerge.push(flatten_pairs(sub_pairs)?);
3770                    }
3771                    submerge.reverse();
3772                    for value in submerge {
3773                        merge.extend(value);
3774                    }
3775                }
3776                _ => {
3777                    return Err(YErr::Marked(format!(
3778                        "expected a mapping or list of mappings for merging, but found {}",
3779                        v.id()
3780                    )));
3781                }
3782            }
3783        } else if k.tag() == TAG_VALUE {
3784            let retagged = match k {
3785                Node::Scalar { value, .. } => Node::Scalar {
3786                    tag: TAG_STR.to_string(),
3787                    value: value.clone(),
3788                },
3789                other => other.clone(),
3790            };
3791            rest.push((retagged, v.clone()));
3792        } else {
3793            rest.push((k.clone(), v.clone()));
3794        }
3795    }
3796    merge.extend(rest);
3797    Ok(merge)
3798}
3799
3800/// `BaseConstructor.construct_mapping` after flattening — dict semantics
3801/// (first-position/last-value on Python-equal keys), used by `!!set`.
3802fn construct_mapping_plain(node: &Node) -> Result<Vec<(Yaml, Yaml)>, YErr> {
3803    let Node::Map { pairs, .. } = node else {
3804        return Err(YErr::Marked(format!(
3805            "expected a mapping node, but found {}",
3806            node.id()
3807        )));
3808    };
3809    let flat = flatten_pairs(pairs)?;
3810    let mut out: Vec<(Yaml, Yaml)> = Vec::new();
3811    for (k_node, v_node) in &flat {
3812        let key = construct_value(k_node)?;
3813        if let Some(_name) = unhashable_type_name(&key) {
3814            return Err(YErr::Marked("found unhashable key".to_string()));
3815        }
3816        let value = construct_value(v_node)?;
3817        if let Some(slot) = out.iter_mut().find(|(k, _)| py_eq(k, &key)) {
3818            slot.1 = value;
3819        } else {
3820            out.push((key, value));
3821        }
3822    }
3823    Ok(out)
3824}
3825
3826/// `_no_duplicates` + `SafeConstructor.construct_mapping` for the default
3827/// map tag: every key is constructed eagerly and checked against a set with
3828/// Python equality semantics before any value is constructed.
3829fn construct_strict_map(node: &Node) -> Result<Yaml, YErr> {
3830    let Node::Map { pairs, .. } = node else {
3831        // The oracle's `_no_duplicates` iterates `node.value` directly with
3832        // no mapping type check, so an explicit `!!map` on a non-mapping node
3833        // crashes it before the base constructor's ConstructorError can fire.
3834        // ORACLE DIVERGENCE (PORT-CONTRACT decision 3):
3835        // mirror those crashes as internal markers, same as every other
3836        // oracle-crash class. Only *empty* scalars/sequences skip the loop
3837        // and reach the caught "expected a mapping node" ConstructorError.
3838        return match node {
3839            Node::Scalar { value, .. } if !value.is_empty() => {
3840                // `for key, _ in "<str>"` unpacks 1-char strings.
3841                Err(YErr::Internal(
3842                    "ValueError: not enough values to unpack (expected 2, got 1)".to_string(),
3843                ))
3844            }
3845            Node::Seq { items, .. } if !items.is_empty() => Err(YErr::Internal(format!(
3846                "TypeError: cannot unpack non-iterable {} object",
3847                py_node_class(&items[0])
3848            ))),
3849            _ => Err(YErr::Marked(format!(
3850                "expected a mapping node, but found {}",
3851                node.id()
3852            ))),
3853        };
3854    };
3855    let mut seen: Vec<Yaml> = Vec::new();
3856    for (k_node, _) in pairs {
3857        let key = construct_value(k_node)?;
3858        if unhashable_type_name(&key).is_some() {
3859            // The healed oracle reports an unhashable key as a structured
3860            // envelope failure (frontmatter.py `_no_duplicates`) instead of
3861            // crashing at the `key in seen` test, so the former decision-3
3862            // divergence marker converges to the same malformed-frontmatter
3863            // issue text. py_repr mirrors the oracle's `{key!r}`.
3864            return Err(YErr::Marked(format!(
3865                "unhashable frontmatter key: {}",
3866                py_repr(&key)?
3867            )));
3868        }
3869        if seen.iter().any(|s| py_eq(s, &key)) {
3870            // py_repr can itself crash the oracle (4300-digit str limit).
3871            return Err(YErr::Marked(format!(
3872                "duplicate frontmatter key: {}",
3873                py_repr(&key)?
3874            )));
3875        }
3876        seen.push(key);
3877    }
3878    // flatten_mapping is a no-op here: merge/value-tagged keys already failed
3879    // during eager key construction above.
3880    let mut out: Vec<(Yaml, Yaml)> = Vec::new();
3881    for (k_node, v_node) in pairs {
3882        let key = construct_value(k_node)?;
3883        let value = construct_value(v_node)?;
3884        out.push((key, value));
3885    }
3886    Ok(Yaml::Map(out))
3887}
3888
3889fn construct_omap_pairs(node: &Node) -> Result<Yaml, YErr> {
3890    // Context strings differ ("an ordered map" vs "pairs") but only the
3891    // problem text reaches output, and those are identical.
3892    let Node::Seq { items, .. } = node else {
3893        return Err(YErr::Marked(format!(
3894            "expected a sequence, but found {}",
3895            node.id()
3896        )));
3897    };
3898    let mut out: Vec<Yaml> = Vec::new();
3899    for sub in items {
3900        let Node::Map { pairs, .. } = sub else {
3901            return Err(YErr::Marked(format!(
3902                "expected a mapping of length 1, but found {}",
3903                sub.id()
3904            )));
3905        };
3906        if pairs.len() != 1 {
3907            return Err(YErr::Marked(format!(
3908                "expected a single mapping item, but found {} items",
3909                pairs.len()
3910            )));
3911        }
3912        let key = construct_value(&pairs[0].0)?;
3913        let value = construct_value(&pairs[0].1)?;
3914        out.push(Yaml::Tuple(vec![key, value]));
3915    }
3916    Ok(Yaml::List(out))
3917}
3918
3919fn construct_value(node: &Node) -> Result<Yaml, YErr> {
3920    match node.tag() {
3921        TAG_MAP => construct_strict_map(node),
3922        TAG_STR => Ok(Yaml::Str(construct_scalar_value(node)?)),
3923        TAG_NULL => {
3924            construct_scalar_value(node)?;
3925            Ok(Yaml::Null)
3926        }
3927        TAG_BOOL => construct_yaml_bool(node),
3928        TAG_INT => construct_yaml_int(node),
3929        TAG_FLOAT => construct_yaml_float(node),
3930        "tag:yaml.org,2002:binary" => construct_yaml_binary(node),
3931        TAG_TIMESTAMP => construct_yaml_timestamp(node),
3932        TAG_SEQ => {
3933            let Node::Seq { items, .. } = node else {
3934                return Err(YErr::Marked(format!(
3935                    "expected a sequence node, but found {}",
3936                    node.id()
3937                )));
3938            };
3939            let mut out = Vec::new();
3940            for item in items {
3941                out.push(construct_value(item)?);
3942            }
3943            Ok(Yaml::List(out))
3944        }
3945        "tag:yaml.org,2002:omap" | "tag:yaml.org,2002:pairs" => construct_omap_pairs(node),
3946        "tag:yaml.org,2002:set" => {
3947            let pairs = construct_mapping_plain(node)?;
3948            Ok(Yaml::Set(pairs.into_iter().map(|(k, _)| k).collect()))
3949        }
3950        other => Err(YErr::Marked(format!(
3951            "could not determine a constructor for the tag {}",
3952            py_repr_str(other)
3953        ))),
3954    }
3955}
3956
3957/// `yaml.load(raw, Loader=_BoundedLoader)`.
3958fn yaml_load(raw: &str) -> Result<Yaml, YErr> {
3959    check_printable(raw)?;
3960    let scanner = Scanner::new(raw);
3961    let parser = Parser::new(scanner);
3962    let mut composer = Composer {
3963        p: parser,
3964        anchors: std::collections::HashSet::new(),
3965        depth: 0,
3966    };
3967    match composer.get_single_node()? {
3968        None => Ok(Yaml::Null),
3969        Some(node) => construct_value(&node),
3970    }
3971}
3972
3973// ---------------------------------------------------------------------------
3974// parse_frontmatter — envelope load + field validation
3975// ---------------------------------------------------------------------------
3976
3977fn map_get<'a>(pairs: &'a [(Yaml, Yaml)], name: &str) -> Option<&'a Yaml> {
3978    pairs.iter().find_map(|(k, v)| match k {
3979        Yaml::Str(s) if s == name => Some(v),
3980        _ => None,
3981    })
3982}
3983
3984/// The envelope load (`_load_frontmatter_mapping`): oversize gate, bounded
3985/// YAML load, exception→issue mapping, non-mapping rejection. Public so the
3986/// conformance vectors can compare the loaded value model directly.
3987pub fn load_frontmatter_mapping(raw: &str) -> (Option<Vec<(Yaml, Yaml)>>, Vec<Issue>) {
3988    if exceeds_byte_cap(raw, MAX_FRONTMATTER_BYTES) {
3989        return (
3990            None,
3991            vec![Issue::error(
3992                "malformed-frontmatter",
3993                format!("frontmatter exceeds the {MAX_FRONTMATTER_BYTES}-byte cap"),
3994            )],
3995        );
3996    }
3997    match yaml_load(raw) {
3998        Ok(Yaml::Map(pairs)) => (Some(pairs), Vec::new()),
3999        Ok(_) => (
4000            None,
4001            vec![Issue::error(
4002                "malformed-frontmatter",
4003                "frontmatter must be a YAML mapping of supported fields".to_string(),
4004            )],
4005        ),
4006        Err(YErr::Marked(problem)) => {
4007            if problem.contains("duplicate frontmatter key") {
4008                (
4009                    None,
4010                    vec![Issue::error("duplicate-frontmatter-key", problem)],
4011                )
4012            } else {
4013                (
4014                    None,
4015                    vec![Issue::error(
4016                        "malformed-frontmatter",
4017                        format!("frontmatter is not valid YAML: {problem}"),
4018                    )],
4019                )
4020            }
4021        }
4022        Err(YErr::Reader(msg)) => (
4023            None,
4024            vec![Issue::error(
4025                "malformed-frontmatter",
4026                format!("frontmatter is not valid YAML: {msg}"),
4027            )],
4028        ),
4029        // ORACLE DIVERGENCE (PORT-CONTRACT decision 3): these inputs crash
4030        // the oracle with an uncaught exception; we return a distinguishable
4031        // internal issue instead. The code below never appears in oracle
4032        // output, so any occurrence flags the divergence for the harness.
4033        Err(YErr::Internal(msg)) => (
4034            None,
4035            vec![Issue {
4036                severity: "error",
4037                code: "internal-oracle-divergence".to_string(),
4038                message: msg,
4039                line: None,
4040            }],
4041        ),
4042    }
4043}
4044
4045/// Full-document YAML load for the strict `.decided/config.yaml` readers
4046/// (`decided gate`, `decided.services.init._parse_config_yaml`): `Ok(root)` on a
4047/// clean parse, `Err(problem text)` otherwise. The oracle embeds PyYAML's
4048/// exact multi-line exception prose in its `invalid YAML: <exc>` message;
4049/// that prose is not byte-reproducible here (stderr is out of parity
4050/// scope), so the error text is the engine's own parse problem.
4051pub fn yaml_load_config(raw: &str) -> Result<Yaml, String> {
4052    yaml_load(raw).map_err(|e| match e {
4053        YErr::Marked(p) | YErr::Reader(p) | YErr::Internal(p) => p,
4054    })
4055}
4056
4057fn check_unknown_fields(pairs: &[(Yaml, Yaml)], issues: &mut Vec<Issue>) -> Result<(), YErr> {
4058    for (key, _) in pairs {
4059        let known = matches!(key, Yaml::Str(s) if SUPPORTED_FIELDS.contains(&s.as_str()));
4060        if !known {
4061            issues.push(Issue::error(
4062                "invalid-metadata-field",
4063                format!(
4064                    "unsupported frontmatter field: {} (supported: {})",
4065                    py_repr(key)?,
4066                    SUPPORTED_FIELDS.join(", ")
4067                ),
4068            ));
4069        }
4070    }
4071    Ok(())
4072}
4073
4074fn validate_schema_version(
4075    pairs: &[(Yaml, Yaml)],
4076    issues: &mut Vec<Issue>,
4077) -> Result<Option<SchemaVersion>, YErr> {
4078    let Some(value) = map_get(pairs, "schema_version") else {
4079        issues.push(Issue::error(
4080            "invalid-metadata-field",
4081            "frontmatter is missing required field 'schema_version'".to_string(),
4082        ));
4083        return Ok(None); // data.get() is None here
4084    };
4085    // Python: isinstance(v, int) and not isinstance(v, bool) — bignums pass.
4086    let (supported, sv) = match value {
4087        Yaml::Int(v) => (SUPPORTED_SCHEMA_VERSIONS.contains(v), SchemaVersion::Int(*v)),
4088        Yaml::BigInt(b) => (false, SchemaVersion::Big(b.clone())),
4089        _ => {
4090            issues.push(Issue::error(
4091                "invalid-metadata-field",
4092                "frontmatter field 'schema_version' must be an integer".to_string(),
4093            ));
4094            return Ok(None);
4095        }
4096    };
4097    if !supported {
4098        // f-string str(int): over the 4300-digit limit the oracle crashes.
4099        if let SchemaVersion::Big(b) = &sv {
4100            if b.digits.len() > INT_MAX_STR_DIGITS {
4101                return Err(int_to_str_limit_err());
4102            }
4103        }
4104        issues.push(Issue::error(
4105            "unsupported-schema-version",
4106            format!(
4107                "unsupported frontmatter schema_version: {} (supported: {})",
4108                sv,
4109                SUPPORTED_SCHEMA_VERSIONS
4110                    .iter()
4111                    .map(|s| s.to_string())
4112                    .collect::<Vec<_>>()
4113                    .join(", ")
4114            ),
4115        ));
4116    }
4117    Ok(Some(sv))
4118}
4119
4120fn validate_id(pairs: &[(Yaml, Yaml)], issues: &mut Vec<Issue>) -> Option<String> {
4121    let value = map_get(pairs, "id")?;
4122    if matches!(value, Yaml::Null) {
4123        return None;
4124    }
4125    let Yaml::Str(s) = value else {
4126        issues.push(Issue::error(
4127            "invalid-metadata-field",
4128            "frontmatter field 'id' must be a string".to_string(),
4129        ));
4130        return None;
4131    };
4132    if !is_valid_id(s) {
4133        issues.push(Issue::error(
4134            "invalid-id-syntax",
4135            format!(
4136                "invalid artifact ID syntax: {} (expected <KEY>-<12-char Crockford base32 suffix>, e.g. RAC-01JY4M8X2QZ7)",
4137                py_repr_str(s)
4138            ),
4139        ));
4140        return None;
4141    }
4142    Some(normalize_id(s))
4143}
4144
4145fn validate_type(
4146    pairs: &[(Yaml, Yaml)],
4147    issues: &mut Vec<Issue>,
4148) -> Result<Option<String>, YErr> {
4149    let Some(value) = map_get(pairs, "type") else {
4150        return Ok(None);
4151    };
4152    if matches!(value, Yaml::Null) {
4153        return Ok(None);
4154    }
4155    let registered = match value {
4156        Yaml::Str(s) => crate::spec::spec_for(s).is_some(),
4157        _ => false,
4158    };
4159    if !registered {
4160        issues.push(Issue::error(
4161            "invalid-metadata-field",
4162            format!(
4163                "frontmatter field 'type' is not a registered artifact type: {}",
4164                py_repr(value)?
4165            ),
4166        ));
4167        return Ok(None);
4168    }
4169    Ok(match value {
4170        Yaml::Str(s) => Some(s.clone()),
4171        _ => None,
4172    })
4173}
4174
4175fn validate_relationships(
4176    pairs: &[(Yaml, Yaml)],
4177    issues: &mut Vec<Issue>,
4178) -> Vec<(String, Vec<String>)> {
4179    let Some(value) = map_get(pairs, "relationships") else {
4180        return Vec::new();
4181    };
4182    if matches!(value, Yaml::Null) {
4183        return Vec::new();
4184    }
4185    let well_formed = match value {
4186        Yaml::Map(rel_pairs) => rel_pairs.iter().all(|(kind, targets)| {
4187            matches!(kind, Yaml::Str(_))
4188                && matches!(targets, Yaml::List(items) if items.iter().all(|t| matches!(t, Yaml::Str(_))))
4189        }),
4190        _ => false,
4191    };
4192    if !well_formed {
4193        issues.push(Issue::error(
4194            "invalid-metadata-field",
4195            "frontmatter field 'relationships' must map relationship kinds to lists of artifact IDs"
4196                .to_string(),
4197        ));
4198        return Vec::new();
4199    }
4200    let Yaml::Map(rel_pairs) = value else {
4201        return Vec::new();
4202    };
4203    rel_pairs
4204        .iter()
4205        .map(|(kind, targets)| {
4206            let k = match kind {
4207                Yaml::Str(s) => s.clone(),
4208                _ => String::new(),
4209            };
4210            let t = match targets {
4211                Yaml::List(items) => items
4212                    .iter()
4213                    .map(|t| match t {
4214                        Yaml::Str(s) => normalize_id(s),
4215                        _ => String::new(),
4216                    })
4217                    .collect(),
4218                _ => Vec::new(),
4219            };
4220            (k, t)
4221        })
4222        .collect()
4223}
4224
4225fn parse_tags(pairs: &[(Yaml, Yaml)], issues: &mut Vec<Issue>) -> Vec<String> {
4226    let Some(value) = map_get(pairs, "tags") else {
4227        return Vec::new();
4228    };
4229    if matches!(value, Yaml::Null) {
4230        return Vec::new();
4231    }
4232    let well_formed = matches!(value, Yaml::List(items) if items
4233        .iter()
4234        .all(|t| matches!(t, Yaml::Str(s) if !py_strip(s).is_empty())));
4235    if !well_formed {
4236        issues.push(Issue::error(
4237            "invalid-metadata-field",
4238            "frontmatter field 'tags' must be a list of non-empty strings".to_string(),
4239        ));
4240        return Vec::new();
4241    }
4242    let Yaml::List(items) = value else {
4243        return Vec::new();
4244    };
4245    items
4246        .iter()
4247        .map(|t| match t {
4248            Yaml::Str(s) => py_strip(s).to_string(),
4249            _ => String::new(),
4250        })
4251        .collect()
4252}
4253
4254/// Parse and schema-validate raw frontmatter YAML.
4255///
4256/// Returns `(metadata, issues)`: metadata is `None` only on envelope-level
4257/// failures (oversize, malformed YAML, alias, depth, duplicate key,
4258/// non-mapping top level); field-level problems always return a constructed
4259/// metadata. Issue order is pinned: unknown fields (document order), then
4260/// schema_version, id, type, relationships, tags.
4261pub fn parse_frontmatter(raw: &str) -> (Option<ArtifactMetadata>, Vec<Issue>) {
4262    let (data, mut issues) = load_frontmatter_mapping(raw);
4263    let Some(pairs) = data else {
4264        return (None, issues);
4265    };
4266    match validate_fields(&pairs, &mut issues) {
4267        Ok(metadata) => (Some(metadata), issues),
4268        // ORACLE DIVERGENCE (PORT-CONTRACT decision 3): message formatting in
4269        // a field validator can crash the oracle (4300-digit int->str limit
4270        // on a bignum key/value). Mirror it as the internal marker; nothing
4271        // else survives the oracle's crash, so earlier issues are dropped.
4272        Err(YErr::Internal(msg)) => (
4273            None,
4274            vec![Issue {
4275                severity: "error",
4276                code: "internal-oracle-divergence".to_string(),
4277                message: msg,
4278                line: None,
4279            }],
4280        ),
4281        // Validators only raise the Internal class.
4282        Err(_) => unreachable!("field validators raise only internal errors"),
4283    }
4284}
4285
4286fn validate_fields(
4287    pairs: &[(Yaml, Yaml)],
4288    issues: &mut Vec<Issue>,
4289) -> Result<ArtifactMetadata, YErr> {
4290    check_unknown_fields(pairs, issues)?;
4291    let schema_version = validate_schema_version(pairs, issues)?;
4292    let id = validate_id(pairs, issues);
4293    let artifact_type = validate_type(pairs, issues)?;
4294    let relationships = validate_relationships(pairs, issues);
4295    let tags = parse_tags(pairs, issues);
4296    Ok(ArtifactMetadata {
4297        schema_version: schema_version.unwrap_or(SchemaVersion::Int(0)),
4298        id,
4299        artifact_type,
4300        relationships,
4301        tags,
4302        provenance: "frontmatter",
4303    })
4304}
4305
4306// ---------------------------------------------------------------------------
4307// parse_file support (src/rac/core/markdown.py read stage) — the frontmatter
4308// contract owns the wordings; the markdown module sequences the issues.
4309// ---------------------------------------------------------------------------
4310
4311/// "file cap" wording — emitted by `parse_file` for an oversized file.
4312pub fn oversize_file_issue(cap: u64) -> Issue {
4313    Issue {
4314        severity: "error",
4315        code: "artifact-oversize".to_string(),
4316        message: format!("artifact exceeds the {cap}-byte file cap (set DECIDED_MAX_FILE_BYTES to raise it)"),
4317        line: Some(1),
4318    }
4319}
4320
4321/// "parse cap" wording — emitted by `parse` for oversized text. Pinned as
4322/// distinct from the file-cap wording; do not unify.
4323pub fn oversize_parse_issue(cap: u64) -> Issue {
4324    Issue {
4325        severity: "error",
4326        code: "artifact-oversize".to_string(),
4327        message: format!("artifact exceeds the {cap}-byte parse cap (set DECIDED_MAX_FILE_BYTES to raise it)"),
4328        line: Some(1),
4329    }
4330}
4331
4332pub fn non_utf8_issue() -> Issue {
4333    Issue {
4334        severity: "warning",
4335        code: "non-utf8-content".to_string(),
4336        message: "artifact is not valid UTF-8; decoded lossily".to_string(),
4337        line: Some(1),
4338    }
4339}
4340
4341/// The unterminated-frontmatter issue `markdown.parse` appends when
4342/// `split.raw is None and split.unterminated`.
4343pub fn unterminated_issue() -> Issue {
4344    Issue {
4345        severity: "error",
4346        code: "malformed-frontmatter".to_string(),
4347        message: "frontmatter block opened with --- on line 1 but never closed".to_string(),
4348        line: Some(1),
4349    }
4350}
4351
4352#[derive(Debug)]
4353pub struct ArtifactRead {
4354    /// Decoded text (lossily when `lossy`); None on oversize/unreadable.
4355    pub text: Option<String>,
4356    /// The terminal read issue (oversize / unreadable), if any.
4357    pub issue: Option<Issue>,
4358    /// True when the bytes were not valid UTF-8 (warning appended by the
4359    /// caller AFTER parsing, as the last parse issue).
4360    pub lossy: bool,
4361}
4362
4363/// Python `str(OSError)`: `[Errno N] <strerror>: '<path>'`.
4364fn py_oserror_message(e: &std::io::Error, path: &str) -> String {
4365    let s = e.to_string();
4366    let msg = match s.find(" (os error") {
4367        Some(pos) => &s[..pos],
4368        None => s.as_str(),
4369    };
4370    match e.raw_os_error() {
4371        Some(n) => format!("[Errno {n}] {msg}: '{path}'"),
4372        None => format!("{msg}: '{path}'"),
4373    }
4374}
4375
4376/// The read stage of `parse_file`: size check, capped read, strict-then-lossy
4377/// UTF-8 decode (`errors="replace"`, one U+FFFD per bogus byte — Rust's
4378/// `from_utf8_lossy` follows the same WHATWG policy).
4379pub fn read_artifact_text(path: &str) -> ArtifactRead {
4380    use std::io::Read;
4381    let cap_state = file_cap();
4382    let unreadable = |e: &std::io::Error| ArtifactRead {
4383        text: None,
4384        issue: Some(Issue {
4385            severity: "error",
4386            code: "unreadable-artifact".to_string(),
4387            message: format!("cannot read artifact: {}", py_oserror_message(e, path)),
4388            line: Some(1),
4389        }),
4390        lossy: false,
4391    };
4392    let size = match std::fs::metadata(path) {
4393        Ok(m) => m.len(),
4394        Err(e) => return unreadable(&e),
4395    };
4396    let cap = match cap_state {
4397        FileCap::Cap(cap) => cap,
4398        // ORACLE DIVERGENCE (PORT-CONTRACT decision 3): a cap >= 2^63 - 1
4399        // makes the oracle's `fh.read(cap + 1)` crash uncaught on EVERY
4400        // successfully opened file. Mirror it as the marker. The oracle stats the path and opens the file first,
4401        // so an unreadable path still reports unreadable-artifact.
4402        FileCap::OracleCrash(msg) => {
4403            return match std::fs::File::open(path) {
4404                Ok(_) => ArtifactRead {
4405                    text: None,
4406                    issue: Some(Issue {
4407                        severity: "error",
4408                        code: "internal-oracle-divergence".to_string(),
4409                        message: msg.to_string(),
4410                        line: None,
4411                    }),
4412                    lossy: false,
4413                },
4414                Err(e) => unreadable(&e),
4415            };
4416        }
4417    };
4418    if size > cap {
4419        return ArtifactRead {
4420            text: None,
4421            issue: Some(oversize_file_issue(cap)),
4422            lossy: false,
4423        };
4424    }
4425    let mut data = Vec::new();
4426    match std::fs::File::open(path) {
4427        Ok(f) => {
4428            let mut handle = f.take(cap + 1);
4429            if let Err(e) = handle.read_to_end(&mut data) {
4430                return unreadable(&e);
4431            }
4432        }
4433        Err(e) => return unreadable(&e),
4434    }
4435    if data.len() as u64 > cap {
4436        return ArtifactRead {
4437            text: None,
4438            issue: Some(oversize_file_issue(cap)),
4439            lossy: false,
4440        };
4441    }
4442    match String::from_utf8(data) {
4443        Ok(text) => ArtifactRead {
4444            text: Some(text),
4445            issue: None,
4446            lossy: false,
4447        },
4448        Err(e) => {
4449            let text = String::from_utf8_lossy(e.as_bytes()).into_owned();
4450            ArtifactRead {
4451                text: Some(text),
4452                issue: None,
4453                lossy: true,
4454            }
4455        }
4456    }
4457}