Skip to main content

sup_xml_core/xsd/
validate.rs

1//! Instance-document validation against a compiled [`Schema`].
2//!
3//! Drives [`XmlReader`] events and walks the schema's content models in
4//! parallel.  Errors carry line/column locators inherited from the
5//! reader plus an XPath-style instance path (`/root/items/item[3]`).
6//!
7//! v1 implements:
8//! * Element / attribute structural validation.
9//! * Simple-type content validation against built-in types + facets.
10//! * Sequence / choice / all content models with occurrence bounds.
11//! * Substitution groups.
12//! * `xsi:type` overrides and `xsi:nil="true"` (when the schema's
13//!   `honor_xsi_nil` opt is on — currently always honored).
14//! * Wildcards (`xs:any` / `xs:anyAttribute`) with strict / lax / skip.
15//! * Identity constraints (`xs:key`, `xs:keyref`, `xs:unique`) —
16//!   uniqueness enforcement plus keyref referential integrity across
17//!   the constraint-declaring element's subtree.  Selectors and
18//!   fields use the XSD §3.11.6 XPath subset.
19
20use std::sync::Arc;
21
22use rustc_hash::FxHashMap as HashMap;
23
24use crate::reader::{Attr, EventInto, XmlReader};
25
26use super::error::{ValidationError, ValidationIssue, ValidationKind, ValidationOptions};
27
28mod walker;
29pub(crate) use walker::DocumentEventSource;
30
31/// Source of XML events that drives the XSD validator's state machine.
32///
33/// Both the streaming `XmlReader` path (used by `validate_str`) and
34/// the arena-DOM walker (used by `validate_doc`) implement this
35/// trait so the validator's content-model / namespace / identity-
36/// constraint logic doesn't need to know which source it's reading
37/// from.  Each method mirrors a single `XmlReader` accessor.
38pub(crate) trait XsdEventSource<'x> {
39    fn next_into(&mut self, attr_buf: &mut Vec<Attr<'x>>) -> crate::error::Result<EventInto<'x>>;
40    /// Byte offset of the `<` of the most recently emitted
41    /// StartElement; `None` if no StartElement has been emitted yet
42    /// or the source can't supply byte offsets (e.g. the DOM walker).
43    fn last_start_offset(&self) -> Option<usize>;
44    /// Current source offset.  DOM walkers return 0.
45    fn src_offset(&self) -> usize;
46    /// Translate a byte offset to a 1-based `(line, column)` pair.
47    /// DOM walkers return `(0, 0)` since they have no byte addressing.
48    fn line_col_at(&self, offset: usize) -> (u32, u32);
49    /// Add a default/fixed-value attribute to the *current* element (the
50    /// one whose `StartElement` was most recently emitted).  Only the
51    /// live-document source implements this; string/byte sources can't
52    /// mutate their input and leave it a no-op.
53    fn fill_default_attr(&self, _name: &str, _value: &str) {}
54    /// Stable identity key (the node's address) of the element whose
55    /// `StartElement` was most recently emitted, when the source can
56    /// supply one.  The arena-DOM walker returns the live node's
57    /// address so callers can recover per-node schema types after
58    /// validation (see [`PsviTypes`]); byte/stream sources have no
59    /// persistent nodes and return `None`.
60    fn current_node_key(&self) -> Option<usize> { None }
61}
62
63impl<'x> XsdEventSource<'x> for XmlReader<'x> {
64    #[inline] fn next_into(&mut self, buf: &mut Vec<Attr<'x>>) -> crate::error::Result<EventInto<'x>> {
65        XmlReader::next_into(self, buf)
66    }
67    #[inline] fn last_start_offset(&self) -> Option<usize> {
68        XmlReader::last_start_offset(self)
69    }
70    #[inline] fn src_offset(&self) -> usize {
71        XmlReader::src_offset(self)
72    }
73    #[inline] fn line_col_at(&self, offset: usize) -> (u32, u32) {
74        XmlReader::line_col_at(self, offset)
75    }
76}
77use super::identity::{
78    ConstraintKind, FieldPath, NameTest, PathExpr, PathStep, SelectorPath,
79};
80use super::schema::{
81    AttributeUseKind, BlockSet, ContentModel, ElementDecl, GroupKind,
82    Particle, ProcessContents, QName, Schema, Term, TypeRef, Wildcard,
83};
84use super::types::{BuiltinType, ComplexType, DerivationMethod, SimpleType};
85
86// ── public entry points ──────────────────────────────────────────────────────
87
88impl Schema {
89    pub fn validate_str(&self, xml: &str) -> Result<(), ValidationError> {
90        self.validate_str_opts(xml, ValidationOptions::default())
91    }
92
93    pub fn validate_bytes(&self, xml: &[u8]) -> Result<(), ValidationError> {
94        let s = std::str::from_utf8(xml).map_err(|e| ValidationError::single(
95            ValidationIssue {
96                message: format!("invalid UTF-8: {e}"),
97                line: None, column: None, path: String::new(),
98                kind: ValidationKind::Other,
99                expected: Vec::new(), value: None, type_name: None,
100            }
101        ))?;
102        self.validate_str(s)
103    }
104
105    pub fn validate_str_opts(&self, xml: &str, opts: ValidationOptions)
106        -> Result<(), ValidationError>
107    {
108        let mut v = Validator::new(self, xml, opts);
109        v.run();
110        if v.issues.is_empty() {
111            Ok(())
112        } else {
113            Err(ValidationError { issues: v.issues })
114        }
115    }
116
117    /// Validate an already-parsed [`Document`](sup_xml_tree::dom::Document)
118    /// directly, without going through XML re-serialisation.
119    ///
120    /// Equivalent to serialising the document and calling
121    /// [`validate_str`](Self::validate_str), but skips that
122    /// round-trip — useful when the document came from
123    /// [`parse_str`](crate::parse_str) or
124    /// [`process_xincludes`](crate::xinclude::process_xincludes)
125    /// and you'd otherwise burn a second parse pass.
126    ///
127    /// Diagnostic positioning trade-off: source byte offsets aren't
128    /// available from a `Document` (the original bytes are gone),
129    /// so reported issues carry `line: None` / `column: None`
130    /// instead of the precise positions you'd get from
131    /// `validate_str`.  Element paths (`/catalog/book[3]`) are
132    /// reported the same either way.
133    pub fn validate_doc(&self, doc: &sup_xml_tree::dom::Document)
134        -> std::result::Result<(), ValidationError>
135    {
136        self.validate_doc_opts(doc, ValidationOptions::default())
137    }
138
139    /// As [`validate_doc`](Self::validate_doc), but honours an
140    /// explicit [`ValidationOptions`].
141    pub fn validate_doc_opts(
142        &self, doc: &sup_xml_tree::dom::Document, opts: ValidationOptions,
143    ) -> std::result::Result<(), ValidationError> {
144        let source = DocumentEventSource::new(doc);
145        let mut v = Validator::new_with_source(self, source, opts);
146        v.run();
147        if v.issues.is_empty() {
148            Ok(())
149        } else {
150            Err(ValidationError { issues: v.issues })
151        }
152    }
153
154    /// As [`validate_doc`](Self::validate_doc), but also returns
155    /// [`PsviTypes`] — a per-node map of the schema type that governed
156    /// each element during validation.
157    ///
158    /// This is the post-schema-validation infoset entry point that
159    /// schema-aware XPath/XSLT processing builds on (typed atomization,
160    /// `instance of element(*, T)`, `data()`).  The returned table is
161    /// keyed by the addresses of `doc`'s nodes, so it is only valid
162    /// while `doc` is alive.  Validation errors are reported in the
163    /// `Result` exactly as `validate_doc` reports them; the annotations
164    /// are returned regardless, so a caller that wants best-effort
165    /// typing of a partially-valid document can ignore the `Result`.
166    pub fn validate_doc_typed(&self, doc: &sup_xml_tree::dom::Document)
167        -> (std::result::Result<(), ValidationError>, PsviTypes)
168    {
169        let source = DocumentEventSource::new(doc);
170        let mut v = Validator::new_with_source(self, source, ValidationOptions::default());
171        v.type_sink = Some(HashMap::default());
172        v.run();
173        let psvi = PsviTypes { by_node: v.type_sink.take().unwrap_or_default() };
174        let res = if v.issues.is_empty() {
175            Ok(())
176        } else {
177            Err(ValidationError { issues: v.issues })
178        };
179        (res, psvi)
180    }
181}
182
183/// Post-schema-validation type annotations produced by
184/// [`Schema::validate_doc_typed`].
185///
186/// Maps each validated element to the schema type that governed it,
187/// keyed by the source node's address.  Identity is by address: the
188/// document the table was built from must outlive every lookup (the
189/// borrow on `validate_doc_typed` ties the contents to that document at
190/// the call site).  Nodes validation never reached — e.g. content under
191/// a skip wildcard — simply have no entry and return `None`.
192#[derive(Default)]
193pub struct PsviTypes {
194    by_node: HashMap<usize, TypeRef>,
195}
196
197impl PsviTypes {
198    /// Governing type recorded for `node`, or `None` if validation
199    /// didn't assign one.
200    pub fn governing_type(&self, node: &sup_xml_tree::dom::Node) -> Option<&TypeRef> {
201        self.by_node.get(&(node as *const sup_xml_tree::dom::Node as usize))
202    }
203
204    /// True when no node received a type annotation.
205    pub fn is_empty(&self) -> bool { self.by_node.is_empty() }
206}
207
208// ── internal driver ──────────────────────────────────────────────────────────
209
210const XSI_NS: &str = "http://www.w3.org/2001/XMLSchema-instance";
211
212struct Validator<'s, 'x, E: XsdEventSource<'x>> {
213    schema:    &'s Schema,
214    reader:    E,
215    opts:      ValidationOptions,
216    /// Lifetime witness so the validator inherits 'x from the event
217    /// source's borrowed slices (attribute names/values, text
218    /// content) without an extra explicit parameter on every method.
219    _src:      std::marker::PhantomData<&'x str>,
220    issues:    Vec<ValidationIssue>,
221    /// Stack of in-progress element contexts.
222    stack:     Vec<ElementCtx<'s>>,
223    attr_buf:  Vec<Attr<'x>>,
224    /// Prefix → namespace bindings active at the current depth (for
225    /// resolving `xsi:type` and substitutionGroup QName values).
226    ns_stack:  Vec<HashMap<String, String>>,
227    /// Active identity-constraint scopes — pushed on entering an
228    /// element with `<xs:key>`/`<xs:keyref>`/`<xs:unique>` declarations,
229    /// popped (and validated) on leaving.
230    key_scopes: Vec<KeyScope>,
231    /// When `Some`, records each element's governing type keyed by
232    /// source-node address as validation proceeds — the post-schema-
233    /// validation infoset (see [`PsviTypes`]).  `None` on the ordinary
234    /// pass/fail path so it costs nothing.
235    type_sink: Option<HashMap<usize, TypeRef>>,
236}
237
238struct ElementCtx<'s> {
239    decl:      Arc<ElementDecl>,
240    /// Type used for validating *this* element — equal to `decl.type_def`
241    /// unless overridden by `xsi:type`.
242    type_def:  TypeRef,
243    /// Cached attribute values for identity-constraint field eval.
244    /// Populated when this element matched a constraint *or* when the
245    /// parent is collecting child snapshots for multi-step field paths.
246    /// Empty otherwise — zero overhead on the common path.
247    cached_attrs: Vec<(QName, String)>,
248    /// Index into `Validator::key_scopes` of the scope this element
249    /// declared (if any).  `usize::MAX` means "no scope declared here."
250    declared_scope: usize,
251    /// For each scope-and-constraint pair this element matched, the
252    /// (scope_idx, constraint_idx) pair to record at EndElement.
253    matched_constraints: Vec<(usize, usize)>,
254    /// How many levels of descendants below this element must be
255    /// captured into [`child_snapshots`].  Set to the max of:
256    ///   * own matched constraints' deepest field child-descent
257    ///     (see [`max_field_child_descent`]), and
258    ///   * `parent.collect_depth - 1` (so an ancestor's deep field
259    ///     path propagates the capture requirement down the stack).
260    /// Zero means "no descendant capture needed."
261    collect_depth: usize,
262    /// Snapshots of direct child elements — populated only when
263    /// `collect_depth > 0`.  Each snapshot itself carries the next
264    /// level of `children` when `collect_depth >= 2`, forming a
265    /// `collect_depth`-deep tree used by [`eval_field`].
266    child_snapshots: Vec<ChildSnapshot>,
267    /// Marker lifetime — held by `cached_attrs` references back to the
268    /// schema's QNames in some paths.
269    _phantom: std::marker::PhantomData<&'s ()>,
270    /// Position within the parent's content model.
271    cursor:    ContentCursor,
272    /// Buffer for accumulated text; flushed at EndElement.
273    text_buf:  String,
274    /// `xsi:nil="true"` on this element?
275    is_nil:    bool,
276    /// Full namespace-qualified name of this element.  Carried so that
277    /// identity-constraint selector matching can compare against
278    /// ancestor frames using the full namespace, not local-name only.
279    /// Cost is one extra `Option<Arc<str>>` per stack frame — typical
280    /// depth of 5-10 elements is negligible.
281    ///
282    /// Path locators built from `name.local` only (no `[n]` indices —
283    /// see `current_path`).
284    name:        QName,
285    /// Did this element push a new namespace scope?  Used by
286    /// [`pop_ns_scope_if`] at EndElement so we only pop when push
287    /// actually happened.
288    pushed_ns:   bool,
289    /// Track which non-prohibited attributes were seen, for required-
290    /// attribute checks at EndElement.
291    seen_attrs: Vec<QName>,
292    /// 1-based position among preceding-or-self siblings under the
293    /// parent that share this element's local-name.  Used by
294    /// [`current_path`] to disambiguate which `book` of fifty
295    /// triggered an error.  Set to 1 for the root.
296    sibling_index: u32,
297    /// Per-child-local-name counters, used to assign
298    /// [`sibling_index`] to each new child as it's pushed.  Keyed
299    /// by local-name only — XSD typing matches on expanded names,
300    /// but diagnostic paths read more naturally without prefixes.
301    ///
302    /// Stored as a flat Vec rather than a HashMap so an element with
303    /// few distinct child names (the common case — most parents have
304    /// 1–10 unique children) pays no allocation overhead beyond the
305    /// Vec itself.  The Arc<str> key clones from the child's
306    /// already-interned `QName::local` with no heap allocation
307    /// (just an atomic refcount bump).  HashMap profiled at ~10%
308    /// of validate time on customer1.xml; Vec form drops that to
309    /// near-zero.
310    child_counters: Vec<(Arc<str>, u32)>,
311    /// Byte offset of this element's start tag within the source
312    /// buffer.  Snapshot from `XmlReader::src_offset()` just before
313    /// the StartElement event was consumed (so it points at the
314    /// element's `<`, not past its `>`).  Used by [`Validator::report`]
315    /// and [`Validator::report_at`] to translate to (line, column) on
316    /// demand via [`XmlReader::line_col_at`].
317    start_offset: u32,
318}
319
320/// Cursor into a parent's content model.  Three flavours:
321///
322/// * `None` — element has Empty or Simple content.
323/// * `Dfa` — sequence/choice/element/wildcard models compiled to a
324///   deterministic finite automaton at schema-compile time.  O(1) per
325///   child element via [`Dfa::step`].
326/// * `Group` (kept for `xs:all` only) — the existing particle-walk
327///   matcher with bitset tracking, since `all` doesn't compile to a
328///   DFA without exponential blowup.
329enum ContentCursor {
330    None,
331    /// DFA-driven matching (sequence/choice/element/wildcard).
332    Dfa {
333        dfa:   Arc<super::dfa::Dfa>,
334        state: super::dfa::StateId,
335    },
336    /// All-group fallback — each particle matches at most maxOccurs
337    /// times in any order.
338    Group {
339        kind:        GroupKind,
340        particles:   Arc<[Particle]>,
341        idx:         usize,
342        cur_count:   u32,
343        all_seen:    HashMap<usize, u32>,
344        /// `minOccurs` of the outer particle wrapping this group.
345        /// When the group accepted zero children and `outer_min == 0`,
346        /// per-child `minOccurs` checks are skipped — the user said
347        /// "this whole all-group is optional."
348        outer_min:   u32,
349    },
350}
351
352/// One identity-constraint scope — pushed when we enter an element
353/// that declares any constraints, popped (and validated) on exit.
354///
355/// Holds the declaring [`ElementDecl`] via `Arc::clone` so we don't
356/// fight the borrow checker over the constraints' lifetime.  The
357/// constraints are reachable via `scope.decl.identity`.
358struct KeyScope {
359    /// `self.stack.len()` at the moment this scope was pushed.  Used
360    /// to compute relative paths when matching selectors.
361    declaring_depth: usize,
362    /// Byte offset of the declaring element's start tag — used to
363    /// attribute uniqueness / keyref errors to the schema-defining
364    /// element rather than wherever the scanner happens to be when
365    /// finalize fires.
366    declaring_offset: u32,
367    decl: Arc<ElementDecl>,
368    /// `decl.identity[i]` → list of field tuples collected so far.
369    /// `None` represents a missing field (legal for `xs:unique`,
370    /// rejected for `xs:key`).
371    collected: Vec<Vec<KeyTuple>>,
372}
373
374/// One per matched selector — a tuple of field values (one slot per
375/// `<xs:field>`).
376type KeyTuple = Vec<Option<String>>;
377
378/// Snapshot of a descendant element's identity-relevant data —
379/// captured at EndElement time when an ancestor has multi-step field
380/// paths to evaluate.  Recursive: `children` holds snapshots of this
381/// element's own children when an ancestor's field path needs to
382/// descend further.  The recursion depth is bounded by the deepest
383/// child-step count across all in-scope field paths, computed once at
384/// element-push time via [`max_field_child_descent`].
385#[derive(Debug)]
386#[derive(Clone)]
387pub(super) struct ChildSnapshot {
388    pub(super) name:     QName,
389    pub(super) attrs:    Vec<(QName, String)>,
390    pub(super) text:     String,
391    pub(super) children: Vec<ChildSnapshot>,
392}
393
394/// Deepest child-step count across all alternatives of a field path.
395/// `@attr` → 0, `child` → 1, `child/@attr` → 1, `a/b` → 2, `a/b/@c` → 2.
396/// Determines how many levels of child snapshots the matched element
397/// must capture to evaluate this field.
398fn max_field_child_descent(fp: &FieldPath) -> usize {
399    fp.paths.iter()
400        .map(|p| p.steps.iter().filter(|s| matches!(s, PathStep::Child(_))).count())
401        .max()
402        .unwrap_or(0)
403}
404
405/// Increment the child-name counter for `local` on a parent
406/// element's frame and return the new value (the 1-based sibling
407/// index used in diagnostic paths like `/catalog/book[3]`).  Linear
408/// scan over the parent's small counter table: real-world XSDs
409/// have <20 distinct child names per type, so linear is cheaper
410/// than a HashMap (no hashing, no String allocation per lookup —
411/// the key is a cheap Arc<str> clone of the already-interned
412/// `QName::local`).
413fn bump_child_counter(counters: &mut Vec<(Arc<str>, u32)>, local: &Arc<str>) -> u32 {
414    for (name, n) in counters.iter_mut() {
415        if name.as_ref() == local.as_ref() {
416            *n += 1;
417            return *n;
418        }
419    }
420    counters.push((local.clone(), 1));
421    1
422}
423
424/// Capture an element's identity-relevant attributes — non-namespace,
425/// non-`xsi:*` attrs with their parsed QNames.  Used both for the
426/// matched element itself (for `@attr` field eval) and for snapshots
427/// pushed to a parent that's collecting children for multi-step paths.
428fn snapshot_attrs<F>(attrs: &[Attr], mut to_qname: F) -> Vec<(QName, String)>
429where
430    F: FnMut(&str) -> QName,
431{
432    attrs.iter()
433        .filter(|a| {
434            let n = a.name;
435            !n.starts_with("xmlns") && !n.starts_with("xsi:")
436        })
437        .map(|a| (to_qname(a.name), a.value.to_string()))
438        .collect()
439}
440
441impl<'s, 'x> Validator<'s, 'x, XmlReader<'x>> {
442    /// Streaming-validator constructor — pulls events directly off the
443    /// `XmlReader` for the bytes-in path used by `validate_str`.
444    fn new(schema: &'s Schema, xml: &'x str, opts: ValidationOptions) -> Self {
445        Self::new_with_source(schema, XmlReader::from_str(xml), opts)
446    }
447}
448
449impl<'s, 'x, E: XsdEventSource<'x>> Validator<'s, 'x, E> {
450    /// Source-generic constructor — used by both `new` (streaming
451    /// reader) and `validate_doc` (DOM walker).
452    fn new_with_source(schema: &'s Schema, source: E, opts: ValidationOptions) -> Self {
453        Self {
454            schema,
455            reader: source,
456            opts,
457            issues: Vec::new(),
458            stack: Vec::new(),
459            attr_buf: Vec::new(),
460            ns_stack: vec![HashMap::default()],
461            key_scopes: Vec::new(),
462            type_sink: None,
463            _src: std::marker::PhantomData,
464        }
465    }
466
467    /// Record the governing `type_def` for the element currently being
468    /// validated, keyed by its source-node address.  No-op unless type
469    /// collection is enabled (`type_sink` is `Some`).
470    fn record_governing_type(&mut self, type_def: &TypeRef) {
471        let Some(key) = self.reader.current_node_key() else { return };
472        if let Some(sink) = self.type_sink.as_mut() {
473            sink.insert(key, type_def.clone());
474        }
475    }
476
477    // ── issue reporting ─────────────────────────────────────────────────
478
479    fn report(&mut self, kind: ValidationKind, message: impl Into<String>) -> bool {
480        // Default offset: the element currently being validated (top of
481        // stack).  Falls back to the reader's current position when the
482        // stack is empty (root-level / pre-element issues).
483        let offset = self.stack.last()
484            .map(|c| c.start_offset as usize)
485            .unwrap_or_else(|| self.reader.src_offset());
486        self.report_at(kind, message, offset)
487    }
488
489    /// Report an issue at a specific source byte offset.  Used when the
490    /// issue's natural anchor isn't the top-of-stack element — e.g.
491    /// "missing required element" detected at EndElement (use the
492    /// just-ended element's offset, since by then it's already popped),
493    /// or "unexpected element" detected before push (use the offset of
494    /// the bad element itself, not its parent).
495    fn report_at(&mut self, kind: ValidationKind, message: impl Into<String>, offset: usize) -> bool {
496        let (line, col) = self.reader.line_col_at(offset);
497        let path = self.current_path();
498        let issue = ValidationIssue {
499            message: message.into(),
500            line: Some(line), column: Some(col),
501            path,
502            kind,
503            expected: Vec::new(), value: None, type_name: None,
504        };
505        self.issues.push(issue);
506        if self.opts.fail_fast || self.issues.len() >= self.opts.max_issues {
507            return true; // signal caller to stop
508        }
509        false
510    }
511
512    fn current_path(&self) -> String {
513        // Built only when an issue is reported — the success path skips
514        // every per-element String allocation that previously happened
515        // here (see `handle_start`).
516        //
517        // Each step is emitted as `/name[N]` when N > 1, plain `/name`
518        // otherwise — readable for the common no-repeat case, but
519        // points at the right one of N when sibling elements share
520        // a name.  Saxon would always emit `[1]`; we drop it for the
521        // single-occurrence case so simple paths don't read as
522        // `/a[1]/b[1]/c[1]`.
523        if self.stack.is_empty() { return String::new(); }
524        let mut s = String::new();
525        for ctx in &self.stack {
526            s.push('/');
527            s.push_str(&ctx.name.local);
528            if ctx.sibling_index >= 2 {
529                use std::fmt::Write;
530                let _ = write!(s, "[{}]", ctx.sibling_index);
531            }
532        }
533        s
534    }
535
536    // ── namespace bookkeeping ────────────────────────────────────────────
537
538    /// Push a new namespace scope only when this element actually
539    /// declares one (`xmlns=` or `xmlns:*`).  For the typical instance
540    /// where namespaces are declared once at the root and inherited
541    /// thereafter, every other element pays nothing here — we previously
542    /// cloned the parent's HashMap per element.
543    ///
544    /// Returns `true` when a scope was pushed, `false` otherwise.  The
545    /// caller stashes the bool on the [`ElementCtx`] so the matching
546    /// `pop_ns_scope_if` knows whether to pop.
547    fn push_ns_scope(&mut self, attrs: &[Attr<'x>]) -> bool {
548        let has_ns = attrs.iter().any(|a| {
549            let n = a.name;
550            n == "xmlns" || n.starts_with("xmlns:")
551        });
552        if !has_ns { return false; }
553
554        let mut new = self.ns_stack.last().cloned().unwrap_or_default();
555        for a in attrs {
556            let n = a.name;
557            if n == "xmlns" {
558                new.insert(String::new(), a.value.to_string());
559            } else if let Some(prefix) = n.strip_prefix("xmlns:") {
560                new.insert(prefix.to_string(), a.value.to_string());
561            }
562        }
563        self.ns_stack.push(new);
564        true
565    }
566
567    fn pop_ns_scope_if(&mut self, pushed: bool) {
568        if pushed { self.ns_stack.pop(); }
569    }
570
571    fn resolve_prefix(&self, prefix: &str) -> Option<&str> {
572        for scope in self.ns_stack.iter().rev() {
573            if let Some(uri) = scope.get(prefix) {
574                return Some(uri.as_str());
575            }
576        }
577        None
578    }
579
580    /// Parse an element name from instance source.  Per XML
581    /// Namespaces, an unprefixed element name takes the default
582    /// namespace (xmlns="…") if one is bound, else lives in no
583    /// namespace.  The schema's `targetNamespace` is NOT used as a
584    /// fallback — that's a job for the schema's
585    /// `elementFormDefault`, applied at schema-compile time.
586    fn parse_element_qname(&self, raw: &str) -> QName {
587        match raw.split_once(':') {
588            Some((p, local)) => QName {
589                namespace: self.resolve_prefix(p).map(Arc::from),
590                local:     Arc::from(local),
591            },
592            None => QName {
593                namespace: self.resolve_prefix("").map(Arc::from),
594                local:     Arc::from(raw),
595            },
596        }
597    }
598
599    /// Parse an attribute name from instance source.  Per XML
600    /// Namespaces, an unprefixed attribute is ALWAYS in no namespace
601    /// (attributes do not pick up the default xmlns).
602    fn parse_attribute_qname(&self, raw: &str) -> QName {
603        match raw.split_once(':') {
604            Some((p, local)) => QName {
605                namespace: self.resolve_prefix(p).map(Arc::from),
606                local:     Arc::from(local),
607            },
608            None => QName {
609                namespace: None,
610                local:     Arc::from(raw),
611            },
612        }
613    }
614
615    /// Parse a QName-valued attribute *value* (e.g. xsi:type,
616    /// substitutionGroup).  Same namespace rules as element names:
617    /// default xmlns or no namespace.
618    fn parse_qname_value(&self, raw: &str) -> QName {
619        self.parse_element_qname(raw)
620    }
621
622    // ── event loop ──────────────────────────────────────────────────────
623
624    fn run(&mut self) {
625        loop {
626            let ev = match self.reader.next_into(&mut self.attr_buf) {
627                Ok(ev) => ev,
628                Err(e) => {
629                    self.issues.push(ValidationIssue {
630                        message: format!("XML parse error: {e}"),
631                        line: e.line, column: e.column, path: self.current_path(),
632                        kind: ValidationKind::Other,
633                        expected: Vec::new(), value: None, type_name: None,
634                    });
635                    return;
636                }
637            };
638            match ev {
639                // EntityRef events only appear under
640                // `resolve_entities=false`, which isn't a mode XSD
641                // validation supports — schema-style assertions about
642                // typed text/element content depend on the values
643                // being post-expansion.  Skip them.
644                EventInto::Comment(_)
645                | EventInto::Pi { .. }
646                | EventInto::EntityRef { .. } => continue,
647                EventInto::StartElement { name } => {
648                    // Snapshot the offset of the just-emitted start
649                    // tag's `<` for downstream line/column attribution.
650                    // `last_start_offset()` is updated by the reader
651                    // inside its start-tag dispatch; falling back to
652                    // `src_offset()` covers the entity-stream case
653                    // where the source offset is meaningless (it'll
654                    // point past the closing `>` — best we can do).
655                    let event_offset = self.reader.last_start_offset()
656                        .unwrap_or_else(|| self.reader.src_offset());
657                    // Move `attr_buf` out of `self` so `handle_start`
658                    // can call other `&mut self` methods while still
659                    // reading the parsed attributes.  `mem::take` is
660                    // zero-alloc — the buffer's heap allocation moves
661                    // through the local var and back into `self`,
662                    // preserving its capacity across iterations.
663                    // The fresh `Vec::new()` left behind has zero
664                    // capacity and zero allocation; the next
665                    // `next_into` call will populate it (and the
666                    // reader's eventual reuse of the now-emptied
667                    // original buffer happens on the next start tag).
668                    let attrs = std::mem::take(&mut self.attr_buf);
669                    let bail = self.handle_start(name, &attrs, event_offset);
670                    // Put the (capacity-preserving) buffer back so the
671                    // reader can fill it on the next call without
672                    // reallocating.
673                    self.attr_buf = attrs;
674                    if bail { return; }
675                }
676                EventInto::EndElement { .. } => {
677                    if self.handle_end() { return; }
678                }
679                EventInto::Text(t) | EventInto::CData(t) => {
680                    if let Some(ctx) = self.stack.last_mut() {
681                        ctx.text_buf.push_str(&t);
682                    }
683                }
684                EventInto::Eof => {
685                    if !self.stack.is_empty() {
686                        self.report(ValidationKind::Other,
687                            "unexpected EOF (unclosed elements)");
688                    }
689                    return;
690                }
691            }
692        }
693    }
694
695    fn handle_start(
696        &mut self,
697        name: std::borrow::Cow<'x, str>,
698        attrs: &[Attr<'x>],
699        event_offset: usize,
700    ) -> bool {
701        let pushed_ns = self.push_ns_scope(attrs);
702        let qn = self.parse_element_qname(&name);
703
704        // Locate the element decl: from the parent's content model on
705        // the stack, or from the schema for the root.
706        let decl = if let Some(parent) = self.stack.last_mut() {
707            // Match against parent's cursor.
708            let m = match_in_cursor(&qn, &mut parent.cursor, self.schema);
709            match m {
710                MatchOutcome::Element(decl) => decl,
711                MatchOutcome::Wildcard(wc) => {
712                    // Wildcards: validate per process_contents.
713                    return self.handle_wildcard_match(&qn, attrs, wc, pushed_ns, event_offset);
714                }
715                MatchOutcome::None => {
716                    // Attribute the "unexpected" error to the offending
717                    // element itself — not its parent.  This is our
718                    // native wording; the compat shim translates it to
719                    // libxml2's `xmlschemas.c` phrasing for ABI consumers
720                    // (see `libxml2_validation_message`), using the
721                    // expected-element names the content model wanted.
722                    let expected = expected_element_names(&parent.cursor);
723                    let stop = self.report_at(ValidationKind::UnexpectedElement,
724                        format!("unexpected element <{qn}>"), event_offset);
725                    if let Some(last) = self.issues.last_mut() {
726                        last.expected = expected;
727                    }
728                    if stop { return true; }
729                    // Skip the body to keep parsing.
730                    return self.skip_body_into_issues(pushed_ns);
731                }
732            }
733        } else {
734            // Root element.  XSD 1.0 §5.2 permits "type-only" validation
735            // when a root element isn't globally declared but carries
736            // `xsi:type=` — assess against that type with an
737            // ad-hoc declaration.  Common in test suites where a small
738            // schema only defines named types.
739            match self.schema.element(&qn) {
740                Some(d) => d.clone(),
741                None => {
742                    if let Some(xsi_type) = find_xsi_type(&attrs) {
743                        let type_qn = self.parse_qname_value(xsi_type);
744                        if let Some(tr) = self.lookup_type_by_qname(&type_qn) {
745                            Arc::new(ElementDecl {
746                                name:               qn.clone(),
747                                type_def:           tr,
748                                nillable:           true,
749                                default:            None,
750                                fixed:              None,
751                                abstract_:          false,
752                                substitution_group: None,
753                                identity:           Vec::new(),
754                                block:              super::schema::BlockSet::empty(),
755                                final_:             super::schema::BlockSet::empty(),
756                            })
757                        } else {
758                            self.report_at(ValidationKind::UnexpectedElement,
759                                format!("root element <{qn}>: xsi:type {type_qn} not declared in schema"),
760                                event_offset);
761                            return self.skip_body_into_issues(pushed_ns);
762                        }
763                    } else {
764                        self.report_at(ValidationKind::UnexpectedElement,
765                            format!("root element <{qn}> not declared in schema"),
766                            event_offset);
767                        return self.skip_body_into_issues(pushed_ns);
768                    }
769                }
770            }
771        };
772
773        // Resolve declared type → real type.
774        let mut type_def = self.resolve_type(&decl.type_def);
775
776        // xsi:type override — per XSD 1.0 §3.4.6, the override must
777        // derive from the declared type (transitively, by restriction
778        // or extension) AND that derivation must not be blocked by
779        // the element's `block=` or the declared type's `final=`.
780        //
781        // Tracked here so the abstract-element check below can know
782        // whether a valid concrete override was applied.  Per
783        // cvc-elt-2, an abstract element declaration IS allowed in
784        // an instance when accompanied by an `xsi:type` that
785        // resolves to a non-abstract type derived from the declared
786        // type — that's how abstract heads with xsi:type-based
787        // polymorphism (SOAP, CloudEvents, audit logs) work.
788        let mut xsi_type_override_applied = false;
789        if let Some(xsi_type) = find_xsi_type(&attrs) {
790            let override_qn = self.parse_qname_value(xsi_type);
791            if let Some(t) = self.lookup_type_by_qname(&override_qn) {
792                let declared = type_def.clone();
793                match self.derivation_methods_from(&t, &declared) {
794                    None => {
795                        self.report_at(ValidationKind::TypeMismatch,
796                            format!("xsi:type {override_qn} does not derive from declared type"),
797                            event_offset);
798                        // Keep declared type — best chance of producing
799                        // useful downstream errors.
800                    }
801                    Some(methods) => {
802                        // Block= on the element: any method in `methods`
803                        // that's also in decl.block forbids the
804                        // substitution.  Methods is empty for identity
805                        // (T == D), so identity is never blocked.
806                        // The declared type's own `block` also
807                        // participates (cvc-type-2.x): a complexType
808                        // declared with block="extension" forbids
809                        // any xsi:type whose derivation chain includes
810                        // extension.
811                        let type_block = if let TypeRef::Complex(declared_ct) = &declared {
812                            declared_ct.block
813                        } else { BlockSet::empty() };
814                        let element_blocked = (decl.block | type_block) & methods;
815                        if !element_blocked.is_empty() {
816                            self.report_at(ValidationKind::TypeMismatch,
817                                format!(
818                                    "xsi:type {override_qn} blocked by block={}",
819                                    format_block_set(element_blocked),
820                                ),
821                                event_offset);
822                            // Don't apply the override.
823                        } else {
824                            // Final= on the declared type's complex
825                            // form: any method in `methods` that's in
826                            // declared_ct.final_ forbids the
827                            // derivation.  (Simple types: skip — we
828                            // don't track final on built-ins.)
829                            let final_blocked = if let TypeRef::Complex(declared_ct) = &declared {
830                                declared_ct.final_ & methods
831                            } else {
832                                BlockSet::empty()
833                            };
834                            if !final_blocked.is_empty() {
835                                self.report_at(ValidationKind::TypeMismatch,
836                                    format!(
837                                        "xsi:type {override_qn} blocked by base type final={}",
838                                        format_block_set(final_blocked),
839                                    ),
840                                    event_offset);
841                                // Don't apply the override.
842                            } else {
843                                // cvc-type: the type the element ends
844                                // up assessed against must itself not
845                                // be abstract.  An abstract xsi:type
846                                // target doesn't rescue an abstract
847                                // element decl from cvc-elt-2 below.
848                                let target_is_abstract = matches!(&t,
849                                    TypeRef::Complex(ct) if ct.abstract_);
850                                if !target_is_abstract {
851                                    type_def = t;
852                                    xsi_type_override_applied = true;
853                                } else {
854                                    self.report_at(ValidationKind::TypeMismatch,
855                                        format!("xsi:type {override_qn} is abstract"),
856                                        event_offset);
857                                }
858                            }
859                        }
860                    }
861                }
862            } else {
863                self.report_at(ValidationKind::TypeMismatch,
864                    format!("xsi:type {override_qn:?} not declared in schema"),
865                    event_offset);
866            }
867        }
868
869        // XSD 1.0 §3.3.4 / cvc-elt-2: an element declared `abstract="true"`
870        // must not appear in instance documents — only declarations that
871        // substitute for it via `substitutionGroup=` can.  Match dispatch
872        // resolves substitutes before we get here, so reaching this point
873        // with `decl.abstract_` true means the literal abstract name was
874        // used.  The exception (handled above) is when an `xsi:type`
875        // attribute points to a non-abstract concrete subtype — then
876        // the abstract head is acting as a typed slot, not as the
877        // element being instantiated.
878        if decl.abstract_ && !xsi_type_override_applied {
879            self.report_at(ValidationKind::UnexpectedElement,
880                format!("element <{qn}> is abstract and cannot appear in instances"),
881                event_offset);
882            return self.skip_body_into_issues(pushed_ns);
883        }
884
885        // Parse xsi:nil as xs:boolean (XSD 1.0 §3.2.2):
886        //   "true"  | "1" → nil enabled
887        //   "false" | "0" → nil disabled
888        //   anything else → lexical error; treat the element as NOT
889        //   nil so its content still gets validated and any further
890        //   issues surface in the same pass.
891        let is_nil = match find_xsi_nil(&attrs) {
892            None => false,
893            Some(raw) => match parse_xsi_nil(raw) {
894                XsiNilParse::True  => true,
895                XsiNilParse::False => false,
896                XsiNilParse::Invalid(bad) => {
897                    self.report_at(ValidationKind::TypeMismatch,
898                        format!("xsi:nil value {bad:?} is not a valid xs:boolean \
899                                 (expected \"true\", \"false\", \"1\", or \"0\")"),
900                        event_offset);
901                    false
902                }
903            },
904        };
905        if is_nil && !decl.nillable {
906            // Element not yet pushed — anchor at its event_offset.
907            self.report_at(ValidationKind::NillableViolation,
908                format!("xsi:nil on non-nillable element <{qn}>"),
909                event_offset);
910        }
911
912        // Validate attributes.  All issues raised here are anchored at
913        // `event_offset` (the start of the current element) — the
914        // element hasn't been pushed onto the stack yet, so the
915        // default `report()` would attribute to the parent.
916        let seen_attrs = self.validate_attrs_against_type(&type_def, &attrs, event_offset);
917
918        // Build content cursor.  `xsi:nil` always emits None
919        // (no children allowed); otherwise use the type's compiled
920        // matcher (DFA when available, all-group fallback otherwise).
921        let cursor = if is_nil { ContentCursor::None } else { build_cursor(&type_def) };
922
923        // ── identity-constraint bookkeeping ────────────────────────
924        //
925        // Two things to do per element:
926        //   1.  If THIS element's decl carries constraints, push a new
927        //       scope.
928        //   2.  For each ALREADY-active scope, check whether THIS
929        //       element matches any of its constraints' selectors —
930        //       if so, mark for field eval at EndElement.
931        let declared_scope = if !decl.identity.is_empty() {
932            let collected = vec![Vec::new(); decl.identity.len()];
933            self.key_scopes.push(KeyScope {
934                declaring_depth: self.stack.len(),
935                declaring_offset: event_offset as u32,
936                decl: decl.clone(),
937                collected,
938            });
939            self.key_scopes.len() - 1
940        } else {
941            usize::MAX
942        };
943
944        // Parent (about to be replaced as `last`) may want a snapshot of
945        // this child element for multi-step field eval.  Check before
946        // we push so we know whether to cache attrs even if we
947        // ourselves matched no constraint.
948        let parent_collect_depth = self.stack.last()
949            .map(|p| p.collect_depth).unwrap_or(0);
950        let parent_collects = parent_collect_depth > 0;
951        // XSD 1.1 `xs:assert` evaluation needs the full subtree of
952        // every element whose complex type carries assertions, plus
953        // that element's own attributes.  Detect the case so we can
954        // force-snapshot.  Cheap: just a vec-empty check on the
955        // resolved type definition.
956        let has_assertions = matches!(
957            &type_def, TypeRef::Complex(ct) if !ct.assertions.is_empty()
958        );
959        let need_capture = parent_collects || has_assertions;
960        let (matched, mut cached_attrs) = self.check_active_scopes(&qn, &attrs);
961        if cached_attrs.is_empty() && need_capture {
962            cached_attrs = snapshot_attrs(&attrs, |s| self.parse_attribute_qname(s));
963        }
964        // Own descent need: the deepest child-step count across all
965        // matched constraints' fields.  When this element has
966        // assertions, treat the subtree as unbounded — XPath in a
967        // test expression may walk arbitrarily deep.
968        let own_collect_depth = if has_assertions {
969            usize::MAX
970        } else {
971            matched.iter().map(|(si, ci)| {
972                self.key_scopes[*si].decl.identity[*ci].fields.iter()
973                    .map(max_field_child_descent)
974                    .max().unwrap_or(0)
975            }).max().unwrap_or(0)
976        };
977        // Ancestor propagation: if parent is collecting to depth N, we
978        // (as one of its captured children) must collect N-1 below
979        // ourselves so the snapshot tree reaches the required depth.
980        let collect_depth = own_collect_depth
981            .max(parent_collect_depth.saturating_sub(1));
982
983        // Sibling index: per-name counter on the parent's frame.
984        // The root has no parent and gets index 1.
985        let sibling_index = match self.stack.last_mut() {
986            Some(parent) => bump_child_counter(&mut parent.child_counters, &qn.local),
987            None => 1,
988        };
989
990        self.record_governing_type(&type_def);
991        self.stack.push(ElementCtx {
992            decl,
993            type_def,
994            cursor,
995            text_buf: String::new(),
996            is_nil,
997            name: qn,
998            pushed_ns,
999            seen_attrs,
1000            cached_attrs,
1001            declared_scope,
1002            matched_constraints: matched,
1003            collect_depth,
1004            child_snapshots: Vec::new(),
1005            start_offset: event_offset as u32,
1006            _phantom: std::marker::PhantomData,
1007            sibling_index,
1008            child_counters: Vec::new(),
1009        });
1010        false
1011    }
1012
1013    /// For each active key scope, check whether `qn` matches any of the
1014    /// scope's constraints' selectors *at the current relative depth*.
1015    /// Returns the list of matches (scope_idx, constraint_idx) and the
1016    /// attribute snapshot (only populated when there's at least one
1017    /// match — otherwise we don't pay for it).
1018    fn check_active_scopes(
1019        &self,
1020        qn: &QName,
1021        attrs: &[Attr<'x>],
1022    ) -> (Vec<(usize, usize)>, Vec<(QName, String)>) {
1023        if self.key_scopes.is_empty() {
1024            return (Vec::new(), Vec::new());
1025        }
1026        let depth = self.stack.len();
1027        let mut matched = Vec::new();
1028        for (si, scope) in self.key_scopes.iter().enumerate() {
1029            let rel = depth.saturating_sub(scope.declaring_depth);
1030            // Walk back through the path from this element up to the
1031            // declaring element to compare against each selector.
1032            for (ci, c) in scope.decl.identity.iter().enumerate() {
1033                if selector_matches(&c.selector, &self.stack, qn, rel) {
1034                    matched.push((si, ci));
1035                }
1036            }
1037        }
1038        let cached_attrs = if matched.is_empty() {
1039            Vec::new()
1040        } else {
1041            snapshot_attrs(attrs, |s| self.parse_attribute_qname(s))
1042        };
1043        (matched, cached_attrs)
1044    }
1045
1046    /// Evaluate every XSD 1.1 `<xs:assertion>` facet on `st` against
1047    /// the lexical `value`, with `$value` bound to it.  Reports each
1048    /// failure as a separate validation issue at `ctx_off`.
1049    fn run_simple_type_assertions(
1050        &mut self,
1051        st: &super::types::SimpleType,
1052        value: &str,
1053        ctx_off: usize,
1054    ) {
1055        if st.assertions.is_empty() { return; }
1056        for a in &st.assertions {
1057            use super::assertion::{eval_simple_assertion, AssertOutcome};
1058            match eval_simple_assertion(a, value) {
1059                AssertOutcome::Pass => {}
1060                AssertOutcome::Fail => {
1061                    self.report_at(ValidationKind::AssertionViolation,
1062                        format!("assertion failed: {}", a.test), ctx_off);
1063                }
1064                AssertOutcome::Unevaluable(_) => {}
1065            }
1066        }
1067    }
1068
1069    fn handle_end(&mut self) -> bool {
1070        let mut ctx = self.stack.pop().expect("EndElement with empty stack");
1071        self.pop_ns_scope_if(ctx.pushed_ns);
1072
1073        // ── identity-constraint field eval ──────────────────────────
1074        // Evaluate fields for every constraint that matched this
1075        // element at start-time, recording into the matched scope.
1076        // Each Single value is canonicalised through its declared
1077        // (or xsi:type-overridden) simple type so XSD 1.0 §3.11.4
1078        // cvc-identity-constraint.4.2.2 value-space equality holds:
1079        // boolean("1") and decimal("1") don't collide just because
1080        // the lexical form happens to match, and decimal("1") and
1081        // decimal("1.0") DO match because they're the same value.
1082        let element_simple_type =
1083            self.field_dot_simple_type(&ctx.type_def);
1084        for (si, ci) in &ctx.matched_constraints {
1085            let constraint = self.key_scopes[*si].decl.identity[*ci].clone();
1086            let fields = constraint.fields.clone();
1087            // Canonicalising the field value through its simple type
1088            // closes XSD 1.0 §3.11.4.4.2.2's value-space equality
1089            // gap (boolean("1") ≠ decimal("1"), decimal("1") =
1090            // decimal("1.0")) — but only the `.` path resolves a type
1091            // today.  For canonicalisation to be sound across a
1092            // `<xs:key>` and its referring `<xs:keyref>`, BOTH sides
1093            // must canonicalise (otherwise canonical "decimal:5" would
1094            // never match raw "5.0"); fall back to raw lex when the
1095            // peers can't share the canonical form.
1096            let canonicalisable =
1097                fields.iter().all(field_path_is_dot)
1098                    && self.constraint_peers_all_dot(*si, *ci);
1099            let mut tuple: KeyTuple = Vec::with_capacity(fields.len());
1100            let mut ambiguous = false;
1101            for fp in fields {
1102                match eval_field(
1103                    &fp, &ctx.cached_attrs, &ctx.text_buf, &ctx.child_snapshots,
1104                ) {
1105                    FieldEval::Single(v) => {
1106                        let canon = if canonicalisable {
1107                            canonical_field_key(&v, element_simple_type.as_ref())
1108                        } else {
1109                            v
1110                        };
1111                        tuple.push(Some(canon));
1112                    }
1113                    FieldEval::Missing   => tuple.push(None),
1114                    FieldEval::Ambiguous => { ambiguous = true; tuple.push(None); }
1115                }
1116            }
1117            if ambiguous {
1118                // XSD §3.11.6: a field xpath must select at most one
1119                // node.  When multiple nodes match, the constraint
1120                // cannot produce a well-defined key value.
1121                self.report(ValidationKind::Other, format!(
1122                    "<xs:{} {:?}>: a field xpath selected more than one node",
1123                    match constraint.kind {
1124                        ConstraintKind::Key    => "key",
1125                        ConstraintKind::Unique => "unique",
1126                        ConstraintKind::KeyRef => "keyref",
1127                    },
1128                    constraint.name.local,
1129                ));
1130            }
1131            self.key_scopes[*si].collected[*ci].push(tuple);
1132        }
1133        // If we declared a scope here, it ends now: validate uniqueness
1134        // and resolve keyrefs.
1135        if ctx.declared_scope != usize::MAX {
1136            let scope = self.key_scopes.pop().expect("scope stack out of sync");
1137            self.finalize_key_scope(&scope);
1138        }
1139        // Push a snapshot to the parent for multi-step field eval, when
1140        // applicable.  We pass ownership of our own child_snapshots so
1141        // the parent has the full recursive tree for deep field paths.
1142        if let Some(parent) = self.stack.last_mut() {
1143            if parent.collect_depth > 0 {
1144                parent.child_snapshots.push(ChildSnapshot {
1145                    name:     ctx.name.clone(),
1146                    attrs:    ctx.cached_attrs.clone(),
1147                    text:     ctx.text_buf.clone(),
1148                    children: std::mem::take(&mut ctx.child_snapshots),
1149                });
1150            }
1151        }
1152
1153        // Cursor / content checks below all describe ctx (the
1154        // just-ended element) — but ctx is already popped so the
1155        // default report() would attribute to the parent.  Anchor each
1156        // issue at ctx.start_offset instead.
1157        let ctx_off = ctx.start_offset as usize;
1158
1159        // 1. Final occurrence checks for the cursor we just finished.
1160        if let ContentCursor::Dfa { dfa, state } = &ctx.cursor {
1161            if !dfa.is_accept(*state) {
1162                // Surface the element names this state could transition
1163                // on — those are exactly the "expected next" elements
1164                // the schema author meant to require.
1165                let expected: Vec<String> = dfa.states[*state as usize]
1166                    .on_element.iter()
1167                    .map(|t| t.name.local.to_string())
1168                    .collect();
1169                let msg = if expected.is_empty() {
1170                    "element content does not match the schema (no valid continuation)".to_string()
1171                } else if expected.len() == 1 {
1172                    format!("missing required element <{}>", expected[0])
1173                } else {
1174                    format!("missing required element (one of: {})", expected.join(", "))
1175                };
1176                self.report_at(ValidationKind::MissingRequiredElement, msg, ctx_off);
1177            }
1178        }
1179        if let ContentCursor::Group { kind, particles, idx, cur_count, all_seen, outer_min, .. } = &ctx.cursor {
1180            match kind {
1181                GroupKind::Sequence => {
1182                    // Verify any unfilled particles meet their min_occurs == 0.
1183                    if *idx < particles.len() {
1184                        // Check current first.
1185                        let p = &particles[*idx];
1186                        if *cur_count < p.min_occurs {
1187                            let _ = self.handle_min_occurs_violation(p, ctx_off);
1188                        }
1189                        // Then any tail particles.
1190                        for p in &particles[*idx + 1..] {
1191                            if p.min_occurs > 0 {
1192                                let _ = self.handle_min_occurs_violation(p, ctx_off);
1193                            }
1194                        }
1195                    }
1196                }
1197                GroupKind::Choice => {
1198                    // If no choice was ever taken, the choice itself has
1199                    // an occurrence requirement of 1 implicitly.
1200                    if *cur_count == 0 && !particles.is_empty() {
1201                        self.report_at(ValidationKind::MissingRequiredElement,
1202                            "no branch of <xs:choice> matched", ctx_off);
1203                    }
1204                }
1205                GroupKind::All => {
1206                    // An all-group with `minOccurs="0"` is allowed to
1207                    // not occur at all — when nothing matched, per-
1208                    // child min checks are skipped.
1209                    let saw_anything = all_seen.values().any(|&n| n > 0);
1210                    if *outer_min == 0 && !saw_anything {
1211                        // Whole all-group skipped — legal.
1212                    } else {
1213                        for (i, p) in particles.iter().enumerate() {
1214                            let seen = all_seen.get(&i).copied().unwrap_or(0);
1215                            if seen < p.min_occurs {
1216                                let _ = self.handle_min_occurs_violation(p, ctx_off);
1217                            }
1218                        }
1219                    }
1220                }
1221            }
1222        }
1223
1224        // 2. Simple-content / nil / empty validation.
1225        match (&ctx.type_def, ctx.is_nil) {
1226            (_, true) => {
1227                // xsi:nil — element must be empty.
1228                if !ctx.text_buf.trim().is_empty() {
1229                    self.report_at(ValidationKind::NillableViolation,
1230                        "xsi:nil element must have empty content", ctx_off);
1231                }
1232            }
1233            (TypeRef::Simple(st), _) => {
1234                let real = self.resolve_simple_type(st);
1235                // XSD 1.0 §3.3.4 cvc-elt-5: an empty element with a
1236                // `default=` or `fixed=` value validates as that value
1237                // (the schema-supplied "value" stands in for empty
1238                // lexical content).
1239                let to_check = effective_text(&ctx.text_buf, ctx.decl.default.as_deref(),
1240                                              ctx.decl.fixed.as_deref());
1241                if let Err(e) = real.validate_only(to_check) {
1242                    let elem_local = ctx.decl.name.local.to_string();
1243                    self.report_at(e.kind, format!("element content: {}", e.message), ctx_off);
1244                    if let Some(last) = self.issues.last_mut() {
1245                        last.value = Some(to_check.to_string());
1246                        last.type_name = Some(format!("xs:{}", real.builtin.name()));
1247                        // Include the positional `[N]` (matching
1248                        // `current_path`) so the locator points at the right
1249                        // one of N same-named siblings — the element's frame
1250                        // is already popped from `self.stack` at content-check
1251                        // time, so its index isn't in `last.path`.
1252                        let idx = if ctx.sibling_index >= 2 {
1253                            format!("[{}]", ctx.sibling_index)
1254                        } else {
1255                            String::new()
1256                        };
1257                        last.path = format!("{}/{elem_local}{idx}", last.path);
1258                    }
1259                }
1260                // XSD 1.1 `<xs:assertion>` facets on the simple type —
1261                // evaluate `$value`-bound test expressions against
1262                // the lexical input.
1263                self.run_simple_type_assertions(&real, to_check, ctx_off);
1264            }
1265            (TypeRef::Complex(ct), _) => {
1266                match &ct.content {
1267                    ContentModel::Empty => {
1268                        if !ctx.text_buf.trim().is_empty() {
1269                            self.report_at(ValidationKind::TypeMismatch,
1270                                "complexType with empty content cannot have text", ctx_off);
1271                        }
1272                    }
1273                    ContentModel::Simple(st) => {
1274                        // Inline anonymous complex types on local element
1275                        // decls don't go through `merge_inline_extension_in_elements`
1276                        // (which only walks top-level entries), so a
1277                        // `<xs:simpleContent><xs:extension base="T">` body
1278                        // still carries the xs:string placeholder that
1279                        // [`parse_derivation_body`] seeded.  Resolve the
1280                        // extension's declared base lazily here so facet
1281                        // validation sees the real simple type.
1282                        let effective = self.simple_content_effective_type(ct, st);
1283                        let real = self.resolve_simple_type(&effective);
1284                        let to_check = effective_text(&ctx.text_buf, ctx.decl.default.as_deref(),
1285                                                      ctx.decl.fixed.as_deref());
1286                        if let Err(e) = real.validate_only(to_check) {
1287                            let elem_local = ctx.decl.name.local.to_string();
1288                            self.report_at(e.kind,
1289                                format!("element content: {}", e.message), ctx_off);
1290                            if let Some(last) = self.issues.last_mut() {
1291                                last.value = Some(to_check.to_string());
1292                                last.type_name = Some(format!("xs:{}", real.builtin.name()));
1293                                // Include the positional `[N]` (matching
1294                        // `current_path`) so the locator points at the right
1295                        // one of N same-named siblings — the element's frame
1296                        // is already popped from `self.stack` at content-check
1297                        // time, so its index isn't in `last.path`.
1298                        let idx = if ctx.sibling_index >= 2 {
1299                            format!("[{}]", ctx.sibling_index)
1300                        } else {
1301                            String::new()
1302                        };
1303                        last.path = format!("{}/{elem_local}{idx}", last.path);
1304                            }
1305                        }
1306                        self.run_simple_type_assertions(&real, to_check, ctx_off);
1307                    }
1308                    ContentModel::Complex { mixed, .. } => {
1309                        if !*mixed && !ctx.text_buf.trim().is_empty() {
1310                            self.report_at(ValidationKind::TypeMismatch,
1311                                "non-mixed complexType cannot have text content", ctx_off);
1312                        }
1313                    }
1314                }
1315            }
1316        }
1317
1318        // `fixed=` on the declaration applies regardless of simple vs
1319        // complex content (per XSD §3.3.4, equality is *value-space* —
1320        // we approximate with whitespace-trimmed lexical equality, which
1321        // matches the spec for every built-in whose canonical lexical
1322        // form is the trimmed lexical input).
1323        //
1324        // Carve-out: when `xsi:nil="true"` is set, the element is
1325        // treated as having no value, so `fixed=` doesn't apply.
1326        // Otherwise every nillable+fixed element would always fail
1327        // under nil (text_buf "" never equals fixed value).
1328        if !ctx.is_nil
1329            && let Some(fixed) = &ctx.decl.fixed
1330            // Empty content takes the fixed value implicitly (cvc-elt-5.2.2.2);
1331            // a non-empty content must match it lexically (modulo whitespace).
1332            && !ctx.text_buf.is_empty()
1333            && ctx.text_buf.trim() != fixed.trim()
1334        {
1335            self.report_at(ValidationKind::TypeMismatch,
1336                format!("element content {:?} doesn't match fixed value {:?}",
1337                    ctx.text_buf, fixed), ctx_off);
1338        }
1339
1340        // _seen_attrs is currently used only for the required-attr
1341        // pre-check at start time; keep it referenced to silence dead-code.
1342        let _ = ctx.seen_attrs;
1343
1344        // 3. XSD 1.1 `xs:assert` evaluation.  Runs after content/
1345        // attribute/fixed checks so an instance that's already
1346        // structurally invalid doesn't get a redundant assertion
1347        // failure on top.  Each assertion sees the full subtree we
1348        // snapshotted under `collect_depth` — built lazily here.
1349        if let TypeRef::Complex(ct) = &ctx.type_def {
1350            if !ct.assertions.is_empty() {
1351                let snapshot = ChildSnapshot {
1352                    name:     ctx.name.clone(),
1353                    attrs:    ctx.cached_attrs.clone(),
1354                    text:     ctx.text_buf.clone(),
1355                    children: ctx.child_snapshots.clone(),
1356                };
1357                for a in &ct.assertions {
1358                    use super::assertion::{eval_complex_assert, AssertOutcome};
1359                    match eval_complex_assert(a, &snapshot) {
1360                        AssertOutcome::Pass => {}
1361                        AssertOutcome::Fail => {
1362                            self.report_at(ValidationKind::AssertionViolation,
1363                                format!("assertion failed: {}", a.test), ctx_off);
1364                        }
1365                        AssertOutcome::Unevaluable(_why) => {
1366                            // Treat unsupported XPath in the assertion
1367                            // as pass — better to false-negative than
1368                            // false-positive while the evaluator's
1369                            // feature surface is incomplete.
1370                        }
1371                    }
1372                }
1373            }
1374        }
1375
1376        false
1377    }
1378
1379    fn handle_wildcard_match(
1380        &mut self,
1381        qn: &QName,
1382        attrs: &[Attr<'x>],
1383        wc: Wildcard,
1384        pushed_ns: bool,
1385        event_offset: usize,
1386    ) -> bool {
1387        match wc.process_contents {
1388            ProcessContents::Skip => {
1389                self.skip_body_into_issues(pushed_ns)
1390            }
1391            ProcessContents::Lax => {
1392                if let Some(decl) = self.schema.element(qn) {
1393                    let decl = decl.clone();
1394                    self.push_decl_ctx(qn, decl, attrs, pushed_ns, event_offset);
1395                    false
1396                } else {
1397                    self.skip_body_into_issues(pushed_ns)
1398                }
1399            }
1400            ProcessContents::Strict => {
1401                if let Some(decl) = self.schema.element(qn) {
1402                    let decl = decl.clone();
1403                    self.push_decl_ctx(qn, decl, attrs, pushed_ns, event_offset);
1404                    false
1405                } else {
1406                    self.report_at(ValidationKind::UnexpectedElement,
1407                        format!("strict wildcard: <{qn}> not declared in schema"),
1408                        event_offset);
1409                    self.skip_body_into_issues(pushed_ns)
1410                }
1411            }
1412        }
1413    }
1414
1415    /// Validate `attrs` against the complex type's attribute uses
1416    /// and anyAttribute wildcard, reporting unexpected, prohibited,
1417    /// or missing-required attribute issues. Returns the set of
1418    /// QNames that matched a use (for downstream identity-constraint
1419    /// processing). No-op when the type isn't complex.
1420    fn validate_attrs_against_type(
1421        &mut self,
1422        type_def:     &TypeRef,
1423        attrs:        &[Attr<'x>],
1424        event_offset: usize,
1425    ) -> Vec<QName> {
1426        let mut seen_attrs = Vec::with_capacity(attrs.len());
1427        let TypeRef::Complex(ct) = type_def else { return seen_attrs };
1428        for a in attrs {
1429            let aname = a.name;
1430            if aname == "xmlns" || aname.starts_with("xmlns:") { continue; }
1431            // Split into (prefix, local) without allocating.  Per
1432            // XML Namespaces, unprefixed attributes are in *no*
1433            // namespace (they never pick up the default xmlns).
1434            let (prefix_opt, local): (Option<&str>, &str) =
1435                match aname.split_once(':') {
1436                    Some((p, l)) => (Some(p), l),
1437                    None         => (None, aname),
1438                };
1439            let ns_uri: Option<&str> = prefix_opt.and_then(|p| self.resolve_prefix(p));
1440            if ns_uri == Some(XSI_NS) { continue; }
1441            // Lookup by raw (uri, local) — no QName allocation on
1442            // the matched path.  We only build the heap-allocated
1443            // QName for diagnostic / seen_attrs entries when an
1444            // attribute matched a decl (clone the decl's Arc) or
1445            // when an error needs reporting (lazy build).
1446            // XSD §3.4.2 — `use="prohibited"` is a derivation-restriction
1447            // device that removes a base type's attribute use from the
1448            // derived type's attribute uses (so the derived type has no
1449            // declaration for it).  An instance attribute that lands on a
1450            // prohibited entry has no explicit declaration in the
1451            // *effective* attribute uses set, so it falls through to the
1452            // attribute wildcard the same way any unknown attribute
1453            // would.  Treating prohibited as "instance must not present
1454            // this attribute" misreads the spec and rejects valid
1455            // instances that the wildcard would otherwise admit.
1456            let matched = ct.attributes.iter().find(|au| {
1457                au.use_kind != AttributeUseKind::Prohibited
1458                    && au.decl.name.local.as_ref() == local
1459                    && au.decl.name.namespace.as_deref() == ns_uri
1460            });
1461            match matched {
1462                Some(au) => {
1463                    // For ref-form attribute uses the parser stores a
1464                    // placeholder `type_def` of `xs:string` — the real
1465                    // type and any `fixed=` value live on the top-level
1466                    // attribute decl. Look up the ref'd attribute by
1467                    // name; if found, use its type and fixed value,
1468                    // otherwise fall back to the use's own values.
1469                    let referenced = self.schema.attribute(&au.decl.name);
1470                    let resolved_type = referenced
1471                        .map(|d| d.type_def.clone())
1472                        .unwrap_or_else(|| au.decl.type_def.clone());
1473                    let st = self.resolve_simple_type(&resolved_type);
1474                    let val = a.value.as_ref();
1475                    if let Err(e) = st.validate_only(val) {
1476                        self.report_at(e.kind,
1477                            format!("attribute {}: {}", au.decl.name, e.message),
1478                            event_offset);
1479                    }
1480                    let fixed = au.fixed.as_deref()
1481                        .or(au.decl.fixed.as_deref())
1482                        .or_else(|| referenced.and_then(|d| d.fixed.as_deref()));
1483                    if let Some(f) = fixed {
1484                        if val.trim() != f.trim() {
1485                            self.report_at(ValidationKind::TypeMismatch,
1486                                format!("attribute {} value {val:?} \
1487                                         doesn't match fixed value {f:?}",
1488                                        au.decl.name),
1489                                event_offset);
1490                        }
1491                    }
1492                    // Share the decl's already-interned QName: an
1493                    // Arc::clone is just an atomic refcount bump
1494                    // (no heap allocation), while a fresh
1495                    // `parse_attribute_qname` would alloc two
1496                    // Arc<str>s per element.
1497                    seen_attrs.push(au.decl.name.clone());
1498                }
1499                None => {
1500                    // Now we need a heap-allocated QName — either
1501                    // for the wildcard accept check or for an
1502                    // unexpected-attribute diagnostic.
1503                    let attr_qn = QName {
1504                        namespace: ns_uri.map(Arc::from),
1505                        local:     Arc::from(local),
1506                    };
1507                    if let Some(wc) = &ct.any_attribute {
1508                        if attr_wildcard_accepts_qname(wc, &attr_qn, self.schema, ct) {
1509                            // XSD §3.10.4 — `processContents="strict"`
1510                            // is supposed to require the wildcard
1511                            // attribute to be a declared top-level
1512                            // attribute somewhere in the schema, but
1513                            // real-world schemas frequently rely on
1514                            // xsi:schemaLocation hints in the instance
1515                            // to bring those declarations in. Our
1516                            // validator doesn't follow instance hints,
1517                            // so flagging unknown attributes here would
1518                            // produce many false positives. Treat
1519                            // strict like lax for attributes — the
1520                            // typical xs:anyAttribute use case.
1521                            seen_attrs.push(attr_qn);
1522                            continue;
1523                        }
1524                    }
1525                    self.report_at(ValidationKind::UnexpectedAttribute,
1526                        format!("unexpected attribute {attr_qn}"),
1527                        event_offset);
1528                }
1529            }
1530        }
1531        for au in &ct.attributes {
1532            // A value constraint to materialise (only when asked to fill
1533            // defaults, and only for no-namespace attributes).  Computed
1534            // first so the common path — not filling — does no extra work
1535            // for plain optional attributes: it skips straight past them,
1536            // exactly as before, only inspecting `seen_attrs` for the
1537            // required-attribute check.
1538            let fill_value: Option<&str> =
1539                if self.opts.apply_attribute_defaults && au.decl.name.namespace.is_none() {
1540                    au.default.as_deref()
1541                        .or(au.decl.default.as_deref())
1542                        .or(au.fixed.as_deref())
1543                        .or(au.decl.fixed.as_deref())
1544                } else {
1545                    None
1546                };
1547            if au.use_kind != AttributeUseKind::Required && fill_value.is_none() {
1548                continue;
1549            }
1550            let present = seen_attrs.iter().any(|n|
1551                n == &au.decl.name
1552                || (n.local == au.decl.name.local
1553                    && n.namespace.is_none()
1554                    && au.decl.name.namespace.is_none()));
1555            if present { continue; }
1556            // Absent.  Materialise a `default=` / `fixed=` value constraint
1557            // onto the live instance (the XSD post-schema-validation
1558            // infoset adds it; libxml2's XML_SCHEMA_VAL_VC_I_CREATE).  A
1559            // satisfied default isn't then reported as a missing required.
1560            if let Some(value) = fill_value {
1561                self.reader.fill_default_attr(au.decl.name.local.as_ref(), value);
1562                continue;
1563            }
1564            if au.use_kind == AttributeUseKind::Required {
1565                self.report_at(ValidationKind::MissingRequiredAttribute,
1566                    format!("missing required attribute {}", au.decl.name),
1567                    event_offset);
1568            }
1569        }
1570        seen_attrs
1571    }
1572
1573    fn push_decl_ctx(&mut self, qn: &QName, decl: Arc<ElementDecl>, attrs: &[Attr<'x>],
1574        pushed_ns: bool, event_offset: usize,
1575    ) {
1576        let type_def = self.resolve_type(&decl.type_def);
1577        // Run attribute validation against the resolved type — lax/strict
1578        // wildcards land here when the matched element decl pins down a
1579        // concrete type, and the spec requires the per-element attribute
1580        // checks (required, fixed, prohibited, simple-type validation,
1581        // anyAttribute wildcard) to run regardless of how we got here.
1582        let _ = self.validate_attrs_against_type(&type_def, &attrs, event_offset);
1583        let cursor = build_cursor(&type_def);
1584        let declared_scope = if !decl.identity.is_empty() {
1585            let collected = vec![Vec::new(); decl.identity.len()];
1586            self.key_scopes.push(KeyScope {
1587                declaring_depth: self.stack.len(),
1588                declaring_offset: event_offset as u32,
1589                decl: decl.clone(),
1590                collected,
1591            });
1592            self.key_scopes.len() - 1
1593        } else { usize::MAX };
1594        let parent_collect_depth = self.stack.last()
1595            .map(|p| p.collect_depth).unwrap_or(0);
1596        let parent_collects = parent_collect_depth > 0;
1597        let (matched, mut cached_attrs) = self.check_active_scopes(qn, &attrs);
1598        if cached_attrs.is_empty() && parent_collects {
1599            cached_attrs = snapshot_attrs(&attrs, |s| self.parse_attribute_qname(s));
1600        }
1601        let own_collect_depth = matched.iter().map(|(si, ci)| {
1602            self.key_scopes[*si].decl.identity[*ci].fields.iter()
1603                .map(max_field_child_descent)
1604                .max().unwrap_or(0)
1605        }).max().unwrap_or(0);
1606        let collect_depth = own_collect_depth
1607            .max(parent_collect_depth.saturating_sub(1));
1608        let sibling_index = match self.stack.last_mut() {
1609            Some(parent) => bump_child_counter(&mut parent.child_counters, &qn.local),
1610            None => 1,
1611        };
1612        self.record_governing_type(&type_def);
1613        self.stack.push(ElementCtx {
1614            decl,
1615            type_def,
1616            cursor,
1617            text_buf: String::new(),
1618            is_nil: false,
1619            name: qn.clone(),
1620            pushed_ns,
1621            seen_attrs: Vec::new(),
1622            cached_attrs,
1623            declared_scope,
1624            matched_constraints: matched,
1625            collect_depth,
1626            child_snapshots: Vec::new(),
1627            start_offset: event_offset as u32,
1628            _phantom: std::marker::PhantomData,
1629            sibling_index,
1630            child_counters: Vec::new(),
1631        });
1632        // Note: attrs not validated for wildcard-skipped/lax-with-no-decl
1633        // children — matches typical XSD wildcard semantics.
1634        let _ = attrs;
1635    }
1636
1637    /// Skip the body of an element whose start was just consumed (and
1638    /// rejected — caller already reported the issue).  `pushed_ns` is
1639    /// the value returned by [`push_ns_scope`] for the containing
1640    /// element; we pop the scope here when applicable so the caller
1641    /// doesn't have to.
1642    fn skip_body_into_issues(&mut self, pushed_ns: bool) -> bool {
1643        let mut depth = 1usize;
1644        while depth > 0 {
1645            match self.reader.next_into(&mut self.attr_buf) {
1646                Ok(EventInto::StartElement { .. }) => depth += 1,
1647                Ok(EventInto::EndElement { .. })   => depth -= 1,
1648                Ok(EventInto::Eof) => {
1649                    self.report(ValidationKind::Other, "unexpected EOF in skipped subtree");
1650                    return true;
1651                }
1652                Err(e) => {
1653                    self.issues.push(ValidationIssue {
1654                        message: format!("XML parse error: {e}"),
1655                        line: e.line, column: e.column, path: self.current_path(),
1656                        kind: ValidationKind::Other,
1657                        expected: Vec::new(), value: None, type_name: None,
1658                    });
1659                    return true;
1660                }
1661                _ => {}
1662            }
1663        }
1664        self.pop_ns_scope_if(pushed_ns);
1665        false
1666    }
1667
1668    fn handle_min_occurs_violation(&mut self, p: &Particle, at_offset: usize) -> bool {
1669        let what = match &p.term {
1670            Term::Element(e) => format!("element <{}>", e.name),
1671            Term::Group { .. } => "group".to_string(),
1672            Term::Wildcard(_)  => "wildcard".to_string(),
1673            Term::GroupRef(name) => format!("group {name}"),
1674        };
1675        self.report_at(ValidationKind::MissingRequiredElement,
1676            format!("missing required {what} (minOccurs={})", p.min_occurs),
1677            at_offset)
1678    }
1679
1680    // ── type resolution ─────────────────────────────────────────────────
1681
1682    /// Resolve an `UNRESOLVED:` placeholder type to the real one if the
1683    /// schema (or built-in catalogue) declares it.  For non-placeholder
1684    /// types, return as-is.
1685    fn resolve_type(&self, t: &TypeRef) -> TypeRef {
1686        if let TypeRef::Simple(st) = t {
1687            if let Some(name) = &st.name {
1688                if let Some(rest) = name.strip_prefix("UNRESOLVED:") {
1689                    let qn = parse_unresolved_marker(rest);
1690                    if let Some(real) = self.lookup_type_by_qname(&qn) {
1691                        return real;
1692                    }
1693                }
1694            }
1695        }
1696        t.clone()
1697    }
1698
1699    /// True when every constraint cross-referenced with the
1700    /// constraint at `(scope_idx, constraint_idx)` also uses
1701    /// all-`.` field paths.  This is the gate that lets a key /
1702    /// keyref pair share the same canonical key encoding — see
1703    /// the call site in `handle_end` for rationale.  For a Key /
1704    /// Unique target, the cross-references are every `KeyRef`
1705    /// pointing at it.  For a KeyRef, the cross-reference is the
1706    /// `refer=` target.  No referring constraints → vacuously
1707    /// `true` (canonicalisation is safe).
1708    fn constraint_peers_all_dot(&self, scope_idx: usize, constraint_idx: usize) -> bool {
1709        use super::identity::ConstraintKind;
1710        let me = &self.key_scopes[scope_idx].decl.identity[constraint_idx];
1711        match me.kind {
1712            ConstraintKind::Key | ConstraintKind::Unique => {
1713                for scope in &self.key_scopes {
1714                    for c in scope.decl.identity.iter() {
1715                        if c.kind != ConstraintKind::KeyRef { continue; }
1716                        if c.refer.as_ref() != Some(&me.name) { continue; }
1717                        if !c.fields.iter().all(field_path_is_dot) {
1718                            return false;
1719                        }
1720                    }
1721                }
1722                true
1723            }
1724            ConstraintKind::KeyRef => {
1725                let Some(target_name) = me.refer.as_ref() else { return true; };
1726                for scope in &self.key_scopes {
1727                    for c in scope.decl.identity.iter() {
1728                        if !matches!(c.kind,
1729                            ConstraintKind::Key | ConstraintKind::Unique) { continue; }
1730                        if &c.name != target_name { continue; }
1731                        if !c.fields.iter().all(field_path_is_dot) {
1732                            return false;
1733                        }
1734                    }
1735                }
1736                true
1737            }
1738        }
1739    }
1740
1741    /// For an identity-constraint `<xs:field xpath="."/>` evaluated
1742    /// on an element with effective type `type_def`, the simple type
1743    /// that should be used to canonicalise the field value.  Returns
1744    /// `None` for complex element-only types (no text value to
1745    /// canonicalise) so the caller falls back to the raw lexical.
1746    fn field_dot_simple_type(
1747        &self, type_def: &TypeRef,
1748    ) -> Option<Arc<SimpleType>> {
1749        match type_def {
1750            TypeRef::Simple(st) => Some(self.resolve_simple_type(st)),
1751            TypeRef::Complex(ct) => match &ct.content {
1752                ContentModel::Simple(st) => {
1753                    let effective = self.simple_content_effective_type(ct, st);
1754                    Some(self.resolve_simple_type(&effective))
1755                }
1756                _ => None,
1757            },
1758        }
1759    }
1760
1761    /// XSD 1.0 §3.4.2 — for a complex type with `<xs:simpleContent>
1762    /// <xs:extension base="T"/>`, the effective simple content type
1763    /// is `T`.  `merge_inline_extension_in_elements` patches this in
1764    /// for top-level element decls, but inline anonymous complex
1765    /// types on LOCAL element decls (nested inside another complex
1766    /// type's particles) aren't reached by that walk and still carry
1767    /// the `ContentModel::Simple(xs:string)` placeholder
1768    /// `parse_derivation_body` seeded.  Detect that shape here and
1769    /// substitute the extension's declared base, so element-text
1770    /// facet validation sees the right type.  Returns `current`
1771    /// unchanged when the type is already correctly resolved or
1772    /// when the derivation base isn't a simple type.
1773    fn simple_content_effective_type(
1774        &self, ct: &Arc<ComplexType>, current: &Arc<SimpleType>,
1775    ) -> Arc<SimpleType> {
1776        // Only patch the seeded placeholder — `SimpleType::of_builtin(
1777        // String)` leaves `name = None` with no facets, which is what
1778        // [`parse_derivation_body`] writes before the merge pass
1779        // resolves the extension base.  A deliberately-declared
1780        // `xs:string` content has `name = Some("string")` and is left
1781        // untouched.
1782        let is_placeholder = current.name.is_none()
1783            && matches!(current.builtin, super::types::BuiltinType::String);
1784        if !is_placeholder { return current.clone(); }
1785        let Some(d) = &ct.derivation else { return current.clone(); };
1786        if d.method != super::types::DerivationMethod::Extension {
1787            return current.clone();
1788        }
1789        let base = self.resolve_type(&d.base);
1790        match base {
1791            TypeRef::Simple(s) => s,
1792            TypeRef::Complex(_) => current.clone(),
1793        }
1794    }
1795
1796    fn resolve_simple_type(&self, st: &Arc<SimpleType>) -> Arc<SimpleType> {
1797        // First, resolve THIS type if it's an UNRESOLVED placeholder.
1798        let top = if let Some(name) = &st.name {
1799            if let Some(rest) = name.strip_prefix("UNRESOLVED:") {
1800                let qn = parse_unresolved_marker(rest);
1801                if let Some(TypeRef::Simple(real)) = self.lookup_type_by_qname(&qn) {
1802                    real
1803                } else {
1804                    st.clone()
1805                }
1806            } else { st.clone() }
1807        } else { st.clone() };
1808
1809        // Then recursively resolve any placeholders inside variety:
1810        // List.item_type and Union.members can themselves be
1811        // UNRESOLVED, and the existing parser doesn't run a topo-sort
1812        // post-pass.  This recursive resolve makes validation work
1813        // regardless of declaration order.
1814        match &top.variety {
1815            super::types::Variety::Atomic => top,
1816            super::types::Variety::List { item_type } => {
1817                let new_item = self.resolve_simple_type(item_type);
1818                if Arc::ptr_eq(&new_item, item_type) {
1819                    top
1820                } else {
1821                    let mut t: SimpleType = (*top).clone();
1822                    t.variety = super::types::Variety::List { item_type: new_item };
1823                    Arc::new(t)
1824                }
1825            }
1826            super::types::Variety::Union { members } => {
1827                let new_members: Vec<Arc<SimpleType>> = members.iter()
1828                    .map(|m| self.resolve_simple_type(m))
1829                    .collect();
1830                let changed = new_members.iter().zip(members.iter())
1831                    .any(|(a, b)| !Arc::ptr_eq(a, b));
1832                if !changed {
1833                    top
1834                } else {
1835                    let mut t: SimpleType = (*top).clone();
1836                    t.variety = super::types::Variety::Union { members: new_members };
1837                    Arc::new(t)
1838                }
1839            }
1840        }
1841    }
1842
1843    fn lookup_type_by_qname(&self, qn: &QName) -> Option<TypeRef> {
1844        // Built-ins live in the XSD spec namespace.
1845        if qn.namespace.as_deref() == Some(QName::XSD_NS) {
1846            if let Some(b) = BuiltinType::from_name(&qn.local) {
1847                return Some(TypeRef::Simple(Arc::new(SimpleType {
1848                    name: Some(qn.local.clone()),
1849                    builtin: b,
1850                    facets: super::facets::FacetSet::default(),
1851                    whitespace: b.default_whitespace(),
1852                    variety: super::types::Variety::Atomic,
1853                    final_: super::schema::BlockSet::default(),
1854                    assertions: Vec::new(),
1855                })));
1856            }
1857        }
1858        // Schema-declared types.
1859        self.schema.type_def(qn).cloned()
1860    }
1861}
1862
1863fn build_cursor(type_def: &TypeRef) -> ContentCursor {
1864    match type_def {
1865        TypeRef::Simple(_) => ContentCursor::None,
1866        TypeRef::Complex(ct) => match ct.matcher.get() {
1867            Some(super::dfa::ContentMatcher::Dfa(dfa)) => ContentCursor::Dfa {
1868                dfa: dfa.clone(),
1869                state: dfa.initial,
1870            },
1871            Some(super::dfa::ContentMatcher::All) => match &ct.content {
1872                ContentModel::Complex { root: Particle { term: Term::Group { kind, particles }, min_occurs, .. }, .. } => {
1873                    ContentCursor::Group {
1874                        kind: *kind,
1875                        particles: Arc::clone(particles),
1876                        idx: 0,
1877                        cur_count: 0,
1878                        all_seen: HashMap::default(),
1879                        outer_min: *min_occurs,
1880                    }
1881                }
1882                _ => ContentCursor::None,
1883            },
1884            Some(super::dfa::ContentMatcher::None) | None => ContentCursor::None,
1885        },
1886    }
1887}
1888
1889/// Look up an attribute named `xsi:<local>` (where `xsi` is bound to the
1890/// XSD-instance namespace).  We match only on the literal `xsi:` prefix
1891/// since that's the universal convention; instances using a different
1892/// prefix bound to the same URI fall through to the regular attribute
1893/// validation path (and would currently miss the special handling — a
1894/// known v1 limitation, documented).
1895///
1896/// Two specialisations to keep the hot path allocation-free.
1897fn find_xsi_type<'a>(attrs: &'a [Attr]) -> Option<&'a str> {
1898    attrs.iter().find(|a| a.name() == "xsi:type").map(|a| a.value.as_ref())
1899}
1900
1901/// XSD 1.0 §3.3.4 cvc-elt-5.1.2 / cvc-elt-5.2.2.2: an empty element
1902/// with a `default=` or `fixed=` value behaves as if its content
1903/// were the supplied value when validating against the simple
1904/// type.  `fixed` takes precedence over `default` (an element can
1905/// only declare one of the two per §3.3.3, but we honour the
1906/// precedence order anyway).
1907fn effective_text<'a>(text: &'a str, default: Option<&'a str>, fixed: Option<&'a str>) -> &'a str {
1908    if !text.is_empty() { return text; }
1909    fixed.or(default).unwrap_or(text)
1910}
1911
1912fn find_xsi_nil<'a>(attrs: &'a [Attr]) -> Option<&'a str> {
1913    attrs.iter().find(|a| a.name() == "xsi:nil").map(|a| a.value.as_ref())
1914}
1915
1916/// Result of parsing a raw `xsi:nil` value as xs:boolean (XSD 1.0 §3.2.2).
1917///
1918/// The lexical space is exactly `{"true", "false", "1", "0"}` —
1919/// case-sensitive.  Anything else is a lexical error; we surface that
1920/// distinctly so the validator can emit a clean type-mismatch
1921/// diagnostic instead of silently picking a default.
1922enum XsiNilParse<'a> {
1923    True,
1924    False,
1925    Invalid(&'a str),
1926}
1927
1928fn parse_xsi_nil(raw: &str) -> XsiNilParse<'_> {
1929    match raw {
1930        "true"  | "1" => XsiNilParse::True,
1931        "false" | "0" => XsiNilParse::False,
1932        other         => XsiNilParse::Invalid(other),
1933    }
1934}
1935
1936fn parse_unresolved_marker(s: &str) -> QName {
1937    // Format: `{ns}local`   or just `local`
1938    if let Some(rest) = s.strip_prefix('{') {
1939        if let Some(end) = rest.find('}') {
1940            let ns = &rest[..end];
1941            let local = &rest[end + 1..];
1942            return QName::new(if ns.is_empty() { None } else { Some(ns) }, local);
1943        }
1944    }
1945    QName::new(None, s)
1946}
1947
1948// ── content matching ─────────────────────────────────────────────────────────
1949
1950enum MatchOutcome {
1951    Element(Arc<ElementDecl>),
1952    Wildcard(Wildcard),
1953    None,
1954}
1955
1956/// The element names a content-model cursor would accept next — what
1957/// libxml2 renders as "Expected is ( … )" on an unexpected-element
1958/// error.  Best-effort: returns the DFA state's outgoing element
1959/// transitions, or the current all-group particle's element names.
1960fn expected_element_names(cursor: &ContentCursor) -> Vec<String> {
1961    match cursor {
1962        ContentCursor::Dfa { dfa, state } => dfa.states[*state as usize]
1963            .on_element
1964            .iter()
1965            .map(|t| t.name.local.to_string())
1966            .collect(),
1967        ContentCursor::Group { particles, idx, .. } => particles
1968            .get(*idx)
1969            .map(particle_element_names)
1970            .unwrap_or_default(),
1971        ContentCursor::None => Vec::new(),
1972    }
1973}
1974
1975fn particle_element_names(p: &Particle) -> Vec<String> {
1976    match &p.term {
1977        super::schema::Term::Element(decl) => vec![decl.name.local.to_string()],
1978        super::schema::Term::Group { particles, .. } => {
1979            particles.iter().flat_map(particle_element_names).collect()
1980        }
1981        _ => Vec::new(),
1982    }
1983}
1984
1985fn match_in_cursor(qn: &QName, cursor: &mut ContentCursor, schema: &Schema) -> MatchOutcome {
1986    match cursor {
1987        ContentCursor::None => MatchOutcome::None,
1988        ContentCursor::Dfa { dfa, state } => {
1989            let target_ns = schema.target_namespace();
1990            let siblings  = dfa.defined_siblings.clone();
1991            let step = dfa.step(*state, qn, |wc, qn| {
1992                super::dfa::wildcard_admits(
1993                    wc, qn, target_ns,
1994                    |q| schema.element(q).is_some(),
1995                    |q| siblings.iter().any(|s| s == q),
1996                )
1997            });
1998            match step {
1999                Some(super::dfa::DfaTransition::Element { next, decl }) => {
2000                    *state = next;
2001                    MatchOutcome::Element(decl)
2002                }
2003                Some(super::dfa::DfaTransition::Wildcard { next, process_contents }) => {
2004                    *state = next;
2005                    // Downstream handling only consults `process_contents`;
2006                    // the synthetic wildcard skips the now-redundant
2007                    // namespace/notQName state.
2008                    MatchOutcome::Wildcard(super::schema::Wildcard {
2009                        namespaces: super::schema::NamespaceConstraint::Any,
2010                        process_contents,
2011                        not_qnames:                Vec::new(),
2012                        not_namespaces:            Vec::new(),
2013                        not_qname_defined:         false,
2014                        not_qname_defined_sibling: false,
2015                    })
2016                }
2017                None => MatchOutcome::None,
2018            }
2019        }
2020        ContentCursor::Group { kind, particles, idx, cur_count, all_seen, .. } => {
2021            match kind {
2022                GroupKind::Sequence => match_sequence(qn, particles, idx, cur_count, schema),
2023                GroupKind::Choice   => match_choice(qn, particles, idx, cur_count, schema),
2024                GroupKind::All      => match_all(qn, particles, all_seen, schema),
2025            }
2026        }
2027    }
2028}
2029
2030fn match_sequence(
2031    qn: &QName,
2032    particles: &Arc<[Particle]>,
2033    idx: &mut usize,
2034    cur_count: &mut u32,
2035    schema: &Schema,
2036) -> MatchOutcome {
2037    while *idx < particles.len() {
2038        let p = &particles[*idx];
2039        if particle_accepts(p, qn, schema) {
2040            // If we'd exceed maxOccurs, advance to next particle and retry.
2041            if !p.max_occurs.allows(*cur_count + 1) {
2042                *idx += 1;
2043                *cur_count = 0;
2044                continue;
2045            }
2046            *cur_count += 1;
2047            return outcome_for(p, qn, schema);
2048        }
2049        // Doesn't match — current particle must have met its min already.
2050        if *cur_count < p.min_occurs {
2051            return MatchOutcome::None;
2052        }
2053        *idx += 1;
2054        *cur_count = 0;
2055    }
2056    MatchOutcome::None
2057}
2058
2059fn match_choice(
2060    qn: &QName,
2061    particles: &Arc<[Particle]>,
2062    idx: &mut usize,
2063    cur_count: &mut u32,
2064    schema: &Schema,
2065) -> MatchOutcome {
2066    // First match in the choice: pick that particle.
2067    if *cur_count == 0 {
2068        for (i, p) in particles.iter().enumerate() {
2069            if particle_accepts(p, qn, schema) {
2070                *idx = i;
2071                *cur_count = 1;
2072                return outcome_for(p, qn, schema);
2073            }
2074        }
2075        return MatchOutcome::None;
2076    }
2077    // Subsequent matches stay on the chosen particle.
2078    let p = &particles[*idx];
2079    if particle_accepts(p, qn, schema) && p.max_occurs.allows(*cur_count + 1) {
2080        *cur_count += 1;
2081        return outcome_for(p, qn, schema);
2082    }
2083    MatchOutcome::None
2084}
2085
2086fn match_all(
2087    qn: &QName,
2088    particles: &Arc<[Particle]>,
2089    all_seen: &mut HashMap<usize, u32>,
2090    schema: &Schema,
2091) -> MatchOutcome {
2092    let siblings = collect_particle_siblings(particles);
2093    for (i, p) in particles.iter().enumerate() {
2094        if particle_accepts_with_siblings(p, qn, schema, &siblings) {
2095            let seen = all_seen.entry(i).or_insert(0);
2096            if !p.max_occurs.allows(*seen + 1) {
2097                continue;
2098            }
2099            *seen += 1;
2100            return outcome_for(p, qn, schema);
2101        }
2102    }
2103    MatchOutcome::None
2104}
2105
2106/// Flatten the element names statically declared in `particles` for
2107/// the `##definedSibling` exclusion in any wildcards they contain.
2108/// Walks nested groups; substitution-group members aren't included
2109/// (they're independent top-level declarations, not siblings of this
2110/// type's content).
2111fn collect_particle_siblings(particles: &[Particle]) -> Vec<QName> {
2112    fn walk(p: &Particle, out: &mut Vec<QName>) {
2113        match &p.term {
2114            Term::Element(decl) => {
2115                if !out.iter().any(|n| n == &decl.name) {
2116                    out.push(decl.name.clone());
2117                }
2118            }
2119            Term::Group { particles, .. } => {
2120                for c in particles.iter() { walk(c, out); }
2121            }
2122            Term::Wildcard(_) | Term::GroupRef(_) => {}
2123        }
2124    }
2125    let mut out = Vec::new();
2126    for p in particles { walk(p, &mut out); }
2127    out
2128}
2129
2130fn particle_accepts(p: &Particle, qn: &QName, schema: &Schema) -> bool {
2131    match &p.term {
2132        Term::Element(decl) => {
2133            if &decl.name == qn { return true; }
2134            // Substitution-group dispatch — disabled when the anchor
2135            // has `block="substitution"` (XSD 1.0 §3.3.4 cvc-elt-2.2).
2136            if decl.block.contains(super::schema::BlockSet::SUBSTITUTION) {
2137                return false;
2138            }
2139            for sub in schema.substitutes_for(&decl.name) {
2140                if &sub.name == qn { return true; }
2141            }
2142            false
2143        }
2144        Term::Wildcard(wc) => wildcard_accepts_qname(wc, qn, schema, &[]),
2145        Term::Group { particles, .. } => particles.iter().any(|p2| particle_accepts(p2, qn, schema)),
2146        // GroupRef should have been expanded at schema compile time.
2147        Term::GroupRef(_) => false,
2148    }
2149}
2150
2151/// Variant of [`particle_accepts`] that knows the surrounding
2152/// sibling-element set — wildcards using `notQName="##definedSibling"`
2153/// (XSD 1.1 §3.10.4) need this to know which names to exclude.  The
2154/// `xs:all` path threads its top-level particles in as `siblings`;
2155/// `xs:sequence` / `xs:choice` flow through the DFA path instead and
2156/// don't reach this helper.
2157fn particle_accepts_with_siblings(
2158    p:        &Particle,
2159    qn:       &QName,
2160    schema:   &Schema,
2161    siblings: &[QName],
2162) -> bool {
2163    match &p.term {
2164        Term::Wildcard(wc) => wildcard_accepts_qname(wc, qn, schema, siblings),
2165        Term::Group { particles, .. } => particles
2166            .iter()
2167            .any(|p2| particle_accepts_with_siblings(p2, qn, schema, siblings)),
2168        // Element / GroupRef share the simpler path.
2169        _ => particle_accepts(p, qn, schema),
2170    }
2171}
2172
2173/// Standalone version of [`Validator::derivation_methods_from`] —
2174/// returns the set of XSD derivation methods used to derive `child`
2175/// from `ancestor` (via the schema's type map), or `None` when
2176/// `child` doesn't derive from `ancestor`. Used by the
2177/// substitution-group dispatch which doesn't have `&Validator`.
2178fn derivation_methods_between(
2179    child:    &TypeRef,
2180    ancestor: &TypeRef,
2181    schema:   &Schema,
2182) -> Option<BlockSet> {
2183    let child = resolve_typeref_via_schema(child, schema);
2184    let ancestor = resolve_typeref_via_schema(ancestor, schema);
2185    if type_refs_equal(&child, &ancestor) {
2186        return Some(BlockSet::empty());
2187    }
2188    if is_any_type(&ancestor) {
2189        return Some(BlockSet::RESTRICTION);
2190    }
2191    if is_any_simple_type(&ancestor) {
2192        return match &child {
2193            TypeRef::Simple(_)  => Some(BlockSet::RESTRICTION),
2194            TypeRef::Complex(_) => None,
2195        };
2196    }
2197    match &child {
2198        TypeRef::Complex(ct) => {
2199            let mut methods = BlockSet::empty();
2200            let mut cur: Arc<ComplexType> = ct.clone();
2201            for _ in 0..64 {
2202                let d = cur.derivation.as_ref()?;
2203                methods |= match d.method {
2204                    DerivationMethod::Restriction => BlockSet::RESTRICTION,
2205                    DerivationMethod::Extension   => BlockSet::EXTENSION,
2206                };
2207                let resolved_base = resolve_typeref_via_schema(&d.base, schema);
2208                if type_refs_equal(&resolved_base, &ancestor) {
2209                    return Some(methods);
2210                }
2211                match resolved_base {
2212                    TypeRef::Complex(next) => { cur = next; }
2213                    TypeRef::Simple(_)     => return None,
2214                }
2215            }
2216            None
2217        }
2218        TypeRef::Simple(s_child) => {
2219            if let TypeRef::Simple(s_anc) = &ancestor {
2220                if s_child.builtin == s_anc.builtin {
2221                    Some(BlockSet::RESTRICTION)
2222                } else { None }
2223            } else { None }
2224        }
2225    }
2226}
2227
2228fn resolve_typeref_via_schema(tr: &TypeRef, schema: &Schema) -> TypeRef {
2229    if let TypeRef::Simple(st) = tr {
2230        if let Some(name) = &st.name {
2231            if let Some(rest) = name.strip_prefix("UNRESOLVED:") {
2232                let qn = parse_unresolved_marker(rest);
2233                if let Some(real) = schema.type_def(&qn) {
2234                    return real.clone();
2235                }
2236            }
2237        }
2238    }
2239    tr.clone()
2240}
2241
2242/// Element-wildcard match against a candidate qname.  Delegates to
2243/// the shared XSD 1.1 helper in [`super::dfa`].  `siblings` is the
2244/// enclosing complex type's element-declaration name set, consulted
2245/// only when the wildcard carries `notQName="##definedSibling"`.
2246fn wildcard_accepts_qname(
2247    wc:       &Wildcard,
2248    qn:       &QName,
2249    schema:   &Schema,
2250    siblings: &[QName],
2251) -> bool {
2252    super::dfa::wildcard_admits(
2253        wc, qn, schema.target_namespace(),
2254        |q| schema.element(q).is_some(),
2255        |q| siblings.iter().any(|s| s == q),
2256    )
2257}
2258
2259/// Attribute-wildcard match against a candidate attribute qname.
2260/// `##defined` consults the schema's *attribute* table (not elements);
2261/// `##definedSibling` consults the enclosing complex type's declared
2262/// attribute uses.
2263fn attr_wildcard_accepts_qname(
2264    wc:           &Wildcard,
2265    qn:           &QName,
2266    schema:       &Schema,
2267    ct:           &ComplexType,
2268) -> bool {
2269    super::dfa::wildcard_admits(
2270        wc, qn, schema.target_namespace(),
2271        |q| schema.attribute(q).is_some(),
2272        |q| ct.attributes.iter().any(|au| &au.decl.name == q),
2273    )
2274}
2275
2276fn outcome_for(p: &Particle, qn: &QName, schema: &Schema) -> MatchOutcome {
2277    match &p.term {
2278        Term::Element(decl) => {
2279            // Substitution: if qn is a substitute, use that decl instead.
2280            if &decl.name == qn {
2281                return MatchOutcome::Element(decl.clone());
2282            }
2283            // Honor `block="substitution"` (cvc-elt-2.2) — fall back
2284            // to the anchor decl so dispatch reports the qname mismatch
2285            // against it rather than promoting a forbidden substitute.
2286            if !decl.block.contains(super::schema::BlockSet::SUBSTITUTION) {
2287                for sub in schema.substitutes_for(&decl.name) {
2288                    if &sub.name == qn {
2289                        // XSD §3.3.6 (cvc-elt-substitution) — if the
2290                        // head's `block` covers the derivation method
2291                        // used to derive the substitute's type from
2292                        // the head's type, the substitution is
2293                        // forbidden. Fall back to the anchor so the
2294                        // ensuing dispatch reports a qname mismatch.
2295                        let methods = derivation_methods_between(
2296                            &sub.type_def, &decl.type_def, schema,
2297                        );
2298                        if let Some(m) = methods {
2299                            if !(decl.block & m).is_empty() {
2300                                return MatchOutcome::Element(decl.clone());
2301                            }
2302                        }
2303                        return MatchOutcome::Element(sub.clone());
2304                    }
2305                }
2306            }
2307            MatchOutcome::Element(decl.clone())
2308        }
2309        Term::Wildcard(wc) => MatchOutcome::Wildcard(wc.clone()),
2310        Term::Group { particles, .. } => {
2311            // Nested group matched — find the actual particle that took it.
2312            for p in particles.iter() {
2313                if particle_accepts(p, qn, schema) {
2314                    return outcome_for(p, qn, schema);
2315                }
2316            }
2317            MatchOutcome::None
2318        }
2319        Term::GroupRef(_) => MatchOutcome::None,
2320    }
2321}
2322
2323// ── identity-constraint helpers ──────────────────────────────────────────────
2324
2325/// Match a [`SelectorPath`] against the *current* element, given the
2326/// element name `qn`, the current stack frames, and `rel_depth` (the
2327/// number of element levels between the constraint-declaring element
2328/// and the current element).
2329///
2330/// v1 supports the two most common shapes: `name` (direct children) and
2331/// `.//name` (any descendant), plus `*` and `name1/name2`.  Step-by-step
2332/// matching against the parents at the corresponding stack offsets.
2333fn selector_matches<'s>(
2334    sel: &SelectorPath,
2335    stack: &[ElementCtx<'s>],
2336    qn: &QName,
2337    rel_depth: usize,
2338) -> bool {
2339    sel.paths.iter().any(|p| path_matches(p, stack, qn, rel_depth))
2340}
2341
2342fn path_matches<'s>(
2343    p: &PathExpr,
2344    stack: &[ElementCtx<'s>],
2345    qn: &QName,
2346    rel_depth: usize,
2347) -> bool {
2348    let n = p.steps.len();
2349    if n == 0 {
2350        // `.` selector — matches the constraint-declaring element itself.
2351        return rel_depth == 0;
2352    }
2353    if p.descendant {
2354        // `.//step1/.../stepN` — matches at any depth ≥ N.
2355        if rel_depth < n { return false; }
2356    } else {
2357        // Anchored — must match at exact depth N.
2358        if rel_depth != n { return false; }
2359    }
2360    // Compare each step against the corresponding ancestor.
2361    // `qn` is the element at depth `rel_depth`; ancestors live in the
2362    // stack frames.  Steps are matched in reverse: steps[n-1] vs qn,
2363    // steps[n-2] vs parent, etc.
2364    let stack_top = stack.len();
2365    for (k, step) in p.steps.iter().rev().enumerate() {
2366        let nm = match step {
2367            PathStep::Child(nm) | PathStep::Attribute(nm) => nm,
2368        };
2369        let target_qn: &QName = if k == 0 {
2370            qn
2371        } else {
2372            &stack[stack_top - k].name
2373        };
2374        if !name_test_matches_qname(nm, target_qn) {
2375            return false;
2376        }
2377    }
2378    true
2379}
2380
2381fn name_test_matches_qname(nt: &NameTest, qn: &QName) -> bool {
2382    match nt {
2383        NameTest::Any        => true,
2384        // Forgiving namespace match: an unprefixed name in an
2385        // identity-constraint XPath matches elements in *any*
2386        // namespace (matches libxml2/Xerces behaviour for the typical
2387        // case where the selector author meant "the schema's
2388        // namespace" but didn't bind a prefix).
2389        NameTest::Name(want) => {
2390            want.local == qn.local
2391                && (want.namespace.is_none() || want.namespace == qn.namespace)
2392        }
2393        NameTest::AnyInNs(ns) => qn.namespace.as_deref() == Some(ns.as_ref()),
2394    }
2395}
2396
2397/// Evaluate one [`FieldPath`] against the just-finished element.
2398///
2399/// Walks each alternative path (separated by `|` in XSD source) and
2400/// returns the first non-missing value.  Supports arbitrary-depth
2401/// `child/.../@attr` paths via recursive descent through the
2402/// pre-collected snapshot tree — see [`ChildSnapshot`].
2403///
2404/// Carve-out: the `.//` descendant-axis prefix on field paths is
2405/// parsed but not yet honored.  XSD §3.11.6 allows it on fields; in
2406/// practice it's rare — real-world fields are anchored.  Adding
2407/// support would mean searching the snapshot tree rather than
2408/// indexing by name; flagged in `thoughts/unfinished.txt`.
2409/// Outcome of evaluating an `<xs:field>` xpath at a matched
2410/// selector node.
2411enum FieldEval {
2412    /// xpath selected no node — the tuple slot is `None`
2413    /// (legal for `xs:unique`, fatal for `xs:key`).
2414    Missing,
2415    /// xpath selected exactly one node — the value of that node.
2416    Single(String),
2417    /// xpath selected more than one node — XSD §3.11.6 forbids this.
2418    Ambiguous,
2419}
2420
2421fn eval_field(
2422    fp: &FieldPath,
2423    cached_attrs: &[(QName, String)],
2424    text_buf: &str,
2425    children: &[ChildSnapshot],
2426) -> FieldEval {
2427    // The schema-compile XPath subset stores alternatives in `paths`
2428    // (one per `|`-separated branch).  We keep the union semantics:
2429    // the first branch that yields a value wins.  Ambiguity reported
2430    // by any branch propagates.
2431    for path in &fp.paths {
2432        match eval_path_steps(&path.steps, cached_attrs, text_buf, children) {
2433            FieldEval::Single(v)  => return FieldEval::Single(v),
2434            FieldEval::Ambiguous  => return FieldEval::Ambiguous,
2435            FieldEval::Missing    => continue,
2436        }
2437    }
2438    FieldEval::Missing
2439}
2440
2441/// Recursive evaluator for a single path's [`PathStep`] sequence.
2442/// Walks child steps through `children`, descending into each
2443/// snapshot's own sub-children for nested steps.  Terminates on:
2444///   * empty steps (`.`)           → current element's text
2445///   * `[Attribute]` (last step)   → attribute on current element
2446///   * `[Child, rest...]`          → descend into matching child
2447fn eval_path_steps(
2448    steps: &[PathStep],
2449    attrs: &[(QName, String)],
2450    text:  &str,
2451    children: &[ChildSnapshot],
2452) -> FieldEval {
2453    match steps {
2454        [] => FieldEval::Single(text.trim().to_string()),
2455        [PathStep::Attribute(nm)] => match lookup_attr(attrs, nm) {
2456            Some(v) => FieldEval::Single(v),
2457            None    => FieldEval::Missing,
2458        },
2459        [PathStep::Child(nm), rest @ ..] => {
2460            let mut matched: Option<&ChildSnapshot> = None;
2461            for c in children {
2462                if name_test_matches_qname(nm, &c.name) {
2463                    if matched.is_some() {
2464                        return FieldEval::Ambiguous;
2465                    }
2466                    matched = Some(c);
2467                }
2468            }
2469            match matched {
2470                Some(c) => eval_path_steps(rest, &c.attrs, &c.text, &c.children),
2471                None    => FieldEval::Missing,
2472            }
2473        }
2474        // Attribute in non-final position is rejected by the
2475        // identity-constraint XPath micro-parser, so we shouldn't
2476        // see it here.  If we do (corrupt schema), miss safely.
2477        _ => FieldEval::Missing,
2478    }
2479}
2480
2481fn lookup_attr(attrs: &[(QName, String)], nt: &NameTest) -> Option<String> {
2482    match nt {
2483        NameTest::Any => attrs.first().map(|(_, v)| v.clone()),
2484        NameTest::Name(want) => attrs.iter()
2485            .find(|(qn, _)| {
2486                // Match on local name; tolerate the "no namespace" form
2487                // (XSD identity-constraint XPath unprefixed names).
2488                qn.local == want.local
2489                    && (want.namespace.is_none() || want.namespace == qn.namespace)
2490            })
2491            .map(|(_, v)| v.clone()),
2492        NameTest::AnyInNs(ns) => attrs.iter()
2493            .find(|(qn, _)| qn.namespace.as_deref() == Some(ns.as_ref()))
2494            .map(|(_, v)| v.clone()),
2495    }
2496}
2497
2498impl<'s, 'x, E: XsdEventSource<'x>> Validator<'s, 'x, E> {
2499    /// Validate a closing key-scope: uniqueness for key/unique,
2500    /// referential integrity for keyref.
2501    fn finalize_key_scope(&mut self, scope: &KeyScope) {
2502        // All identity-constraint issues are attributed to the
2503        // declaring element — by the time finalize fires, that element
2504        // has already been popped from the stack and the reader has
2505        // advanced past it, so the default offset is meaningless.
2506        let off = scope.declaring_offset as usize;
2507        for (ci, c) in scope.decl.identity.iter().enumerate() {
2508            let tuples = &scope.collected[ci];
2509            match c.kind {
2510                ConstraintKind::Key | ConstraintKind::Unique => {
2511                    // Uniqueness: every non-null tuple must appear once.
2512                    // For xs:unique, tuples containing any absent field
2513                    // are skipped (no key value → nothing to compare).
2514                    // For xs:key, every field must be present — a None
2515                    // there is a validity error in its own right.
2516                    let mut seen: HashMap<&KeyTuple, ()> = HashMap::default();
2517                    for t in tuples {
2518                        let has_null = t.iter().any(|f| f.is_none());
2519                        if has_null {
2520                            if c.kind == ConstraintKind::Key {
2521                                self.report_at(
2522                                    ValidationKind::Other,
2523                                    format!(
2524                                        "<xs:key {:?}>: a selected element is missing one of the field values",
2525                                        c.name.local
2526                                    ),
2527                                    off,
2528                                );
2529                            }
2530                            // xs:unique skips null tuples entirely.
2531                            continue;
2532                        }
2533                        if seen.insert(t, ()).is_some() {
2534                            let pretty = format_tuple(t);
2535                            self.report_at(
2536                                ValidationKind::KeyNotUnique,
2537                                format!(
2538                                    "<xs:{} {:?}>: duplicate key value {pretty}",
2539                                    if c.kind == ConstraintKind::Key { "key" } else { "unique" },
2540                                    c.name.local
2541                                ),
2542                                off,
2543                            );
2544                        }
2545                    }
2546                }
2547                ConstraintKind::KeyRef => {
2548                    let refer = match &c.refer {
2549                        Some(r) => r,
2550                        None => continue, // schema-compile error, but be tolerant
2551                    };
2552                    // Find the referenced key/unique in this scope or
2553                    // any enclosing scope.
2554                    // Resolve the referenced key: same-scope first, then
2555                    // any enclosing scope.  Collect dangling tuples and
2556                    // report after the borrow ends — `report` mutably
2557                    // borrows `self`, but the lookup borrows scopes
2558                    // immutably.
2559                    let dangling: Vec<KeyTuple> = {
2560                        let referenced: Option<&Vec<KeyTuple>> = scope.decl.identity.iter()
2561                            .position(|other| &other.name == refer)
2562                            .map(|i| &scope.collected[i])
2563                            .or_else(|| {
2564                                for outer in self.key_scopes.iter().rev() {
2565                                    if let Some(i) = outer.decl.identity.iter()
2566                                        .position(|other| &other.name == refer)
2567                                    {
2568                                        return Some(&outer.collected[i]);
2569                                    }
2570                                }
2571                                None
2572                            });
2573                        match referenced {
2574                            None => {
2575                                self.report_at(
2576                                    ValidationKind::Other,
2577                                    format!(
2578                                        "<xs:keyref {:?}>: refer={refer} not found in scope",
2579                                        c.name.local
2580                                    ),
2581                                    off,
2582                                );
2583                                continue;
2584                            }
2585                            // XSD §3.11.4 / cvc-identity-constraint.4.3:
2586                            // a keyref tuple with any missing field
2587                            // produces no obligation — the constraint
2588                            // only applies when every field selects a
2589                            // value.  Skip incomplete tuples.
2590                            Some(target) => tuples.iter()
2591                                .filter(|t| t.iter().all(|f| f.is_some()))
2592                                .filter(|t| !target.contains(t))
2593                                .cloned()
2594                                .collect(),
2595                        }
2596                    };
2597                    for t in dangling {
2598                        let pretty = format_tuple(&t);
2599                        self.report_at(
2600                            ValidationKind::KeyRefDangling,
2601                            format!(
2602                                "<xs:keyref {:?}>: value {pretty} has no matching key",
2603                                c.name.local
2604                            ),
2605                            off,
2606                        );
2607                    }
2608                }
2609            }
2610        }
2611    }
2612}
2613
2614// ── xsi:type derivation check ────────────────────────────────────────────────
2615
2616impl<'s, 'x, E: XsdEventSource<'x>> Validator<'s, 'x, E> {
2617    /// Walk `child`'s derivation chain looking for `ancestor`.  Returns
2618    /// `Some(methods)` when `child` transitively derives from
2619    /// `ancestor`, where `methods` is the union of
2620    /// [`DerivationMethod`]s encountered along the chain (empty when
2621    /// `child == ancestor`, signalling identity).  Returns `None`
2622    /// when `child` does not derive from `ancestor`.
2623    ///
2624    /// Used by xsi:type processing to gate against the XSD 1.0 §3.4.6
2625    /// substitution rules.  Caller checks the returned method set
2626    /// against the element's `block=` and the declared type's
2627    /// `final=` to decide whether the substitution is permitted.
2628    ///
2629    /// ## UNRESOLVED placeholder bases
2630    ///
2631    /// The parser leaves `derivation.base` as an `UNRESOLVED:` Simple
2632    /// placeholder for user-defined base types (so it doesn't have to
2633    /// topo-sort the type table at compile time).  We resolve each
2634    /// step lazily here via [`Validator::resolve_type`] before
2635    /// comparing or descending.
2636    ///
2637    /// ## Simple types
2638    ///
2639    /// Simple types in v1 don't carry an explicit derivation chain —
2640    /// we only know the ultimate built-in and any facet layer.  Two
2641    /// simple types sharing the same built-in are treated as derived
2642    /// (by restriction) for the purposes of this check.  This is a
2643    /// sound over-approximation: the worst case is that we accept an
2644    /// xsi:type override that should have been rejected for a
2645    /// derivation reason we can't see.  The override's facets are
2646    /// still validated.
2647    fn derivation_methods_from(&self, child: &TypeRef, ancestor: &TypeRef) -> Option<BlockSet> {
2648        let child = self.resolve_type(child);
2649        let ancestor = self.resolve_type(ancestor);
2650        if type_refs_equal(&child, &ancestor) {
2651            return Some(BlockSet::empty());
2652        }
2653        // XSD §3.4.7 / §3.16.7 — every type derives (transitively) from
2654        // `xs:anyType`, and every simple type also derives from
2655        // `xs:anySimpleType`.  Neither is one of the 44 enumerated
2656        // built-ins, so the chain walk below can't see them; short-circuit
2657        // here.  The derivation is restriction in both directions:
2658        // anySimpleType is the restriction of anyType to simple content,
2659        // and every specific simple type is a restriction of anySimpleType.
2660        if is_any_type(&ancestor) {
2661            return Some(BlockSet::RESTRICTION);
2662        }
2663        if is_any_simple_type(&ancestor) {
2664            return match &child {
2665                TypeRef::Simple(_)  => Some(BlockSet::RESTRICTION),
2666                TypeRef::Complex(_) => None,
2667            };
2668        }
2669        match &child {
2670            TypeRef::Complex(ct) => {
2671                let mut methods = BlockSet::empty();
2672                let mut cur: Arc<ComplexType> = ct.clone();
2673                // Bound the walk in case of malformed schemas with
2674                // cycles — real chains in practice are 2-5 deep.
2675                for _ in 0..64 {
2676                    let d = cur.derivation.as_ref()?;
2677                    methods |= match d.method {
2678                        DerivationMethod::Restriction => BlockSet::RESTRICTION,
2679                        DerivationMethod::Extension   => BlockSet::EXTENSION,
2680                    };
2681                    let resolved_base = self.resolve_type(&d.base);
2682                    if type_refs_equal(&resolved_base, &ancestor) {
2683                        return Some(methods);
2684                    }
2685                    match resolved_base {
2686                        TypeRef::Complex(next) => { cur = next; }
2687                        TypeRef::Simple(_)     => return None,
2688                    }
2689                }
2690                None
2691            }
2692            TypeRef::Simple(s_child) => {
2693                if let TypeRef::Simple(s_anc) = &ancestor {
2694                    // Built-in derivation per XSD §3.16: every chain
2695                    // between built-ins is restriction.
2696                    if s_child.builtin.derives_from(s_anc.builtin) {
2697                        return Some(BlockSet::RESTRICTION);
2698                    }
2699                    // Named simple types: derives-from is determined
2700                    // by traversing the user simple type's name.
2701                    if s_child.name.as_deref() == s_anc.name.as_deref()
2702                        && s_child.name.is_some()
2703                    {
2704                        return Some(BlockSet::empty());
2705                    }
2706                    // Union membership: an xsi:type that's one of the
2707                    // ancestor's union members is a valid restriction
2708                    // (per cvc-elt-2 the xsi:type's value space ⊆ the
2709                    // declared union's value space).
2710                    use super::types::Variety;
2711                    if let Variety::Union { members } = &s_anc.variety {
2712                        for m in members {
2713                            if self.derivation_methods_from(
2714                                &TypeRef::Simple(s_child.clone()),
2715                                &TypeRef::Simple(m.clone()),
2716                            ).is_some() {
2717                                return Some(BlockSet::RESTRICTION);
2718                            }
2719                        }
2720                    }
2721                }
2722                None
2723            }
2724        }
2725    }
2726}
2727
2728/// True if `t` represents `xs:anyType` — either the synthesised complex
2729/// type built by [`any_type_ref`] or an unresolved placeholder produced
2730/// for `type="xs:anyType"` on a declaration the parser couldn't fold in.
2731fn is_any_type(t: &TypeRef) -> bool {
2732    match t {
2733        TypeRef::Complex(ct) => ct.name.as_ref()
2734            .is_some_and(|n| n.namespace.as_deref() == Some(QName::XSD_NS)
2735                          && n.local.as_ref() == "anyType"),
2736        TypeRef::Simple(st) => is_xsd_placeholder(st, "anyType"),
2737    }
2738}
2739
2740/// True if `t` represents `xs:anySimpleType`.  The schema parser
2741/// currently emits an UNRESOLVED simple placeholder for it (no top-level
2742/// declaration exists to patch over), so most callers see it through the
2743/// placeholder shape.
2744/// True if `t` represents `xs:anySimpleType`.  Three shapes can show
2745/// up at this site: the UNRESOLVED placeholder the parser emits for a
2746/// reference (`type="xs:anySimpleType"`), the resolved built-in with
2747/// `name="anySimpleType"` returned from [`lookup_type_by_qname`], or
2748/// an anonymous SimpleType whose `builtin` is `AnySimpleType` (the
2749/// shape the parser produces for an inline `<xs:element type=
2750/// "xs:anySimpleType"/>` declaration — `name` is unset).
2751fn is_any_simple_type(t: &TypeRef) -> bool {
2752    match t {
2753        TypeRef::Simple(st) => is_xsd_placeholder(st, "anySimpleType")
2754            || st.name.as_deref() == Some("anySimpleType")
2755            || matches!(st.builtin, super::types::BuiltinType::AnySimpleType),
2756        _ => false,
2757    }
2758}
2759
2760fn is_xsd_placeholder(st: &Arc<SimpleType>, local: &str) -> bool {
2761    st.name.as_deref()
2762        .and_then(|n| n.strip_prefix("UNRESOLVED:"))
2763        .map(parse_unresolved_marker)
2764        .is_some_and(|qn| qn.namespace.as_deref() == Some(QName::XSD_NS)
2765                      && qn.local.as_ref() == local)
2766}
2767
2768/// Identity for type references: same Arc, or — when independent
2769/// SimpleType wrappers are constructed for the same built-in (e.g.
2770/// `lookup_type_by_qname` produces fresh wrappers for `xs:int`
2771/// queries) — same name + same built-in.
2772fn type_refs_equal(a: &TypeRef, b: &TypeRef) -> bool {
2773    match (a, b) {
2774        (TypeRef::Complex(x), TypeRef::Complex(y)) => {
2775            Arc::ptr_eq(x, y) || (x.name.is_some() && x.name == y.name)
2776        }
2777        (TypeRef::Simple(x), TypeRef::Simple(y)) => {
2778            Arc::ptr_eq(x, y)
2779                || (x.builtin == y.builtin && x.name == y.name)
2780        }
2781        _ => false,
2782    }
2783}
2784
2785/// Render a [`BlockSet`] as a human-friendly token list for
2786/// inclusion in error messages.
2787fn format_block_set(b: BlockSet) -> String {
2788    let mut parts: Vec<&str> = Vec::new();
2789    if b.contains(BlockSet::RESTRICTION)  { parts.push("restriction"); }
2790    if b.contains(BlockSet::EXTENSION)    { parts.push("extension"); }
2791    if b.contains(BlockSet::SUBSTITUTION) { parts.push("substitution"); }
2792    parts.join(" ")
2793}
2794
2795/// True if `fp` is the trivial `.` (current-element text) field
2796/// path — the only shape for which we currently know how to type-
2797/// canonicalise the collected value.  Attribute / nested-child
2798/// fields would need per-step type lookup against the schema's
2799/// attribute uses or descendant element decls (a future extension);
2800/// they fall through to raw string comparison meanwhile.
2801fn field_path_is_dot(fp: &super::identity::FieldPath) -> bool {
2802    fp.paths.iter().all(|p| p.steps.is_empty())
2803}
2804
2805/// Canonicalise a raw lexical field value into a key fragment that
2806/// compares per the field type's value-space equality (XSD 1.0
2807/// §3.11.4 cvc-identity-constraint.4.2.2).  Two values are
2808/// equal iff their canonical key fragments are equal.
2809///
2810/// Strategy: tag with the type's PRIMITIVE built-in so values of
2811/// different primitive families never collide (boolean(1) ≠
2812/// decimal(1)), then format the parsed value canonically so two
2813/// lexically different but value-equal entries collide
2814/// (decimal("1") == decimal("1.0")).  Unparseable values fall back
2815/// to the raw lexical with the primitive tag — they're already
2816/// either invalid (and surfaced elsewhere) or simply unsupported
2817/// in the typed-value layer here, in which case lex equality is
2818/// the conservative answer.  Types we can't pin to a simple
2819/// builtin (raw Complex with non-simple content) pass through
2820/// untagged so the prior behaviour is preserved.
2821fn canonical_field_key(raw: &str, simple_type: Option<&Arc<SimpleType>>) -> String {
2822    use super::types::{BuiltinType, Value, parse_lexical};
2823    let Some(st) = simple_type else { return raw.to_string(); };
2824    let prim = st.builtin.primitive();
2825    let tag = prim.name();
2826    // Apply the type's whitespace facet to match the parser's view.
2827    let prepared: std::borrow::Cow<'_, str> = match st.whitespace {
2828        super::WhitespaceMode::Preserve => std::borrow::Cow::Borrowed(raw),
2829        super::WhitespaceMode::Replace  =>
2830            std::borrow::Cow::Owned(raw.chars().map(|c| match c {
2831                '\t' | '\n' | '\r' => ' ',
2832                _ => c,
2833            }).collect()),
2834        super::WhitespaceMode::Collapse =>
2835            std::borrow::Cow::Owned(raw.split_whitespace().collect::<Vec<_>>().join(" ")),
2836    };
2837    let parsed = parse_lexical(st.builtin, &prepared);
2838    let body: String = match parsed {
2839        Ok(Value::Bool(b))        => if b { "true".into() } else { "false".into() },
2840        Ok(Value::Decimal(d))     => d.normalize().to_string(),
2841        Ok(Value::Int(n))         => n.to_string(),
2842        Ok(Value::BigInt(b))      => {
2843            let sign = if b.negative { "-" } else { "" };
2844            format!("{sign}{}", b.digits)
2845        }
2846        Ok(Value::Float(f))       => format!("{f:?}"),
2847        Ok(Value::Double(d))      => format!("{d:?}"),
2848        Ok(Value::String(s))      => s,
2849        Ok(Value::Token(t))       => t,
2850        Ok(Value::Bytes(bs))      => bs.iter().map(|b| format!("{b:02X}")).collect(),
2851        Ok(Value::DateTime(dt))   => format!("{dt:?}"),
2852        Ok(Value::Date(d))        => format!("{d:?}"),
2853        Ok(Value::Time(t))        => format!("{t:?}"),
2854        Ok(Value::GYearMonth(g))  => format!("{g:?}"),
2855        Ok(Value::GYear(g))       => format!("{g:?}"),
2856        Ok(Value::GMonthDay(g))   => format!("{g:?}"),
2857        Ok(Value::GDay(g))        => format!("{g:?}"),
2858        Ok(Value::GMonth(g))      => format!("{g:?}"),
2859        Ok(Value::Duration(d))    => format!("{d:?}"),
2860        // Parse failure — fall through to raw under the same tag so
2861        // an invalid value still compares against itself coherently.
2862        Err(_)                    => prepared.to_string(),
2863    };
2864    let _ = BuiltinType::AnySimpleType; // silence unused warning if path skips Ok arms
2865    format!("{tag}:{body}")
2866}
2867
2868fn format_tuple(t: &KeyTuple) -> String {
2869    let mut s = String::from("(");
2870    for (i, v) in t.iter().enumerate() {
2871        if i > 0 { s.push_str(", "); }
2872        match v {
2873            Some(v) => { s.push('"'); s.push_str(v); s.push('"'); }
2874            None    => s.push_str("<null>"),
2875        }
2876    }
2877    s.push(')');
2878    s
2879}
2880
2881// ── tests ────────────────────────────────────────────────────────────────────
2882
2883#[cfg(test)]
2884mod tests {
2885    use super::*;
2886
2887    fn xsd_str(extra_decls: &str) -> String {
2888        format!(
2889            r#"<?xml version="1.0"?>
2890<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
2891           targetNamespace="urn:test"
2892           xmlns="urn:test"
2893           elementFormDefault="qualified">
2894{extra_decls}
2895</xs:schema>"#
2896        )
2897    }
2898
2899    fn instance_ns() -> &'static str { r#"xmlns="urn:test""# }
2900
2901    #[test]
2902    fn validate_simple_string() {
2903        let s = Schema::compile_str(&xsd_str(
2904            r#"<xs:element name="msg" type="xs:string"/>"#
2905        )).unwrap();
2906        s.validate_str(&format!(r#"<msg {ns}>hello</msg>"#, ns = instance_ns())).unwrap();
2907    }
2908
2909    #[test]
2910    fn validate_typed_int_value() {
2911        let s = Schema::compile_str(&xsd_str(
2912            r#"<xs:element name="age" type="xs:int"/>"#
2913        )).unwrap();
2914        assert!(s.validate_str(&format!(r#"<age {}>42</age>"#, instance_ns())).is_ok());
2915        assert!(s.validate_str(&format!(r#"<age {}>not-an-int</age>"#, instance_ns())).is_err());
2916    }
2917
2918    #[test]
2919    fn validate_facet_pattern() {
2920        let s = Schema::compile_str(&xsd_str(r#"
2921            <xs:element name="zip" type="ZipCode"/>
2922            <xs:simpleType name="ZipCode">
2923                <xs:restriction base="xs:string">
2924                    <xs:pattern value="\d{5}(-\d{4})?"/>
2925                </xs:restriction>
2926            </xs:simpleType>
2927        "#)).unwrap();
2928        let ns = instance_ns();
2929        assert!(s.validate_str(&format!(r#"<zip {ns}>12345</zip>"#)).is_ok());
2930        assert!(s.validate_str(&format!(r#"<zip {ns}>12345-6789</zip>"#)).is_ok());
2931        assert!(s.validate_str(&format!(r#"<zip {ns}>not-a-zip</zip>"#)).is_err());
2932    }
2933
2934    #[test]
2935    fn validate_complex_sequence() {
2936        let s = Schema::compile_str(&xsd_str(r#"
2937            <xs:element name="person" type="Person"/>
2938            <xs:complexType name="Person">
2939                <xs:sequence>
2940                    <xs:element name="name" type="xs:string"/>
2941                    <xs:element name="age"  type="xs:int"/>
2942                </xs:sequence>
2943            </xs:complexType>
2944        "#)).unwrap();
2945        let ns = instance_ns();
2946        assert!(s.validate_str(&format!(
2947            r#"<person {ns}><name>Ada</name><age>30</age></person>"#
2948        )).is_ok());
2949        // Wrong order.
2950        assert!(s.validate_str(&format!(
2951            r#"<person {ns}><age>30</age><name>Ada</name></person>"#
2952        )).is_err());
2953    }
2954
2955    #[test]
2956    fn validate_required_attribute() {
2957        let s = Schema::compile_str(&xsd_str(r#"
2958            <xs:element name="thing" type="Thing"/>
2959            <xs:complexType name="Thing">
2960                <xs:attribute name="id" type="xs:int" use="required"/>
2961            </xs:complexType>
2962        "#)).unwrap();
2963        let ns = instance_ns();
2964        assert!(s.validate_str(&format!(r#"<thing {ns} id="1"/>"#)).is_ok());
2965        assert!(s.validate_str(&format!(r#"<thing {ns}/>"#)).is_err());
2966    }
2967
2968    #[test]
2969    fn validate_attribute_type() {
2970        let s = Schema::compile_str(&xsd_str(r#"
2971            <xs:element name="thing" type="Thing"/>
2972            <xs:complexType name="Thing">
2973                <xs:attribute name="age" type="xs:int" use="required"/>
2974            </xs:complexType>
2975        "#)).unwrap();
2976        let ns = instance_ns();
2977        assert!(s.validate_str(&format!(r#"<thing {ns} age="not-int"/>"#)).is_err());
2978    }
2979
2980    #[test]
2981    fn validate_max_occurs() {
2982        let s = Schema::compile_str(&xsd_str(r#"
2983            <xs:element name="items" type="Items"/>
2984            <xs:complexType name="Items">
2985                <xs:sequence>
2986                    <xs:element name="item" type="xs:string" maxOccurs="3"/>
2987                </xs:sequence>
2988            </xs:complexType>
2989        "#)).unwrap();
2990        let ns = instance_ns();
2991        assert!(s.validate_str(&format!(
2992            r#"<items {ns}><item>a</item><item>b</item><item>c</item></items>"#
2993        )).is_ok());
2994        assert!(s.validate_str(&format!(
2995            r#"<items {ns}><item>a</item><item>b</item><item>c</item><item>d</item></items>"#
2996        )).is_err());
2997    }
2998
2999    #[test]
3000    fn validate_min_occurs() {
3001        let s = Schema::compile_str(&xsd_str(r#"
3002            <xs:element name="items" type="Items"/>
3003            <xs:complexType name="Items">
3004                <xs:sequence>
3005                    <xs:element name="item" type="xs:string"
3006                                minOccurs="2" maxOccurs="unbounded"/>
3007                </xs:sequence>
3008            </xs:complexType>
3009        "#)).unwrap();
3010        let ns = instance_ns();
3011        assert!(s.validate_str(&format!(
3012            r#"<items {ns}><item>a</item><item>b</item></items>"#
3013        )).is_ok());
3014        assert!(s.validate_str(&format!(
3015            r#"<items {ns}><item>a</item></items>"#
3016        )).is_err());
3017    }
3018
3019    #[test]
3020    fn validate_unbounded() {
3021        let s = Schema::compile_str(&xsd_str(r#"
3022            <xs:element name="items" type="Items"/>
3023            <xs:complexType name="Items">
3024                <xs:sequence>
3025                    <xs:element name="item" type="xs:string" maxOccurs="unbounded"/>
3026                </xs:sequence>
3027            </xs:complexType>
3028        "#)).unwrap();
3029        let ns = instance_ns();
3030        let mut xml = format!(r#"<items {ns}>"#);
3031        for _ in 0..50 { xml.push_str("<item>x</item>"); }
3032        xml.push_str("</items>");
3033        assert!(s.validate_str(&xml).is_ok());
3034    }
3035
3036    #[test]
3037    fn validate_choice() {
3038        let s = Schema::compile_str(&xsd_str(r#"
3039            <xs:element name="either" type="Either"/>
3040            <xs:complexType name="Either">
3041                <xs:choice>
3042                    <xs:element name="left"  type="xs:int"/>
3043                    <xs:element name="right" type="xs:string"/>
3044                </xs:choice>
3045            </xs:complexType>
3046        "#)).unwrap();
3047        let ns = instance_ns();
3048        assert!(s.validate_str(&format!(r#"<either {ns}><left>1</left></either>"#)).is_ok());
3049        assert!(s.validate_str(&format!(r#"<either {ns}><right>hi</right></either>"#)).is_ok());
3050        // No branch chosen.
3051        assert!(s.validate_str(&format!(r#"<either {ns}/>"#)).is_err());
3052    }
3053
3054    #[test]
3055    fn validate_all_any_order() {
3056        let s = Schema::compile_str(&xsd_str(r#"
3057            <xs:element name="both" type="Both"/>
3058            <xs:complexType name="Both">
3059                <xs:all>
3060                    <xs:element name="a" type="xs:int"/>
3061                    <xs:element name="b" type="xs:int"/>
3062                </xs:all>
3063            </xs:complexType>
3064        "#)).unwrap();
3065        let ns = instance_ns();
3066        assert!(s.validate_str(&format!(r#"<both {ns}><a>1</a><b>2</b></both>"#)).is_ok());
3067        assert!(s.validate_str(&format!(r#"<both {ns}><b>2</b><a>1</a></both>"#)).is_ok());
3068        // Missing one of the two.
3069        assert!(s.validate_str(&format!(r#"<both {ns}><a>1</a></both>"#)).is_err());
3070    }
3071
3072    #[test]
3073    fn validate_xsi_nil() {
3074        let s = Schema::compile_str(&xsd_str(
3075            r#"<xs:element name="opt" type="xs:int" nillable="true"/>"#
3076        )).unwrap();
3077        let inst = format!(
3078            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3079            ns = instance_ns(), xsi = XSI_NS,
3080        );
3081        assert!(s.validate_str(&inst).is_ok());
3082    }
3083
3084    #[test]
3085    fn validate_xsi_nil_on_non_nillable_fails() {
3086        let s = Schema::compile_str(&xsd_str(
3087            r#"<xs:element name="opt" type="xs:int"/>"#
3088        )).unwrap();
3089        let inst = format!(
3090            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3091            ns = instance_ns(), xsi = XSI_NS,
3092        );
3093        assert!(s.validate_str(&inst).is_err());
3094    }
3095
3096    #[test]
3097    fn validate_unknown_root_fails() {
3098        let s = Schema::compile_str(&xsd_str(
3099            r#"<xs:element name="known" type="xs:string"/>"#
3100        )).unwrap();
3101        let ns = instance_ns();
3102        assert!(s.validate_str(&format!(r#"<unknown {ns}>x</unknown>"#)).is_err());
3103    }
3104
3105    #[test]
3106    fn validate_collects_multiple_issues_when_not_fail_fast() {
3107        let s = Schema::compile_str(&xsd_str(r#"
3108            <xs:element name="thing" type="Thing"/>
3109            <xs:complexType name="Thing">
3110                <xs:attribute name="id"   type="xs:int"    use="required"/>
3111                <xs:attribute name="name" type="xs:string" use="required"/>
3112            </xs:complexType>
3113        "#)).unwrap();
3114        let inst = format!(r#"<thing {}/>"#, instance_ns());
3115        let opts = ValidationOptions { fail_fast: false, max_issues: 100, ..Default::default() };
3116        let err = s.validate_str_opts(&inst, opts).unwrap_err();
3117        assert!(err.issues.len() >= 2);
3118    }
3119
3120    // ── identity constraints ─────────────────────────────────────────
3121
3122    fn parts_xsd() -> String {
3123        // Catalog with `xs:key` on part numbers and `xs:keyref` on
3124        // line items referring to those parts — the canonical XSD
3125        // identity-constraint example.
3126        xsd_str(r#"
3127            <xs:element name="catalog">
3128                <xs:complexType>
3129                    <xs:sequence>
3130                        <xs:element name="parts">
3131                            <xs:complexType>
3132                                <xs:sequence>
3133                                    <xs:element name="part" maxOccurs="unbounded">
3134                                        <xs:complexType>
3135                                            <xs:attribute name="num" type="xs:string" use="required"/>
3136                                        </xs:complexType>
3137                                    </xs:element>
3138                                </xs:sequence>
3139                            </xs:complexType>
3140                        </xs:element>
3141                        <xs:element name="orders">
3142                            <xs:complexType>
3143                                <xs:sequence>
3144                                    <xs:element name="line" maxOccurs="unbounded">
3145                                        <xs:complexType>
3146                                            <xs:attribute name="part" type="xs:string" use="required"/>
3147                                        </xs:complexType>
3148                                    </xs:element>
3149                                </xs:sequence>
3150                            </xs:complexType>
3151                        </xs:element>
3152                    </xs:sequence>
3153                </xs:complexType>
3154                <xs:key name="partKey">
3155                    <xs:selector xpath=".//part"/>
3156                    <xs:field xpath="@num"/>
3157                </xs:key>
3158                <xs:keyref name="lineRef" refer="partKey">
3159                    <xs:selector xpath=".//line"/>
3160                    <xs:field xpath="@part"/>
3161                </xs:keyref>
3162            </xs:element>
3163        "#)
3164    }
3165
3166    #[test]
3167    fn xs_key_accepts_unique_values() {
3168        let s = Schema::compile_str(&parts_xsd()).unwrap();
3169        let xml = format!(
3170            r#"<catalog {ns}>
3171                <parts>
3172                    <part num="A1"/><part num="A2"/><part num="A3"/>
3173                </parts>
3174                <orders>
3175                    <line part="A1"/><line part="A3"/>
3176                </orders>
3177            </catalog>"#,
3178            ns = instance_ns(),
3179        );
3180        s.validate_str(&xml).unwrap();
3181    }
3182
3183    #[test]
3184    fn xs_key_rejects_duplicates() {
3185        let s = Schema::compile_str(&parts_xsd()).unwrap();
3186        let xml = format!(
3187            r#"<catalog {ns}>
3188                <parts>
3189                    <part num="A1"/><part num="A1"/>
3190                </parts>
3191                <orders>
3192                    <line part="A1"/>
3193                </orders>
3194            </catalog>"#,
3195            ns = instance_ns(),
3196        );
3197        let err = s.validate_str(&xml).unwrap_err();
3198        assert!(err.issues.iter().any(|i|
3199            matches!(i.kind, ValidationKind::KeyNotUnique)
3200        ), "expected KeyNotUnique, got {:?}", err.issues);
3201    }
3202
3203    #[test]
3204    fn xs_keyref_rejects_dangling() {
3205        let s = Schema::compile_str(&parts_xsd()).unwrap();
3206        let xml = format!(
3207            r#"<catalog {ns}>
3208                <parts>
3209                    <part num="A1"/>
3210                </parts>
3211                <orders>
3212                    <line part="UNKNOWN"/>
3213                </orders>
3214            </catalog>"#,
3215            ns = instance_ns(),
3216        );
3217        let err = s.validate_str(&xml).unwrap_err();
3218        assert!(err.issues.iter().any(|i|
3219            matches!(i.kind, ValidationKind::KeyRefDangling)
3220        ), "expected KeyRefDangling, got {:?}", err.issues);
3221    }
3222
3223    #[test]
3224    fn xs_unique_allows_missing_field() {
3225        // xs:unique tolerates missing fields (where xs:key would error).
3226        let s = Schema::compile_str(&xsd_str(r#"
3227            <xs:element name="root">
3228                <xs:complexType>
3229                    <xs:sequence>
3230                        <xs:element name="x" maxOccurs="unbounded">
3231                            <xs:complexType>
3232                                <xs:attribute name="id" type="xs:string"/>
3233                            </xs:complexType>
3234                        </xs:element>
3235                    </xs:sequence>
3236                </xs:complexType>
3237                <xs:unique name="xUnique">
3238                    <xs:selector xpath=".//x"/>
3239                    <xs:field xpath="@id"/>
3240                </xs:unique>
3241            </xs:element>
3242        "#)).unwrap();
3243        let xml = format!(
3244            r#"<root {ns}><x id="A"/><x/><x id="B"/></root>"#,
3245            ns = instance_ns(),
3246        );
3247        // Missing field tolerated; A and B are unique.
3248        s.validate_str(&xml).unwrap();
3249        // But duplicate present values still fail.
3250        let bad = format!(
3251            r#"<root {ns}><x id="A"/><x id="A"/></root>"#,
3252            ns = instance_ns(),
3253        );
3254        assert!(s.validate_str(&bad).is_err());
3255    }
3256
3257    // ── DFA-driven content matching ──────────────────────────────────
3258
3259    #[test]
3260    fn dfa_rejects_ambiguous_schema_at_compile_time() {
3261        // `(x?, x)` — when an `<x>` arrives, the parser can't tell
3262        // whether to consume the optional first one or skip to the
3263        // required second one without lookahead.  UPA violation.
3264        let xsd = xsd_str(r#"
3265            <xs:complexType name="Bad">
3266                <xs:sequence>
3267                    <xs:element name="x" type="xs:string" minOccurs="0"/>
3268                    <xs:element name="x" type="xs:string"/>
3269                </xs:sequence>
3270            </xs:complexType>
3271            <xs:element name="root" type="Bad"/>
3272        "#);
3273        let err = Schema::compile_str(&xsd).unwrap_err();
3274        assert!(err.message.contains("Unique Particle Attribution"),
3275            "expected UPA error, got {:?}", err.message);
3276    }
3277
3278    #[test]
3279    fn dfa_rejects_ambiguous_choice_at_compile_time() {
3280        // Two branches of a choice naming the same element.  Also UPA.
3281        let xsd = xsd_str(r#"
3282            <xs:element name="root">
3283                <xs:complexType>
3284                    <xs:choice>
3285                        <xs:element name="x" type="xs:string"/>
3286                        <xs:element name="x" type="xs:int"/>
3287                    </xs:choice>
3288                </xs:complexType>
3289            </xs:element>
3290        "#);
3291        assert!(Schema::compile_str(&xsd).is_err());
3292    }
3293
3294    #[test]
3295    fn dfa_error_message_lists_expected_elements() {
3296        // Missing required element should name the expected one.
3297        let s = Schema::compile_str(&xsd_str(r#"
3298            <xs:element name="root">
3299                <xs:complexType>
3300                    <xs:sequence>
3301                        <xs:element name="alpha" type="xs:string"/>
3302                        <xs:element name="beta"  type="xs:string"/>
3303                    </xs:sequence>
3304                </xs:complexType>
3305            </xs:element>
3306        "#)).unwrap();
3307        let xml = format!(r#"<root {ns}><alpha>x</alpha></root>"#, ns = instance_ns());
3308        let err = s.validate_str(&xml).unwrap_err();
3309        // Should mention `beta` as the missing element.
3310        assert!(err.issues.iter().any(|i| i.message.contains("beta")),
3311            "expected `beta` in error, got {:?}", err.issues);
3312    }
3313
3314    #[test]
3315    fn dfa_handles_substitution_groups() {
3316        // Substitution groups: any element substituting for `figure`
3317        // can appear where `figure` is expected.
3318        let s = Schema::compile_str(&xsd_str(r#"
3319            <xs:element name="figure" type="xs:string" abstract="true"/>
3320            <xs:element name="image"  type="xs:string" substitutionGroup="figure"/>
3321            <xs:element name="chart"  type="xs:string" substitutionGroup="figure"/>
3322            <xs:element name="report">
3323                <xs:complexType>
3324                    <xs:sequence>
3325                        <xs:element ref="figure" maxOccurs="unbounded"/>
3326                    </xs:sequence>
3327                </xs:complexType>
3328            </xs:element>
3329        "#)).unwrap();
3330        let xml = format!(
3331            r#"<report {ns}><image>i</image><chart>c</chart><image>j</image></report>"#,
3332            ns = instance_ns(),
3333        );
3334        s.validate_str(&xml).unwrap();
3335    }
3336
3337    #[test]
3338    fn dfa_unbounded_works_correctly() {
3339        // The DFA self-loop at maxOccurs=unbounded should accept any
3340        // count from min upward.
3341        let s = Schema::compile_str(&xsd_str(r#"
3342            <xs:element name="items">
3343                <xs:complexType>
3344                    <xs:sequence>
3345                        <xs:element name="item" type="xs:int" maxOccurs="unbounded"/>
3346                    </xs:sequence>
3347                </xs:complexType>
3348            </xs:element>
3349        "#)).unwrap();
3350        let ns = instance_ns();
3351        for n in [1, 5, 100, 1000] {
3352            let mut xml = format!(r#"<items {ns}>"#);
3353            for i in 0..n { xml.push_str(&format!("<item>{i}</item>")); }
3354            xml.push_str("</items>");
3355            s.validate_str(&xml)
3356                .unwrap_or_else(|e| panic!("n={n} should validate: {e:?}"));
3357        }
3358    }
3359
3360    #[test]
3361    fn dfa_accepts_optional_followed_by_required() {
3362        // Optional first (minOccurs=0) then required — the tricky
3363        // case where the DFA's initial state must accept either the
3364        // optional element or skip directly to the required one.
3365        let s = Schema::compile_str(&xsd_str(r#"
3366            <xs:element name="msg">
3367                <xs:complexType>
3368                    <xs:sequence>
3369                        <xs:element name="header" type="xs:string" minOccurs="0"/>
3370                        <xs:element name="body"   type="xs:string"/>
3371                    </xs:sequence>
3372                </xs:complexType>
3373            </xs:element>
3374        "#)).unwrap();
3375        let ns = instance_ns();
3376        s.validate_str(&format!(
3377            r#"<msg {ns}><header>h</header><body>b</body></msg>"#)).unwrap();
3378        s.validate_str(&format!(
3379            r#"<msg {ns}><body>b</body></msg>"#)).unwrap();
3380        // Just header with no body fails.
3381        assert!(s.validate_str(&format!(
3382            r#"<msg {ns}><header>h</header></msg>"#)).is_err());
3383    }
3384
3385    #[test]
3386    fn xs_key_with_child_attribute_field() {
3387        // Field path is `id/@value` — descend into the `id` child of
3388        // the matched element, then read its `@value` attribute.
3389        // This is a common shape (e.g. SAML assertions key on
3390        // <NameID> attributes inside subjects).
3391        let s = Schema::compile_str(&xsd_str(r#"
3392            <xs:element name="users">
3393                <xs:complexType>
3394                    <xs:sequence>
3395                        <xs:element name="user" maxOccurs="unbounded">
3396                            <xs:complexType>
3397                                <xs:sequence>
3398                                    <xs:element name="id">
3399                                        <xs:complexType>
3400                                            <xs:attribute name="value" type="xs:string" use="required"/>
3401                                        </xs:complexType>
3402                                    </xs:element>
3403                                </xs:sequence>
3404                            </xs:complexType>
3405                        </xs:element>
3406                    </xs:sequence>
3407                </xs:complexType>
3408                <xs:key name="userKey">
3409                    <xs:selector xpath=".//user"/>
3410                    <xs:field xpath="id/@value"/>
3411                </xs:key>
3412            </xs:element>
3413        "#)).unwrap();
3414        let ok = format!(
3415            r#"<users {ns}>
3416                 <user><id value="alice"/></user>
3417                 <user><id value="bob"/></user>
3418               </users>"#,
3419            ns = instance_ns(),
3420        );
3421        s.validate_str(&ok).unwrap();
3422        let dup = format!(
3423            r#"<users {ns}>
3424                 <user><id value="alice"/></user>
3425                 <user><id value="alice"/></user>
3426               </users>"#,
3427            ns = instance_ns(),
3428        );
3429        let err = s.validate_str(&dup).unwrap_err();
3430        assert!(err.issues.iter().any(|i|
3431            matches!(i.kind, ValidationKind::KeyNotUnique)
3432        ), "expected KeyNotUnique, got {:?}", err.issues);
3433    }
3434
3435    #[test]
3436    fn xs_key_with_child_text_field() {
3437        // Field path is `name` — read the text content of the `name`
3438        // child of the matched element.  (Equivalent to `name/.` but
3439        // omitting the trailing dot is the common form.)
3440        let s = Schema::compile_str(&xsd_str(r#"
3441            <xs:element name="users">
3442                <xs:complexType>
3443                    <xs:sequence>
3444                        <xs:element name="user" maxOccurs="unbounded">
3445                            <xs:complexType>
3446                                <xs:sequence>
3447                                    <xs:element name="name" type="xs:string"/>
3448                                </xs:sequence>
3449                            </xs:complexType>
3450                        </xs:element>
3451                    </xs:sequence>
3452                </xs:complexType>
3453                <xs:key name="userKey">
3454                    <xs:selector xpath=".//user"/>
3455                    <xs:field xpath="name"/>
3456                </xs:key>
3457            </xs:element>
3458        "#)).unwrap();
3459        let ok = format!(
3460            r#"<users {ns}>
3461                 <user><name>alice</name></user>
3462                 <user><name>bob</name></user>
3463               </users>"#,
3464            ns = instance_ns(),
3465        );
3466        s.validate_str(&ok).unwrap();
3467        let dup = format!(
3468            r#"<users {ns}>
3469                 <user><name>alice</name></user>
3470                 <user><name>alice</name></user>
3471               </users>"#,
3472            ns = instance_ns(),
3473        );
3474        let err = s.validate_str(&dup).unwrap_err();
3475        assert!(err.issues.iter().any(|i|
3476            matches!(i.kind, ValidationKind::KeyNotUnique)
3477        ), "expected KeyNotUnique, got {:?}", err.issues);
3478    }
3479
3480    #[test]
3481    fn xs_key_text_field() {
3482        // Field uses `.` (text content) instead of @attr.
3483        let s = Schema::compile_str(&xsd_str(r#"
3484            <xs:element name="cities">
3485                <xs:complexType>
3486                    <xs:sequence>
3487                        <xs:element name="city" type="xs:string" maxOccurs="unbounded"/>
3488                    </xs:sequence>
3489                </xs:complexType>
3490                <xs:key name="cityKey">
3491                    <xs:selector xpath=".//city"/>
3492                    <xs:field xpath="."/>
3493                </xs:key>
3494            </xs:element>
3495        "#)).unwrap();
3496        let ok = format!(
3497            r#"<cities {ns}><city>Boston</city><city>NYC</city></cities>"#,
3498            ns = instance_ns(),
3499        );
3500        s.validate_str(&ok).unwrap();
3501        let bad = format!(
3502            r#"<cities {ns}><city>Boston</city><city>Boston</city></cities>"#,
3503            ns = instance_ns(),
3504        );
3505        assert!(s.validate_str(&bad).is_err());
3506    }
3507
3508    // ── deeper-than-two-step field paths ───────────────────────────
3509
3510    #[test]
3511    fn xs_key_with_grandchild_text_field() {
3512        // Field path is `name/first` — two child steps.  The matched
3513        // element is `user`; the value is the text content of
3514        // `user/name/first`.
3515        let s = Schema::compile_str(&xsd_str(r#"
3516            <xs:element name="users">
3517                <xs:complexType>
3518                    <xs:sequence>
3519                        <xs:element name="user" maxOccurs="unbounded">
3520                            <xs:complexType>
3521                                <xs:sequence>
3522                                    <xs:element name="name">
3523                                        <xs:complexType>
3524                                            <xs:sequence>
3525                                                <xs:element name="first" type="xs:string"/>
3526                                                <xs:element name="last"  type="xs:string"/>
3527                                            </xs:sequence>
3528                                        </xs:complexType>
3529                                    </xs:element>
3530                                </xs:sequence>
3531                            </xs:complexType>
3532                        </xs:element>
3533                    </xs:sequence>
3534                </xs:complexType>
3535                <xs:key name="userKey">
3536                    <xs:selector xpath=".//user"/>
3537                    <xs:field xpath="name/first"/>
3538                </xs:key>
3539            </xs:element>
3540        "#)).unwrap();
3541        let ok = format!(
3542            r#"<users {ns}>
3543                 <user><name><first>Alice</first><last>A</last></name></user>
3544                 <user><name><first>Bob</first><last>B</last></name></user>
3545               </users>"#,
3546            ns = instance_ns(),
3547        );
3548        s.validate_str(&ok).unwrap();
3549        let dup = format!(
3550            r#"<users {ns}>
3551                 <user><name><first>Alice</first><last>A</last></name></user>
3552                 <user><name><first>Alice</first><last>Z</last></name></user>
3553               </users>"#,
3554            ns = instance_ns(),
3555        );
3556        let err = s.validate_str(&dup).unwrap_err();
3557        assert!(err.issues.iter().any(|i|
3558            matches!(i.kind, ValidationKind::KeyNotUnique)
3559        ), "expected KeyNotUnique on duplicate name/first, got {:?}", err.issues);
3560    }
3561
3562    #[test]
3563    fn xs_key_with_grandchild_attribute_field() {
3564        // Three-step path ending in attribute: `name/first/@lang`.
3565        let s = Schema::compile_str(&xsd_str(r#"
3566            <xs:element name="users">
3567                <xs:complexType>
3568                    <xs:sequence>
3569                        <xs:element name="user" maxOccurs="unbounded">
3570                            <xs:complexType>
3571                                <xs:sequence>
3572                                    <xs:element name="name">
3573                                        <xs:complexType>
3574                                            <xs:sequence>
3575                                                <xs:element name="first">
3576                                                    <xs:complexType>
3577                                                        <xs:simpleContent>
3578                                                            <xs:extension base="xs:string">
3579                                                                <xs:attribute name="lang" type="xs:string"/>
3580                                                            </xs:extension>
3581                                                        </xs:simpleContent>
3582                                                    </xs:complexType>
3583                                                </xs:element>
3584                                            </xs:sequence>
3585                                        </xs:complexType>
3586                                    </xs:element>
3587                                </xs:sequence>
3588                            </xs:complexType>
3589                        </xs:element>
3590                    </xs:sequence>
3591                </xs:complexType>
3592                <xs:key name="userKey">
3593                    <xs:selector xpath=".//user"/>
3594                    <xs:field xpath="name/first/@lang"/>
3595                </xs:key>
3596            </xs:element>
3597        "#)).unwrap();
3598        let ok = format!(
3599            r#"<users {ns}>
3600                 <user><name><first lang="en">Alice</first></name></user>
3601                 <user><name><first lang="fr">Alphonse</first></name></user>
3602               </users>"#,
3603            ns = instance_ns(),
3604        );
3605        s.validate_str(&ok).unwrap();
3606        let dup = format!(
3607            r#"<users {ns}>
3608                 <user><name><first lang="en">Alice</first></name></user>
3609                 <user><name><first lang="en">Bob</first></name></user>
3610               </users>"#,
3611            ns = instance_ns(),
3612        );
3613        let err = s.validate_str(&dup).unwrap_err();
3614        assert!(err.issues.iter().any(|i|
3615            matches!(i.kind, ValidationKind::KeyNotUnique)
3616        ), "expected KeyNotUnique on duplicate @lang, got {:?}", err.issues);
3617    }
3618
3619    #[test]
3620    fn xs_keyref_with_deep_field() {
3621        // Both key and keyref use a 3-step child path ending in an
3622        // attribute: `header/meta/@sku`.  The keyref should resolve
3623        // against the deeply-extracted key values.
3624        let s = Schema::compile_str(&xsd_str(r#"
3625            <xs:element name="catalog">
3626                <xs:complexType>
3627                    <xs:sequence>
3628                        <xs:element name="parts">
3629                            <xs:complexType>
3630                                <xs:sequence>
3631                                    <xs:element name="part" maxOccurs="unbounded">
3632                                        <xs:complexType>
3633                                            <xs:sequence>
3634                                                <xs:element name="header">
3635                                                    <xs:complexType>
3636                                                        <xs:sequence>
3637                                                            <xs:element name="meta">
3638                                                                <xs:complexType>
3639                                                                    <xs:attribute name="sku" type="xs:string"/>
3640                                                                </xs:complexType>
3641                                                            </xs:element>
3642                                                        </xs:sequence>
3643                                                    </xs:complexType>
3644                                                </xs:element>
3645                                            </xs:sequence>
3646                                        </xs:complexType>
3647                                    </xs:element>
3648                                </xs:sequence>
3649                            </xs:complexType>
3650                        </xs:element>
3651                        <xs:element name="orders">
3652                            <xs:complexType>
3653                                <xs:sequence>
3654                                    <xs:element name="line" maxOccurs="unbounded">
3655                                        <xs:complexType>
3656                                            <xs:sequence>
3657                                                <xs:element name="header">
3658                                                    <xs:complexType>
3659                                                        <xs:sequence>
3660                                                            <xs:element name="ref">
3661                                                                <xs:complexType>
3662                                                                    <xs:attribute name="sku" type="xs:string"/>
3663                                                                </xs:complexType>
3664                                                            </xs:element>
3665                                                        </xs:sequence>
3666                                                    </xs:complexType>
3667                                                </xs:element>
3668                                            </xs:sequence>
3669                                        </xs:complexType>
3670                                    </xs:element>
3671                                </xs:sequence>
3672                            </xs:complexType>
3673                        </xs:element>
3674                    </xs:sequence>
3675                </xs:complexType>
3676                <xs:key name="partKey">
3677                    <xs:selector xpath=".//part"/>
3678                    <xs:field xpath="header/meta/@sku"/>
3679                </xs:key>
3680                <xs:keyref name="lineRef" refer="partKey">
3681                    <xs:selector xpath=".//line"/>
3682                    <xs:field xpath="header/ref/@sku"/>
3683                </xs:keyref>
3684            </xs:element>
3685        "#)).unwrap();
3686        let ok = format!(
3687            r#"<catalog {ns}>
3688                 <parts>
3689                   <part><header><meta sku="A1"/></header></part>
3690                   <part><header><meta sku="A2"/></header></part>
3691                 </parts>
3692                 <orders>
3693                   <line><header><ref sku="A1"/></header></line>
3694                   <line><header><ref sku="A2"/></header></line>
3695                 </orders>
3696               </catalog>"#,
3697            ns = instance_ns(),
3698        );
3699        s.validate_str(&ok).unwrap();
3700        let bad = format!(
3701            r#"<catalog {ns}>
3702                 <parts>
3703                   <part><header><meta sku="A1"/></header></part>
3704                 </parts>
3705                 <orders>
3706                   <line><header><ref sku="A1"/></header></line>
3707                   <line><header><ref sku="UNKNOWN"/></header></line>
3708                 </orders>
3709               </catalog>"#,
3710            ns = instance_ns(),
3711        );
3712        let err = s.validate_str(&bad).unwrap_err();
3713        assert!(err.issues.iter().any(|i|
3714            matches!(i.kind, ValidationKind::KeyRefDangling)
3715        ), "expected KeyRefDangling on unknown sku, got {:?}", err.issues);
3716    }
3717
3718    #[test]
3719    fn xs_unique_three_level_child_path() {
3720        // Three child steps (no trailing attribute): `a/b/c`.
3721        let s = Schema::compile_str(&xsd_str(r#"
3722            <xs:element name="root">
3723                <xs:complexType>
3724                    <xs:sequence>
3725                        <xs:element name="item" maxOccurs="unbounded">
3726                            <xs:complexType>
3727                                <xs:sequence>
3728                                    <xs:element name="a">
3729                                        <xs:complexType>
3730                                            <xs:sequence>
3731                                                <xs:element name="b">
3732                                                    <xs:complexType>
3733                                                        <xs:sequence>
3734                                                            <xs:element name="c" type="xs:string"/>
3735                                                        </xs:sequence>
3736                                                    </xs:complexType>
3737                                                </xs:element>
3738                                            </xs:sequence>
3739                                        </xs:complexType>
3740                                    </xs:element>
3741                                </xs:sequence>
3742                            </xs:complexType>
3743                        </xs:element>
3744                    </xs:sequence>
3745                </xs:complexType>
3746                <xs:unique name="itemUnique">
3747                    <xs:selector xpath=".//item"/>
3748                    <xs:field xpath="a/b/c"/>
3749                </xs:unique>
3750            </xs:element>
3751        "#)).unwrap();
3752        let ok = format!(
3753            r#"<root {ns}>
3754                 <item><a><b><c>x</c></b></a></item>
3755                 <item><a><b><c>y</c></b></a></item>
3756               </root>"#,
3757            ns = instance_ns(),
3758        );
3759        s.validate_str(&ok).unwrap();
3760        let dup = format!(
3761            r#"<root {ns}>
3762                 <item><a><b><c>x</c></b></a></item>
3763                 <item><a><b><c>x</c></b></a></item>
3764               </root>"#,
3765            ns = instance_ns(),
3766        );
3767        let err = s.validate_str(&dup).unwrap_err();
3768        assert!(err.issues.iter().any(|i|
3769            matches!(i.kind, ValidationKind::KeyNotUnique)
3770        ), "expected KeyNotUnique on duplicate a/b/c, got {:?}", err.issues);
3771    }
3772
3773    #[test]
3774    fn xs_key_deep_path_missing_intermediate_is_missing_field() {
3775        // <xs:key> with a 3-step path; if any intermediate child is
3776        // absent, the field is "missing" and (for xs:key) that's an
3777        // error.
3778        let s = Schema::compile_str(&xsd_str(r#"
3779            <xs:element name="users">
3780                <xs:complexType>
3781                    <xs:sequence>
3782                        <xs:element name="user" maxOccurs="unbounded">
3783                            <xs:complexType>
3784                                <xs:sequence>
3785                                    <xs:element name="name" minOccurs="0">
3786                                        <xs:complexType>
3787                                            <xs:sequence>
3788                                                <xs:element name="first" type="xs:string" minOccurs="0"/>
3789                                            </xs:sequence>
3790                                        </xs:complexType>
3791                                    </xs:element>
3792                                </xs:sequence>
3793                            </xs:complexType>
3794                        </xs:element>
3795                    </xs:sequence>
3796                </xs:complexType>
3797                <xs:key name="userKey">
3798                    <xs:selector xpath=".//user"/>
3799                    <xs:field xpath="name/first"/>
3800                </xs:key>
3801            </xs:element>
3802        "#)).unwrap();
3803        // Second <user> has no <name>, so its field is missing.
3804        // First <user> has the full path — its value must be
3805        // extracted, NOT also reported as missing.
3806        let bad = format!(
3807            r#"<users {ns}>
3808                 <user><name><first>Alice</first></name></user>
3809                 <user/>
3810               </users>"#,
3811            ns = instance_ns(),
3812        );
3813        let err = s.validate_str(&bad).unwrap_err();
3814        let missing: Vec<&ValidationIssue> = err.issues.iter()
3815            .filter(|i| i.message.contains("missing one of the field values"))
3816            .collect();
3817        // Exactly one missing-field error — proves the first user's
3818        // value WAS extracted (otherwise both would miss).
3819        assert_eq!(missing.len(), 1,
3820            "expected exactly one missing-field error, got {:?}", err.issues);
3821    }
3822
3823    // ── line/column locators on validation issues ──────────────────
3824
3825    #[test]
3826    fn issue_carries_line_and_column_for_simple_content() {
3827        let s = Schema::compile_str(&xsd_str(
3828            r#"<xs:element name="age" type="xs:int"/>"#
3829        )).unwrap();
3830        let bad = format!(r#"<age {}>not-an-int</age>"#, instance_ns());
3831        let err = s.validate_str(&bad).unwrap_err();
3832        let issue = &err.issues[0];
3833        assert_eq!(issue.line, Some(1), "single-line input, expected line 1, got {issue:?}");
3834        assert!(issue.column.is_some(), "expected column to be filled, got {issue:?}");
3835    }
3836
3837    #[test]
3838    fn issue_line_points_at_offending_element_in_multiline_input() {
3839        let s = Schema::compile_str(&xsd_str(r#"
3840            <xs:element name="r">
3841                <xs:complexType>
3842                    <xs:sequence>
3843                        <xs:element name="bad" type="xs:int"/>
3844                    </xs:sequence>
3845                </xs:complexType>
3846            </xs:element>
3847        "#)).unwrap();
3848        // <bad> sits on line 4 (after newlines on 1/2/3).
3849        let bad = format!("<r {}>\n  \n  \n  <bad>not-an-int</bad>\n</r>", instance_ns());
3850        let err = s.validate_str(&bad).unwrap_err();
3851        let issue = err.issues.iter()
3852            .find(|i| i.message.contains("element content"))
3853            .expect("expected element-content error");
3854        assert_eq!(issue.line, Some(4),
3855            "expected <bad> on line 4, got {issue:?}");
3856    }
3857
3858    #[test]
3859    fn issue_line_for_missing_required_element_points_at_parent() {
3860        let s = Schema::compile_str(&xsd_str(r#"
3861            <xs:element name="r">
3862                <xs:complexType>
3863                    <xs:sequence>
3864                        <xs:element name="x" type="xs:string"/>
3865                    </xs:sequence>
3866                </xs:complexType>
3867            </xs:element>
3868        "#)).unwrap();
3869        // <r> on line 2; missing required <x> should anchor at line 2.
3870        let bad = format!("\n<r {}>\n</r>", instance_ns());
3871        let err = s.validate_str(&bad).unwrap_err();
3872        let issue = err.issues.iter()
3873            .find(|i| i.message.contains("missing required element"))
3874            .expect("expected missing-required-element error");
3875        assert_eq!(issue.line, Some(2),
3876            "expected <r> on line 2, got {issue:?}");
3877    }
3878
3879    #[test]
3880    fn issue_line_for_unexpected_element_points_at_the_element_not_parent() {
3881        let s = Schema::compile_str(&xsd_str(r#"
3882            <xs:element name="r">
3883                <xs:complexType>
3884                    <xs:sequence>
3885                        <xs:element name="ok" type="xs:string"/>
3886                    </xs:sequence>
3887                </xs:complexType>
3888            </xs:element>
3889        "#)).unwrap();
3890        // <r> line 1; <bad> line 2.  The unexpected-element issue
3891        // must point at line 2, not line 1.
3892        let bad = format!("<r {}>\n  <bad/>\n</r>", instance_ns());
3893        let err = s.validate_str(&bad).unwrap_err();
3894        let issue = err.issues.iter()
3895            .find(|i| i.message.contains("unexpected element"))
3896            .expect("expected unexpected-element error");
3897        assert_eq!(issue.line, Some(2),
3898            "expected <bad> on line 2, got {issue:?}");
3899    }
3900
3901    #[test]
3902    fn issue_line_for_missing_required_attribute_points_at_element() {
3903        let s = Schema::compile_str(&xsd_str(r#"
3904            <xs:element name="r">
3905                <xs:complexType>
3906                    <xs:attribute name="must" type="xs:string" use="required"/>
3907                </xs:complexType>
3908            </xs:element>
3909        "#)).unwrap();
3910        let bad = format!("\n\n<r {}/>", instance_ns());
3911        let err = s.validate_str(&bad).unwrap_err();
3912        let issue = err.issues.iter()
3913            .find(|i| i.message.contains("missing required attribute"))
3914            .expect("expected missing-required-attribute error");
3915        assert_eq!(issue.line, Some(3),
3916            "expected <r> on line 3, got {issue:?}");
3917    }
3918
3919    // ── xsi:nil edge cases ─────────────────────────────────────────
3920
3921    #[test]
3922    fn xsi_nil_true_with_empty_content_validates() {
3923        let s = Schema::compile_str(&xsd_str(r#"
3924            <xs:element name="opt" type="xs:int" nillable="true"/>
3925        "#)).unwrap();
3926        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
3927        s.validate_str(&format!(r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3928            ns = instance_ns())).unwrap();
3929        // whitespace-only content is still "empty"
3930        s.validate_str(&format!(r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true">   </opt>"#,
3931            ns = instance_ns())).unwrap();
3932    }
3933
3934    #[test]
3935    fn xsi_nil_true_with_non_empty_content_fails() {
3936        let s = Schema::compile_str(&xsd_str(r#"
3937            <xs:element name="opt" type="xs:int" nillable="true"/>
3938        "#)).unwrap();
3939        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
3940        let err = s.validate_str(&format!(
3941            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true">42</opt>"#,
3942            ns = instance_ns())).unwrap_err();
3943        assert!(err.issues.iter().any(|i|
3944            matches!(i.kind, ValidationKind::NillableViolation)
3945        ), "expected NillableViolation, got {:?}", err.issues);
3946    }
3947
3948    #[test]
3949    fn xsi_nil_on_non_nillable_element_fails() {
3950        let s = Schema::compile_str(&xsd_str(r#"
3951            <xs:element name="opt" type="xs:int"/>
3952        "#)).unwrap();
3953        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
3954        let err = s.validate_str(&format!(
3955            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3956            ns = instance_ns())).unwrap_err();
3957        assert!(err.issues.iter().any(|i|
3958            matches!(i.kind, ValidationKind::NillableViolation)
3959                && i.message.contains("non-nillable")
3960        ), "expected non-nillable rejection, got {:?}", err.issues);
3961    }
3962
3963    #[test]
3964    fn xsi_nil_with_required_attribute_still_validates_attribute() {
3965        // An element with a required attribute and xsi:nil="true":
3966        // the attribute is still required; content must still be empty.
3967        let s = Schema::compile_str(&xsd_str(r#"
3968            <xs:element name="opt" nillable="true">
3969                <xs:complexType>
3970                    <xs:attribute name="id" type="xs:string" use="required"/>
3971                </xs:complexType>
3972            </xs:element>
3973        "#)).unwrap();
3974        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
3975        // Present + nil → valid.
3976        s.validate_str(&format!(
3977            r#"<opt id="x" {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3978            ns = instance_ns())).unwrap();
3979        // Missing required attr + nil → still a missing-attribute error.
3980        let err = s.validate_str(&format!(
3981            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
3982            ns = instance_ns())).unwrap_err();
3983        assert!(err.issues.iter().any(|i|
3984            matches!(i.kind, ValidationKind::MissingRequiredAttribute)
3985        ), "expected MissingRequiredAttribute even under xsi:nil, got {:?}", err.issues);
3986    }
3987
3988    #[test]
3989    fn xsi_nil_skips_required_children_check() {
3990        // With xsi:nil="true", a complex type's required child elements
3991        // are NOT required (the element is treated as absent for
3992        // content-model purposes).
3993        let s = Schema::compile_str(&xsd_str(r#"
3994            <xs:element name="opt" nillable="true">
3995                <xs:complexType>
3996                    <xs:sequence>
3997                        <xs:element name="req" type="xs:string"/>
3998                    </xs:sequence>
3999                </xs:complexType>
4000            </xs:element>
4001        "#)).unwrap();
4002        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
4003        // Without xsi:nil, missing <req> fails.
4004        let err = s.validate_str(&format!(r#"<opt {ns}/>"#, ns = instance_ns())).unwrap_err();
4005        assert!(err.issues.iter().any(|i|
4006            matches!(i.kind, ValidationKind::MissingRequiredElement)
4007        ));
4008        // With xsi:nil, the required child is not enforced.
4009        s.validate_str(&format!(
4010            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
4011            ns = instance_ns())).unwrap();
4012    }
4013
4014    #[test]
4015    fn xsi_nil_overrides_fixed_value_check() {
4016        // Per XSD §3.3.4 / §2.6.2: an element with xsi:nil="true" is
4017        // treated as having no value, so the `fixed=` constraint does
4018        // not apply.  (Without this carve-out, fixed="ABC" + nil would
4019        // always fail because text_buf is "" ≠ "ABC".)
4020        let s = Schema::compile_str(&xsd_str(r#"
4021            <xs:element name="opt" type="xs:string" nillable="true" fixed="ABC"/>
4022        "#)).unwrap();
4023        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
4024        // xsi:nil → fixed= waived.
4025        s.validate_str(&format!(
4026            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="true"/>"#,
4027            ns = instance_ns())).unwrap();
4028        // Without xsi:nil, fixed= enforces.
4029        s.validate_str(&format!(r#"<opt {ns}>ABC</opt>"#, ns = instance_ns())).unwrap();
4030        let err = s.validate_str(&format!(
4031            r#"<opt {ns}>XYZ</opt>"#, ns = instance_ns())).unwrap_err();
4032        assert!(err.issues.iter().any(|i| i.message.contains("fixed")),
4033            "expected fixed mismatch, got {:?}", err.issues);
4034    }
4035
4036    #[test]
4037    fn xsi_nil_false_validates_normally() {
4038        // xsi:nil="false" is equivalent to not having it — content
4039        // must validate normally.
4040        let s = Schema::compile_str(&xsd_str(r#"
4041            <xs:element name="opt" type="xs:int" nillable="true"/>
4042        "#)).unwrap();
4043        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
4044        s.validate_str(&format!(
4045            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="false">42</opt>"#,
4046            ns = instance_ns())).unwrap();
4047        let err = s.validate_str(&format!(
4048            r#"<opt {ns} xmlns:xsi="{xsi}" xsi:nil="false">foo</opt>"#,
4049            ns = instance_ns())).unwrap_err();
4050        assert!(err.issues.iter().any(|i|
4051            matches!(i.kind, ValidationKind::TypeMismatch)
4052        ), "expected TypeMismatch for non-int content, got {:?}", err.issues);
4053    }
4054
4055    #[test]
4056    fn xsi_nil_with_xsi_type_uses_substituted_type() {
4057        // xsi:nil and xsi:type may both be set; nil applies to the
4058        // substituted type.
4059        let s = Schema::compile_str(&xsd_str(r#"
4060            <xs:complexType name="Base">
4061                <xs:sequence>
4062                    <xs:element name="child" type="xs:string"/>
4063                </xs:sequence>
4064            </xs:complexType>
4065            <xs:complexType name="Derived">
4066                <xs:complexContent>
4067                    <xs:extension base="Base">
4068                        <xs:sequence>
4069                            <xs:element name="extra" type="xs:string"/>
4070                        </xs:sequence>
4071                    </xs:extension>
4072                </xs:complexContent>
4073            </xs:complexType>
4074            <xs:element name="x" type="Base" nillable="true"/>
4075        "#)).unwrap();
4076        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
4077        s.validate_str(&format!(
4078            r#"<x {ns} xmlns:xsi="{xsi}" xsi:type="Derived" xsi:nil="true"/>"#,
4079            ns = instance_ns())).unwrap();
4080    }
4081
4082    // ── xs:redefine ────────────────────────────────────────────────
4083
4084    #[test]
4085    fn redefine_replaces_simple_type_in_included_schema() {
4086        // included.xsd defines `Code` as xs:string with length=3 — any
4087        // 3-char string accepted.  outer.xsd redefines `Code` to
4088        // additionally restrict to the enumeration {"ABC", "XYZ"}.
4089        // After compilation, references to `Code` in outer.xsd must
4090        // resolve to the tighter type.
4091        let included = r#"<?xml version="1.0"?>
4092            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4093                       targetNamespace="urn:test"
4094                       xmlns="urn:test">
4095                <xs:simpleType name="Code">
4096                    <xs:restriction base="xs:string">
4097                        <xs:length value="3"/>
4098                    </xs:restriction>
4099                </xs:simpleType>
4100            </xs:schema>
4101        "#;
4102        let outer = r#"<?xml version="1.0"?>
4103            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4104                       targetNamespace="urn:test"
4105                       xmlns="urn:test">
4106                <xs:redefine schemaLocation="included.xsd">
4107                    <xs:simpleType name="Code">
4108                        <xs:restriction base="Code">
4109                            <xs:enumeration value="ABC"/>
4110                            <xs:enumeration value="XYZ"/>
4111                        </xs:restriction>
4112                    </xs:simpleType>
4113                </xs:redefine>
4114                <xs:element name="c" type="Code"/>
4115            </xs:schema>
4116        "#;
4117        let resolver = super::super::resolver::InMemoryResolver::new()
4118            .with("included.xsd", included.as_bytes().to_vec());
4119        let schema = Schema::compile_with(outer, resolver).unwrap();
4120        // Allowed values pass.
4121        schema.validate_str(&format!(r#"<c {}>ABC</c>"#, instance_ns())).unwrap();
4122        schema.validate_str(&format!(r#"<c {}>XYZ</c>"#, instance_ns())).unwrap();
4123        // "DEF" passes the included length=3 check but should fail the
4124        // redefining enumeration.
4125        let err = schema.validate_str(&format!(r#"<c {}>DEF</c>"#, instance_ns())).unwrap_err();
4126        assert!(err.issues.iter().any(|i| i.message.contains("enumeration")),
4127            "expected enumeration failure, got {:?}", err.issues);
4128        // Too-long input still fails (length=3 from base is inherited).
4129        let err = schema.validate_str(&format!(r#"<c {}>TOOLONG</c>"#, instance_ns())).unwrap_err();
4130        assert!(!err.issues.is_empty(),
4131            "expected validation failure for too-long input, got ok");
4132    }
4133
4134    #[test]
4135    fn redefine_complex_type_extension_adds_fields() {
4136        // Canonical xs:redefine pattern: the redefining body extends
4137        // the same-named original.  The composed type validates
4138        // instances containing the original's fields PLUS the new ones.
4139        let included = r#"<?xml version="1.0"?>
4140            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4141                       targetNamespace="urn:test"
4142                       xmlns="urn:test"
4143                       elementFormDefault="qualified">
4144                <xs:complexType name="Address">
4145                    <xs:sequence>
4146                        <xs:element name="city" type="xs:string"/>
4147                    </xs:sequence>
4148                </xs:complexType>
4149                <xs:element name="addr" type="Address"/>
4150            </xs:schema>
4151        "#;
4152        let outer = r#"<?xml version="1.0"?>
4153            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4154                       xmlns:t="urn:test"
4155                       targetNamespace="urn:test"
4156                       xmlns="urn:test"
4157                       elementFormDefault="qualified">
4158                <xs:redefine schemaLocation="included.xsd">
4159                    <xs:complexType name="Address">
4160                        <xs:complexContent>
4161                            <xs:extension base="t:Address">
4162                                <xs:sequence>
4163                                    <xs:element name="country" type="xs:string"/>
4164                                </xs:sequence>
4165                            </xs:extension>
4166                        </xs:complexContent>
4167                    </xs:complexType>
4168                </xs:redefine>
4169            </xs:schema>
4170        "#;
4171        let resolver = super::super::resolver::InMemoryResolver::new()
4172            .with("included.xsd", included.as_bytes().to_vec());
4173        let schema = Schema::compile_with(outer, resolver).unwrap();
4174        // Both original and new fields required.
4175        schema.validate_str(&format!(
4176            r#"<addr {ns}><city>SF</city><country>US</country></addr>"#,
4177            ns = instance_ns(),
4178        )).unwrap();
4179        // Missing the new field — fails (the extension made country required).
4180        let err = schema.validate_str(&format!(
4181            r#"<addr {ns}><city>SF</city></addr>"#,
4182            ns = instance_ns(),
4183        )).unwrap_err();
4184        assert!(err.issues.iter().any(|i|
4185            matches!(i.kind, ValidationKind::MissingRequiredElement)
4186                && i.message.contains("country")
4187        ), "expected missing-country error, got {:?}", err.issues);
4188    }
4189
4190    #[test]
4191    fn redefine_with_no_body_acts_like_include() {
4192        // <xs:redefine schemaLocation="..."/> with an empty body must
4193        // still load the referenced schema.
4194        let included = r#"<?xml version="1.0"?>
4195            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4196                       targetNamespace="urn:test"
4197                       xmlns="urn:test">
4198                <xs:element name="msg" type="xs:string"/>
4199            </xs:schema>
4200        "#;
4201        let outer = r#"<?xml version="1.0"?>
4202            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
4203                       targetNamespace="urn:test"
4204                       xmlns="urn:test">
4205                <xs:redefine schemaLocation="included.xsd"/>
4206            </xs:schema>
4207        "#;
4208        let resolver = super::super::resolver::InMemoryResolver::new()
4209            .with("included.xsd", included.as_bytes().to_vec());
4210        let schema = Schema::compile_with(outer, resolver).unwrap();
4211        schema.validate_str(&format!(r#"<msg {}>hi</msg>"#, instance_ns())).unwrap();
4212    }
4213
4214    // ── restriction with user-defined base ─────────────────────────
4215
4216    #[test]
4217    fn restriction_chain_composes_facets_top_down() {
4218        // A: xs:string with maxLength=20
4219        // B: A with pattern="[A-Z]+"
4220        // C: B with enumeration={"FOO","BAR"}
4221        // Only "FOO" and "BAR" should validate; "ABC" fails enumeration,
4222        // "FOOOO" fails enumeration AND maxLength wouldn't fire here, and
4223        // "foo" fails pattern.
4224        let s = Schema::compile_str(&xsd_str(r#"
4225            <xs:simpleType name="A">
4226                <xs:restriction base="xs:string">
4227                    <xs:maxLength value="20"/>
4228                </xs:restriction>
4229            </xs:simpleType>
4230            <xs:simpleType name="B">
4231                <xs:restriction base="A">
4232                    <xs:pattern value="[A-Z]+"/>
4233                </xs:restriction>
4234            </xs:simpleType>
4235            <xs:simpleType name="C">
4236                <xs:restriction base="B">
4237                    <xs:enumeration value="FOO"/>
4238                    <xs:enumeration value="BAR"/>
4239                </xs:restriction>
4240            </xs:simpleType>
4241            <xs:element name="v" type="C"/>
4242        "#)).unwrap();
4243        // Accept the allowed values.
4244        s.validate_str(&format!(r#"<v {}>FOO</v>"#, instance_ns())).unwrap();
4245        s.validate_str(&format!(r#"<v {}>BAR</v>"#, instance_ns())).unwrap();
4246        // "ABC" fails enumeration (B's pattern accepts it).
4247        let err = s.validate_str(&format!(r#"<v {}>ABC</v>"#, instance_ns())).unwrap_err();
4248        assert!(err.issues.iter().any(|i| i.message.contains("enumeration")),
4249            "expected enumeration failure, got {:?}", err.issues);
4250        // "foo" fails B's pattern.
4251        let err = s.validate_str(&format!(r#"<v {}>foo</v>"#, instance_ns())).unwrap_err();
4252        assert!(err.issues.iter().any(|i| i.message.contains("pattern")),
4253            "expected pattern failure, got {:?}", err.issues);
4254    }
4255
4256    #[test]
4257    fn restriction_preserves_list_variety_from_base() {
4258        // After fix: restricting a list type yields a list type.
4259        // The length facet then counts items, not characters.
4260        let s = Schema::compile_str(&xsd_str(r#"
4261            <xs:simpleType name="IntList">
4262                <xs:list itemType="xs:int"/>
4263            </xs:simpleType>
4264            <xs:simpleType name="ThreeInts">
4265                <xs:restriction base="IntList">
4266                    <xs:length value="3"/>
4267                </xs:restriction>
4268            </xs:simpleType>
4269            <xs:element name="nums" type="ThreeInts"/>
4270        "#)).unwrap();
4271        s.validate_str(&format!(r#"<nums {}>1 2 3</nums>"#, instance_ns())).unwrap();
4272        let err = s.validate_str(&format!(r#"<nums {}>1 2</nums>"#, instance_ns())).unwrap_err();
4273        assert!(err.issues.iter().any(|i| i.message.contains("2 item(s)")),
4274            "expected list-length error counting items, got {:?}", err.issues);
4275        // Items must still be valid ints — restriction inherits item type.
4276        let err = s.validate_str(&format!(r#"<v {}>1 foo 3</v>"#, instance_ns())).unwrap_err();
4277        assert!(!err.issues.is_empty(),
4278            "expected validation failure for non-int item, got ok");
4279    }
4280
4281    // ── xsi:type derivation check ──────────────────────────────────
4282
4283    #[test]
4284    fn xsi_type_accepts_derived_complex_type() {
4285        // Address is a base; USAddress extends it.  An element
4286        // declared as Address may use xsi:type to substitute USAddress,
4287        // and its content (city from Address + state from extension)
4288        // must validate under the merged content model.
4289        let s = Schema::compile_str(&xsd_str(r#"
4290            <xs:complexType name="Address">
4291                <xs:sequence>
4292                    <xs:element name="city" type="xs:string"/>
4293                </xs:sequence>
4294            </xs:complexType>
4295            <xs:complexType name="USAddress">
4296                <xs:complexContent>
4297                    <xs:extension base="Address">
4298                        <xs:sequence>
4299                            <xs:element name="state" type="xs:string"/>
4300                        </xs:sequence>
4301                    </xs:extension>
4302                </xs:complexContent>
4303            </xs:complexType>
4304            <xs:element name="addr" type="Address"/>
4305        "#)).unwrap();
4306        let ok = format!(
4307            r#"<addr xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4308                   xsi:type="USAddress" {ns}>
4309                 <city>SF</city>
4310                 <state>CA</state>
4311               </addr>"#,
4312            ns = instance_ns(),
4313        );
4314        s.validate_str(&ok).unwrap();
4315    }
4316
4317    #[test]
4318    fn extension_three_level_chain_merges_all_levels() {
4319        // A -> B (extension) -> C (extension), instance under C must
4320        // see fields from all three levels.
4321        let s = Schema::compile_str(&xsd_str(r#"
4322            <xs:complexType name="A">
4323                <xs:sequence>
4324                    <xs:element name="a" type="xs:string"/>
4325                </xs:sequence>
4326            </xs:complexType>
4327            <xs:complexType name="B">
4328                <xs:complexContent>
4329                    <xs:extension base="A">
4330                        <xs:sequence>
4331                            <xs:element name="b" type="xs:string"/>
4332                        </xs:sequence>
4333                    </xs:extension>
4334                </xs:complexContent>
4335            </xs:complexType>
4336            <xs:complexType name="C">
4337                <xs:complexContent>
4338                    <xs:extension base="B">
4339                        <xs:sequence>
4340                            <xs:element name="c" type="xs:string"/>
4341                        </xs:sequence>
4342                    </xs:extension>
4343                </xs:complexContent>
4344            </xs:complexType>
4345            <xs:element name="root" type="C"/>
4346        "#)).unwrap();
4347        let ok = format!(
4348            r#"<root {}>
4349                 <a>x</a><b>y</b><c>z</c>
4350               </root>"#,
4351            instance_ns(),
4352        );
4353        s.validate_str(&ok).unwrap();
4354    }
4355
4356    #[test]
4357    fn extension_merges_attributes() {
4358        // Base contributes a required attribute; extension adds another.
4359        let s = Schema::compile_str(&xsd_str(r#"
4360            <xs:complexType name="Tagged">
4361                <xs:attribute name="id" type="xs:string" use="required"/>
4362            </xs:complexType>
4363            <xs:complexType name="TaggedNamed">
4364                <xs:complexContent>
4365                    <xs:extension base="Tagged">
4366                        <xs:attribute name="name" type="xs:string" use="required"/>
4367                    </xs:extension>
4368                </xs:complexContent>
4369            </xs:complexType>
4370            <xs:element name="item" type="TaggedNamed"/>
4371        "#)).unwrap();
4372        s.validate_str(&format!(r#"<item id="x" name="y" {}/>"#, instance_ns())).unwrap();
4373        // Missing inherited id → error.
4374        let err = s.validate_str(&format!(r#"<item name="y" {}/>"#, instance_ns())).unwrap_err();
4375        assert!(err.issues.iter().any(|i| i.message.contains("missing required attribute")
4376            && i.message.contains("id")
4377        ), "expected missing-id error, got {:?}", err.issues);
4378    }
4379
4380    #[test]
4381    fn xsi_type_rejects_unrelated_complex_type() {
4382        // Two unrelated complex types — xsi:type pointing at the
4383        // other one must fail.
4384        let s = Schema::compile_str(&xsd_str(r#"
4385            <xs:complexType name="Address">
4386                <xs:sequence>
4387                    <xs:element name="city" type="xs:string"/>
4388                </xs:sequence>
4389            </xs:complexType>
4390            <xs:complexType name="Person">
4391                <xs:sequence>
4392                    <xs:element name="name" type="xs:string"/>
4393                </xs:sequence>
4394            </xs:complexType>
4395            <xs:element name="addr" type="Address"/>
4396        "#)).unwrap();
4397        let bad = format!(
4398            r#"<addr xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4399                   xsi:type="Person" {ns}>
4400                 <name>alice</name>
4401               </addr>"#,
4402            ns = instance_ns(),
4403        );
4404        let err = s.validate_str(&bad).unwrap_err();
4405        assert!(err.issues.iter().any(|i|
4406            matches!(i.kind, ValidationKind::TypeMismatch)
4407                && i.message.contains("does not derive from")
4408        ), "expected derivation-failure error, got {:?}", err.issues);
4409    }
4410
4411    #[test]
4412    fn xsi_type_accepts_identity_no_op() {
4413        // xsi:type set to the declared type itself is always allowed.
4414        let s = Schema::compile_str(&xsd_str(r#"
4415            <xs:complexType name="Address">
4416                <xs:sequence>
4417                    <xs:element name="city" type="xs:string"/>
4418                </xs:sequence>
4419            </xs:complexType>
4420            <xs:element name="addr" type="Address"/>
4421        "#)).unwrap();
4422        let ok = format!(
4423            r#"<addr xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4424                   xsi:type="Address" {ns}>
4425                 <city>SF</city>
4426               </addr>"#,
4427            ns = instance_ns(),
4428        );
4429        s.validate_str(&ok).unwrap();
4430    }
4431
4432    #[test]
4433    fn xsi_type_blocked_by_element_block_extension() {
4434        // `block="extension"` on the element forbids substituting any
4435        // type that derives by extension.
4436        let s = Schema::compile_str(&xsd_str(r#"
4437            <xs:complexType name="Address">
4438                <xs:sequence>
4439                    <xs:element name="city" type="xs:string"/>
4440                </xs:sequence>
4441            </xs:complexType>
4442            <xs:complexType name="USAddress">
4443                <xs:complexContent>
4444                    <xs:extension base="Address">
4445                        <xs:sequence>
4446                            <xs:element name="state" type="xs:string"/>
4447                        </xs:sequence>
4448                    </xs:extension>
4449                </xs:complexContent>
4450            </xs:complexType>
4451            <xs:element name="addr" type="Address" block="extension"/>
4452        "#)).unwrap();
4453        let bad = format!(
4454            r#"<addr xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4455                   xsi:type="USAddress" {ns}>
4456                 <city>SF</city>
4457                 <state>CA</state>
4458               </addr>"#,
4459            ns = instance_ns(),
4460        );
4461        let err = s.validate_str(&bad).unwrap_err();
4462        assert!(err.issues.iter().any(|i|
4463            matches!(i.kind, ValidationKind::TypeMismatch)
4464                && i.message.contains("blocked")
4465        ), "expected block= rejection, got {:?}", err.issues);
4466    }
4467
4468    #[test]
4469    fn xsi_type_blocked_by_base_type_final_extension() {
4470        // XSD §3.4.6 (Derivation Valid (Extension), clause 1.1):
4471        // if the base type's `final` contains `extension`, deriving
4472        // by extension is a schema component constraint violation
4473        // — the schema itself is invalid and must not compile.
4474        // We surface this at compile time (matches libxml2's
4475        // `cos-st-derived-ok` rejection for the same shape) rather
4476        // than deferring to instance-time `xsi:type` validation.
4477        let result = Schema::compile_str(&xsd_str(r#"
4478            <xs:complexType name="Address" final="extension">
4479                <xs:sequence>
4480                    <xs:element name="city" type="xs:string"/>
4481                </xs:sequence>
4482            </xs:complexType>
4483            <xs:complexType name="USAddress">
4484                <xs:complexContent>
4485                    <xs:extension base="Address">
4486                        <xs:sequence>
4487                            <xs:element name="state" type="xs:string"/>
4488                        </xs:sequence>
4489                    </xs:extension>
4490                </xs:complexContent>
4491            </xs:complexType>
4492            <xs:element name="addr" type="Address"/>
4493        "#));
4494        let err = result.expect_err("schema must not compile");
4495        assert!(
4496            err.message.contains("final") && err.message.contains("extension"),
4497            "expected diagnostic mentioning final/extension, got: {}",
4498            err.message,
4499        );
4500    }
4501
4502    #[test]
4503    fn xsi_type_two_level_chain_derivation_check_accepts() {
4504        // A -> B (extension) -> C (extension).  Declared element is A;
4505        // xsi:type=C should pass the derivation check (transitively
4506        // derives by extension).  Instance uses xsi:nil so content
4507        // validation is skipped — the focus is the derivation walk.
4508        let s = Schema::compile_str(&xsd_str(r#"
4509            <xs:complexType name="A">
4510                <xs:sequence>
4511                    <xs:element name="a" type="xs:string" minOccurs="0"/>
4512                </xs:sequence>
4513            </xs:complexType>
4514            <xs:complexType name="B">
4515                <xs:complexContent>
4516                    <xs:extension base="A">
4517                        <xs:sequence>
4518                            <xs:element name="b" type="xs:string" minOccurs="0"/>
4519                        </xs:sequence>
4520                    </xs:extension>
4521                </xs:complexContent>
4522            </xs:complexType>
4523            <xs:complexType name="C">
4524                <xs:complexContent>
4525                    <xs:extension base="B">
4526                        <xs:sequence>
4527                            <xs:element name="c" type="xs:string" minOccurs="0"/>
4528                        </xs:sequence>
4529                    </xs:extension>
4530                </xs:complexContent>
4531            </xs:complexType>
4532            <xs:element name="root" type="A" nillable="true"/>
4533        "#)).unwrap();
4534        // No content — derivation check passes; content validation has
4535        // nothing to chew on.
4536        let ok = format!(
4537            r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4538                   xsi:type="C" xsi:nil="true" {ns}/>"#,
4539            ns = instance_ns(),
4540        );
4541        s.validate_str(&ok).unwrap();
4542    }
4543
4544    // ── xs:list and xs:union ───────────────────────────────────────
4545
4546    #[test]
4547    fn xs_list_of_int_validates_each_item() {
4548        let s = Schema::compile_str(&xsd_str(r#"
4549            <xs:simpleType name="IntList">
4550                <xs:list itemType="xs:int"/>
4551            </xs:simpleType>
4552            <xs:element name="nums" type="IntList"/>
4553        "#)).unwrap();
4554        s.validate_str(&format!(r#"<nums {}>1 2 3</nums>"#, instance_ns())).unwrap();
4555        s.validate_str(&format!(r#"<nums {}></nums>"#, instance_ns())).unwrap();
4556        let err = s.validate_str(&format!(r#"<nums {}>1 foo 3</nums>"#, instance_ns())).unwrap_err();
4557        assert!(!err.issues.is_empty(),
4558            "expected at least one issue for 'foo' as int, got {:?}", err.issues);
4559    }
4560
4561    #[test]
4562    fn xs_list_length_facet_counts_items_not_chars() {
4563        // length=3 on a list means exactly 3 items, not 3 characters.
4564        // (Schema written with an explicit named base so we don't depend
4565        // on the implicit-base nested-simpleType form.)
4566        let s = Schema::compile_str(&xsd_str(r#"
4567            <xs:simpleType name="IntList">
4568                <xs:list itemType="xs:int"/>
4569            </xs:simpleType>
4570            <xs:simpleType name="ThreeInts">
4571                <xs:restriction base="IntList">
4572                    <xs:length value="3"/>
4573                </xs:restriction>
4574            </xs:simpleType>
4575            <xs:element name="nums" type="ThreeInts"/>
4576        "#)).unwrap();
4577        s.validate_str(&format!(r#"<nums {}>1 2 3</nums>"#, instance_ns())).unwrap();
4578        let too_few = s.validate_str(&format!(r#"<nums {}>1 2</nums>"#, instance_ns())).unwrap_err();
4579        assert!(too_few.issues.iter().any(|i| i.message.contains("length")),
4580            "expected length-facet error for 2 items, got {:?}", too_few.issues);
4581        let too_many = s.validate_str(&format!(r#"<nums {}>1 2 3 4</nums>"#, instance_ns())).unwrap_err();
4582        assert!(too_many.issues.iter().any(|i| i.message.contains("length")),
4583            "expected length-facet error for 4 items, got {:?}", too_many.issues);
4584    }
4585
4586    #[test]
4587    fn xs_union_accepts_any_member_type() {
4588        // Union of int + date: both shapes accepted, nonsense rejected.
4589        let s = Schema::compile_str(&xsd_str(r#"
4590            <xs:simpleType name="IntOrDate">
4591                <xs:union memberTypes="xs:int xs:date"/>
4592            </xs:simpleType>
4593            <xs:element name="val" type="IntOrDate"/>
4594        "#)).unwrap();
4595        s.validate_str(&format!(r#"<val {}>42</val>"#, instance_ns())).unwrap();
4596        s.validate_str(&format!(r#"<val {}>2026-05-16</val>"#, instance_ns())).unwrap();
4597        let err = s.validate_str(&format!(r#"<val {}>nonsense</val>"#, instance_ns())).unwrap_err();
4598        assert!(!err.issues.is_empty(),
4599            "expected union-failure, got {:?}", err.issues);
4600    }
4601
4602    #[test]
4603    fn xs_list_facet_length_unit_test() {
4604        // Direct unit test on SimpleType with Variety::List, avoiding
4605        // the schema-restriction path so the length-facet logic in
4606        // SimpleType::validate is exercised in isolation.
4607        use super::super::types::{SimpleType, Variety};
4608        use super::super::facets::{Facet, FacetSet};
4609        let mut list_facets = FacetSet::default();
4610        list_facets.push(Facet::Length(3));
4611        let three_ints = SimpleType {
4612            name: Some("ThreeInts".into()),
4613            builtin: BuiltinType::String,
4614            facets: list_facets,
4615            whitespace: super::super::whitespace::WhitespaceMode::Collapse,
4616            variety: Variety::List {
4617                item_type: Arc::new(SimpleType::of_builtin(BuiltinType::Int)),
4618            },
4619            final_: super::super::schema::BlockSet::default(),
4620            assertions: Vec::new(),
4621        };
4622        three_ints.validate("1 2 3").unwrap();
4623        let err = three_ints.validate("1 2").unwrap_err();
4624        assert!(err.message.contains("length") && err.message.contains("2 item(s)"),
4625            "expected list length error mentioning item count, got {}", err.message);
4626        let err = three_ints.validate("1 2 3 4").unwrap_err();
4627        assert!(err.message.contains("length") && err.message.contains("4 item(s)"),
4628            "expected list length error mentioning item count, got {}", err.message);
4629        // Item-type failure surfaces as a list-item error.
4630        let err = three_ints.validate("1 foo 3").unwrap_err();
4631        assert!(err.message.contains("list item #2"),
4632            "expected list-item error citing position, got {}", err.message);
4633    }
4634
4635    #[test]
4636    fn xs_union_with_nested_simpletypes() {
4637        // Anonymous members via nested xs:simpleType.
4638        let s = Schema::compile_str(&xsd_str(r#"
4639            <xs:simpleType name="SmallOrBig">
4640                <xs:union>
4641                    <xs:simpleType>
4642                        <xs:restriction base="xs:int">
4643                            <xs:maxInclusive value="10"/>
4644                        </xs:restriction>
4645                    </xs:simpleType>
4646                    <xs:simpleType>
4647                        <xs:restriction base="xs:int">
4648                            <xs:minInclusive value="1000"/>
4649                        </xs:restriction>
4650                    </xs:simpleType>
4651                </xs:union>
4652            </xs:simpleType>
4653            <xs:element name="n" type="SmallOrBig"/>
4654        "#)).unwrap();
4655        s.validate_str(&format!(r#"<n {}>5</n>"#, instance_ns())).unwrap();
4656        s.validate_str(&format!(r#"<n {}>2000</n>"#, instance_ns())).unwrap();
4657        let err = s.validate_str(&format!(r#"<n {}>500</n>"#, instance_ns())).unwrap_err();
4658        assert!(!err.issues.is_empty(),
4659            "expected union-failure for 500 (between 10 and 1000), got {:?}", err.issues);
4660    }
4661
4662    #[test]
4663    fn issue_line_for_duplicate_key_points_at_constraint_declaring_element() {
4664        let s = Schema::compile_str(&xsd_str(r#"
4665            <xs:element name="users">
4666                <xs:complexType>
4667                    <xs:sequence>
4668                        <xs:element name="user" maxOccurs="unbounded">
4669                            <xs:complexType>
4670                                <xs:attribute name="id" type="xs:string" use="required"/>
4671                            </xs:complexType>
4672                        </xs:element>
4673                    </xs:sequence>
4674                </xs:complexType>
4675                <xs:key name="userKey">
4676                    <xs:selector xpath=".//user"/>
4677                    <xs:field xpath="@id"/>
4678                </xs:key>
4679            </xs:element>
4680        "#)).unwrap();
4681        // <users> on line 3 (after 2 leading newlines).
4682        let bad = format!(
4683            "\n\n<users {}>\n  <user id=\"A\"/>\n  <user id=\"A\"/>\n</users>",
4684            instance_ns()
4685        );
4686        let err = s.validate_str(&bad).unwrap_err();
4687        let issue = err.issues.iter()
4688            .find(|i| matches!(i.kind, ValidationKind::KeyNotUnique))
4689            .expect("expected KeyNotUnique error");
4690        assert_eq!(issue.line, Some(3),
4691            "expected <users> (declaring element) on line 3, got {issue:?}");
4692    }
4693
4694    #[test]
4695    fn validate_doc_typed_records_governing_types() {
4696        let s = Schema::compile_str(&xsd_str(r#"
4697            <xs:element name="root">
4698                <xs:complexType>
4699                    <xs:sequence>
4700                        <xs:element name="count" type="xs:integer"/>
4701                        <xs:element name="label" type="xs:string"/>
4702                    </xs:sequence>
4703                </xs:complexType>
4704            </xs:element>
4705        "#)).unwrap();
4706        let mut opts = crate::ParseOptions::default();
4707        opts.namespace_aware = true;
4708        let doc = crate::parse_str(
4709            &format!(r#"<root {}><count>3</count><label>hi</label></root>"#, instance_ns()),
4710            &opts,
4711        ).unwrap();
4712        let (res, psvi) = s.validate_doc_typed(&doc);
4713        assert!(res.is_ok(), "expected valid doc, got {res:?}");
4714        assert!(!psvi.is_empty(), "expected recorded type annotations");
4715
4716        // Walk to the two leaf elements and check their recorded
4717        // primitive types.
4718        let root = doc.root();
4719        assert!(psvi.governing_type(root).is_some(), "root should be typed");
4720        for child in root.children().filter(|n|
4721            matches!(n.kind, sup_xml_tree::dom::NodeKind::Element))
4722        {
4723            let ty = psvi.governing_type(child)
4724                .unwrap_or_else(|| panic!("{} should be typed", child.name()));
4725            let TypeRef::Simple(st) = ty else { panic!("expected simple type") };
4726            match child.name() {
4727                "count" => assert_eq!(st.builtin, BuiltinType::Integer),
4728                "label" => assert_eq!(st.builtin, BuiltinType::String),
4729                other   => panic!("unexpected child {other}"),
4730            }
4731        }
4732    }
4733}