Skip to main content

xsd_schema/validation/
asttree.rs

1//! AST types, error handling, and namespace resolution for identity-constraint XPath.
2//!
3//! This module defines the abstract syntax tree produced by parsing the restricted XPath
4//! subset used in XSD `<selector>` and `<field>` expressions. It also provides the
5//! `xpathDefaultNamespace` cascade resolution required by XSD 1.1.
6
7#![allow(dead_code)]
8
9use std::fmt;
10
11use crate::ids::NameId;
12use crate::namespace::context::NamespaceContextSnapshot;
13use crate::namespace::table::NameTable;
14use crate::schema::model::XsdVersion;
15
16use super::identity_lexer::IdXPathLexError;
17
18/// Error produced during identity-constraint XPath compilation.
19#[derive(Debug, Clone)]
20pub enum IdentityXPathError {
21    /// Lexer error (invalid character, unsupported syntax).
22    Lex(IdXPathLexError),
23    /// Parser error (unexpected token, malformed expression).
24    Parse { message: String, position: usize },
25    /// Unbound namespace prefix.
26    UnboundPrefix { prefix: String, position: usize },
27    /// Restriction violation (e.g. attribute step in selector, attribute not last).
28    Restriction { message: String, position: usize },
29}
30
31impl fmt::Display for IdentityXPathError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            IdentityXPathError::Lex(e) => write!(f, "{e}"),
35            IdentityXPathError::Parse { message, position } => {
36                write!(
37                    f,
38                    "identity XPath parse error at position {position}: {message}"
39                )
40            }
41            IdentityXPathError::UnboundPrefix { prefix, position } => {
42                write!(
43                    f,
44                    "identity XPath error at position {position}: unbound prefix `{prefix}`"
45                )
46            }
47            IdentityXPathError::Restriction { message, position } => {
48                write!(
49                    f,
50                    "identity XPath restriction at position {position}: {message}"
51                )
52            }
53        }
54    }
55}
56
57impl std::error::Error for IdentityXPathError {}
58
59impl From<IdXPathLexError> for IdentityXPathError {
60    fn from(e: IdXPathLexError) -> Self {
61        IdentityXPathError::Lex(e)
62    }
63}
64
65/// How an unprefixed element name matches a namespace.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) enum NamespaceMatch {
68    /// No namespace (XSD 1.0 default, or `##local`).
69    NoNamespace,
70    /// An exact namespace URI.
71    Exact(NameId),
72}
73
74/// A name test in a step.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub(crate) enum NameTest {
77    /// `*` — matches any element/attribute.
78    Wildcard,
79    /// `ns:*` — matches any local name in the given namespace.
80    NamespaceWildcard(NameId),
81    /// `foo` or `ns:foo` — matches a specific QName.
82    QName {
83        namespace: NamespaceMatch,
84        local_name: NameId,
85    },
86}
87
88impl NameTest {
89    /// Check whether this name test matches a given namespace URI and local name.
90    pub(crate) fn matches(&self, namespace_uri: NameId, local_name: NameId) -> bool {
91        match self {
92            NameTest::Wildcard => true,
93            NameTest::NamespaceWildcard(ns) => namespace_uri == *ns,
94            NameTest::QName {
95                namespace,
96                local_name: ln,
97            } => {
98                *ln == local_name
99                    && match namespace {
100                        NamespaceMatch::NoNamespace => {
101                            // No namespace means empty namespace URI
102                            namespace_uri.0 == 0
103                        }
104                        NamespaceMatch::Exact(ns) => namespace_uri == *ns,
105                    }
106            }
107        }
108    }
109}
110
111/// A single step in a path expression.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub(crate) enum AstStep {
114    /// `.` — the current node.
115    SelfNode,
116    /// `foo`, `child::foo`, `*`, etc. — child axis.
117    Child(NameTest),
118    /// `@foo`, `attribute::foo` — attribute axis (field expressions only).
119    Attribute(NameTest),
120}
121
122/// A single path in a selector/field expression.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub(crate) struct AstPath {
125    /// Whether this path starts with `.//` (descendant-or-self).
126    pub descendant: bool,
127    /// The steps in this path.
128    pub steps: Vec<AstStep>,
129}
130
131/// Parsed identity-constraint XPath expression (union of paths).
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub(crate) struct Asttree {
134    /// The alternative paths (union branches).
135    pub paths: Vec<AstPath>,
136}
137
138impl Asttree {
139    /// Compile a selector XPath expression.
140    ///
141    /// Selector expressions may not contain attribute steps.
142    /// In XSD 1.0 mode, `xpathDefaultNamespace` is ignored (forced to `NoNamespace`).
143    pub fn compile_selector(
144        xpath: &str,
145        ns_snapshot: &NamespaceContextSnapshot,
146        name_table: &NameTable,
147        own_xpath_default_ns: Option<&str>,
148        schema_xpath_default_ns: Option<NameId>,
149        target_namespace: Option<NameId>,
150        xsd_version: XsdVersion,
151    ) -> Result<Asttree, IdentityXPathError> {
152        use super::identity_parser::IdXPathParser;
153
154        // In XSD 1.0 mode, xpathDefaultNamespace is not supported
155        let (effective_own, effective_schema) = match xsd_version {
156            XsdVersion::V1_0 => (None, None),
157            XsdVersion::V1_1 => (own_xpath_default_ns, schema_xpath_default_ns),
158        };
159
160        let unprefixed_ns = resolve_effective_default_ns(
161            effective_own,
162            effective_schema,
163            ns_snapshot,
164            target_namespace,
165            name_table,
166        );
167        let mut parser = IdXPathParser::new(xpath, ns_snapshot, name_table, unprefixed_ns)?;
168        parser.parse_selector()
169    }
170
171    /// Compile a field XPath expression.
172    ///
173    /// Field expressions allow an optional final attribute step.
174    /// In XSD 1.0 mode, `xpathDefaultNamespace` is ignored (forced to `NoNamespace`).
175    pub fn compile_field(
176        xpath: &str,
177        ns_snapshot: &NamespaceContextSnapshot,
178        name_table: &NameTable,
179        own_xpath_default_ns: Option<&str>,
180        schema_xpath_default_ns: Option<NameId>,
181        target_namespace: Option<NameId>,
182        xsd_version: XsdVersion,
183    ) -> Result<Asttree, IdentityXPathError> {
184        use super::identity_parser::IdXPathParser;
185
186        // In XSD 1.0 mode, xpathDefaultNamespace is not supported
187        let (effective_own, effective_schema) = match xsd_version {
188            XsdVersion::V1_0 => (None, None),
189            XsdVersion::V1_1 => (own_xpath_default_ns, schema_xpath_default_ns),
190        };
191
192        let unprefixed_ns = resolve_effective_default_ns(
193            effective_own,
194            effective_schema,
195            ns_snapshot,
196            target_namespace,
197            name_table,
198        );
199        let mut parser = IdXPathParser::new(xpath, ns_snapshot, name_table, unprefixed_ns)?;
200        parser.parse_field()
201    }
202}
203
204/// Resolve the effective default namespace for unprefixed element names.
205///
206/// Cascade: `own_raw` > `schema_raw_id` (resolved via `name_table`).
207/// Special values:
208/// - `##defaultNamespace` → snapshot's default namespace
209/// - `##targetNamespace` → schema's target namespace
210/// - `##local` → `NoNamespace`
211/// - other string → `Exact(name_table.add(uri))`
212/// - no value → `NoNamespace` (XSD 1.0 behavior)
213fn resolve_effective_default_ns(
214    own_raw: Option<&str>,
215    schema_raw_id: Option<NameId>,
216    ns_snapshot: &NamespaceContextSnapshot,
217    target_namespace: Option<NameId>,
218    name_table: &NameTable,
219) -> NamespaceMatch {
220    // Try own-level first, then schema-level
221    let effective = if let Some(raw) = own_raw {
222        Some(raw.to_string())
223    } else {
224        schema_raw_id.map(|id| name_table.resolve(id))
225    };
226
227    match effective.as_deref() {
228        Some("##defaultNamespace") => match ns_snapshot.default_ns {
229            Some(ns_id) => NamespaceMatch::Exact(ns_id),
230            None => NamespaceMatch::NoNamespace,
231        },
232        Some("##targetNamespace") => match target_namespace {
233            Some(ns_id) => NamespaceMatch::Exact(ns_id),
234            None => NamespaceMatch::NoNamespace,
235        },
236        Some("##local") => NamespaceMatch::NoNamespace,
237        Some(uri) => NamespaceMatch::Exact(name_table.add(uri)),
238        None => NamespaceMatch::NoNamespace,
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::namespace::context::NamespaceContextSnapshot;
246    use crate::namespace::table::NameTable;
247
248    // --- Namespace resolution tests ---
249
250    #[test]
251    fn no_default_unprefixed() {
252        let table = NameTable::new();
253        let snapshot = NamespaceContextSnapshot::default();
254        let result = resolve_effective_default_ns(None, None, &snapshot, None, &table);
255        assert_eq!(result, NamespaceMatch::NoNamespace);
256    }
257
258    #[test]
259    fn own_default_namespace() {
260        let table = NameTable::new();
261        let uri_id = table.add("http://example.com/ns");
262        let snapshot = NamespaceContextSnapshot {
263            default_ns: Some(uri_id),
264            bindings: vec![],
265        };
266        let result =
267            resolve_effective_default_ns(Some("##defaultNamespace"), None, &snapshot, None, &table);
268        assert_eq!(result, NamespaceMatch::Exact(uri_id));
269    }
270
271    #[test]
272    fn own_target_namespace() {
273        let table = NameTable::new();
274        let tns = table.add("http://example.com/target");
275        let snapshot = NamespaceContextSnapshot::default();
276        let result = resolve_effective_default_ns(
277            Some("##targetNamespace"),
278            None,
279            &snapshot,
280            Some(tns),
281            &table,
282        );
283        assert_eq!(result, NamespaceMatch::Exact(tns));
284    }
285
286    #[test]
287    fn own_local() {
288        let table = NameTable::new();
289        let snapshot = NamespaceContextSnapshot::default();
290        let result = resolve_effective_default_ns(Some("##local"), None, &snapshot, None, &table);
291        assert_eq!(result, NamespaceMatch::NoNamespace);
292    }
293
294    #[test]
295    fn own_literal_uri() {
296        let table = NameTable::new();
297        let snapshot = NamespaceContextSnapshot::default();
298        let result =
299            resolve_effective_default_ns(Some("http://example.com"), None, &snapshot, None, &table);
300        let expected_id = table.add("http://example.com");
301        assert_eq!(result, NamespaceMatch::Exact(expected_id));
302    }
303
304    #[test]
305    fn cascade_own_over_schema() {
306        let table = NameTable::new();
307        let schema_ns = table.add("http://example.com");
308        let snapshot = NamespaceContextSnapshot::default();
309        // own = ##local wins over schema-level URI
310        let result =
311            resolve_effective_default_ns(Some("##local"), Some(schema_ns), &snapshot, None, &table);
312        assert_eq!(result, NamespaceMatch::NoNamespace);
313    }
314
315    #[test]
316    fn cascade_schema_fallback() {
317        let table = NameTable::new();
318        let schema_ns = table.add("http://example.com");
319        let snapshot = NamespaceContextSnapshot::default();
320        // own = None, falls through to schema-level
321        let result = resolve_effective_default_ns(None, Some(schema_ns), &snapshot, None, &table);
322        assert_eq!(result, NamespaceMatch::Exact(schema_ns));
323    }
324
325    #[test]
326    fn default_ns_absent() {
327        let table = NameTable::new();
328        let snapshot = NamespaceContextSnapshot {
329            default_ns: None,
330            bindings: vec![],
331        };
332        let result =
333            resolve_effective_default_ns(Some("##defaultNamespace"), None, &snapshot, None, &table);
334        assert_eq!(result, NamespaceMatch::NoNamespace);
335    }
336
337    #[test]
338    fn target_ns_absent() {
339        let table = NameTable::new();
340        let snapshot = NamespaceContextSnapshot::default();
341        let result =
342            resolve_effective_default_ns(Some("##targetNamespace"), None, &snapshot, None, &table);
343        assert_eq!(result, NamespaceMatch::NoNamespace);
344    }
345
346    // --- NameTest::matches tests ---
347
348    #[test]
349    fn wildcard_matches_anything() {
350        let table = NameTable::new();
351        let ns = table.add("http://example.com");
352        let ln = table.add("foo");
353        assert!(NameTest::Wildcard.matches(ns, ln));
354    }
355
356    #[test]
357    fn namespace_wildcard_matches_same_ns() {
358        let table = NameTable::new();
359        let ns = table.add("http://example.com");
360        let ln = table.add("foo");
361        assert!(NameTest::NamespaceWildcard(ns).matches(ns, ln));
362    }
363
364    #[test]
365    fn namespace_wildcard_rejects_different_ns() {
366        let table = NameTable::new();
367        let ns1 = table.add("http://example.com/1");
368        let ns2 = table.add("http://example.com/2");
369        let ln = table.add("foo");
370        assert!(!NameTest::NamespaceWildcard(ns1).matches(ns2, ln));
371    }
372
373    #[test]
374    fn qname_exact_match() {
375        let table = NameTable::new();
376        let ns = table.add("http://example.com");
377        let ln = table.add("foo");
378        let test = NameTest::QName {
379            namespace: NamespaceMatch::Exact(ns),
380            local_name: ln,
381        };
382        assert!(test.matches(ns, ln));
383    }
384
385    #[test]
386    fn qname_no_namespace_match() {
387        let table = NameTable::new();
388        let ln = table.add("foo");
389        let test = NameTest::QName {
390            namespace: NamespaceMatch::NoNamespace,
391            local_name: ln,
392        };
393        use crate::namespace::table::well_known;
394        // NameId(0) = empty string = no namespace
395        assert!(test.matches(well_known::EMPTY, ln));
396    }
397
398    #[test]
399    fn qname_rejects_wrong_local() {
400        let table = NameTable::new();
401        let ns = table.add("http://example.com");
402        let ln1 = table.add("foo");
403        let ln2 = table.add("bar");
404        let test = NameTest::QName {
405            namespace: NamespaceMatch::Exact(ns),
406            local_name: ln1,
407        };
408        assert!(!test.matches(ns, ln2));
409    }
410
411    // --- XSD version gating tests ---
412
413    #[test]
414    fn compile_selector_v10_ignores_own_xpath_default_ns() {
415        // In XSD 1.0 mode, xpathDefaultNamespace should be ignored,
416        // so unprefixed element names resolve to NoNamespace.
417        let table = NameTable::new();
418        let snapshot = NamespaceContextSnapshot::default();
419        let tree = Asttree::compile_selector(
420            "foo",
421            &snapshot,
422            &table,
423            Some("http://example.com/default"),
424            None,
425            None,
426            XsdVersion::V1_0,
427        )
428        .unwrap();
429        match &tree.paths[0].steps[0] {
430            AstStep::Child(NameTest::QName { namespace, .. }) => {
431                assert_eq!(*namespace, NamespaceMatch::NoNamespace);
432            }
433            other => panic!("expected Child(QName{{NoNamespace, ..}}), got {other:?}"),
434        }
435    }
436
437    #[test]
438    fn compile_selector_v11_applies_own_xpath_default_ns() {
439        // In XSD 1.1 mode, xpathDefaultNamespace should be applied.
440        let table = NameTable::new();
441        let snapshot = NamespaceContextSnapshot::default();
442        let ns_id = table.add("http://example.com/default");
443        let tree = Asttree::compile_selector(
444            "foo",
445            &snapshot,
446            &table,
447            Some("http://example.com/default"),
448            None,
449            None,
450            XsdVersion::V1_1,
451        )
452        .unwrap();
453        match &tree.paths[0].steps[0] {
454            AstStep::Child(NameTest::QName { namespace, .. }) => {
455                assert_eq!(*namespace, NamespaceMatch::Exact(ns_id));
456            }
457            other => panic!("expected Child(QName{{Exact, ..}}), got {other:?}"),
458        }
459    }
460
461    #[test]
462    fn compile_selector_v10_ignores_schema_xpath_default_ns() {
463        // In XSD 1.0 mode, even schema-level xpathDefaultNamespace is ignored.
464        let table = NameTable::new();
465        let schema_ns = table.add("http://example.com/schema");
466        let snapshot = NamespaceContextSnapshot::default();
467        let tree = Asttree::compile_selector(
468            "foo",
469            &snapshot,
470            &table,
471            None,
472            Some(schema_ns),
473            None,
474            XsdVersion::V1_0,
475        )
476        .unwrap();
477        match &tree.paths[0].steps[0] {
478            AstStep::Child(NameTest::QName { namespace, .. }) => {
479                assert_eq!(*namespace, NamespaceMatch::NoNamespace);
480            }
481            other => panic!("expected Child(QName{{NoNamespace, ..}}), got {other:?}"),
482        }
483    }
484
485    #[test]
486    fn compile_field_v10_ignores_xpath_default_ns() {
487        // In XSD 1.0 mode, field compilation ignores xpathDefaultNamespace.
488        let table = NameTable::new();
489        let snapshot = NamespaceContextSnapshot::default();
490        let tree = Asttree::compile_field(
491            "foo",
492            &snapshot,
493            &table,
494            Some("http://example.com/default"),
495            None,
496            None,
497            XsdVersion::V1_0,
498        )
499        .unwrap();
500        match &tree.paths[0].steps[0] {
501            AstStep::Child(NameTest::QName { namespace, .. }) => {
502                assert_eq!(*namespace, NamespaceMatch::NoNamespace);
503            }
504            other => panic!("expected Child(QName{{NoNamespace, ..}}), got {other:?}"),
505        }
506    }
507}