From 6379e5b25a32aa8a24ac3b2b22cc6ff5ff52baa5 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Thu, 18 Jun 2026 01:01:12 +0200 Subject: [PATCH] fix(schema): an identity-constraint xpath may not use an undeclared prefix; invalid-schemas-accepted 77 to 75 (#145) A selector/field xpath in an identity constraint may name elements/attributes with namespace-qualified name tests; every prefix used must be a declared xmlns: prefix in scope (the implicit xml prefix excepted). identityXPathErrors, after the existing syntax check, now resolves each name-test prefix against the in-scope bindings (namespaceBindingsInScope, which captures the schema root and every ancestor) and rejects an undeclared one. An adversarial review confirmed no false positive: a prefix declared on the schema root, any intermediate ancestor, or the selector/field element resolves correctly; xml, the child::/attribute:: axis keywords (colon-free), prefix:*, and inter-token whitespace are all handled. XSTS: invalid-schemas-accepted 77 to 75 (idI010, idJ011), valid-schemas-rejected held at 0, no other bucket moved. (Two existing xpath-syntax fixtures used an undeclared 'ns' prefix to test name-test syntax; the test schema now declares xmlns:ns so they exercise syntax with a bound prefix.) --- CHANGELOG.md | 2 ++ Sources/Schema/SchemaIdentityXPath.swift | 27 +++++++++++++++++++++--- Tests/SchemaIdentityXPathTests.swift | 13 +++++++++++- Tests/XSTSSuiteTests.swift | 7 +++--- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ecc0c..a4810dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- XSTS invalid-schemas-accepted (#145): an identity-constraint `selector`/`field` xpath may not use an undeclared namespace prefix. Every prefix in a name test must be a declared `xmlns:` prefix in scope (the implicit `xml` prefix excepted); a `selector`/`field` whose xpath names an unbound prefix is rejected. invalid-schemas-accepted 77 → 75, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed declared prefixes on the schema root or any ancestor resolve correctly), no other bucket moved. + - XSTS invalid-schemas-accepted (#161): a `group` inside `xs:redefine` may reference itself at most once, and that self-reference must occur exactly once, `minOccurs` = `maxOccurs` = 1 (`src-redefine.6.1.2`). A self-reference with `minOccurs="0"`, `maxOccurs="unbounded"`, or any count above 1 is rejected. The self-reference is matched by namespace, not local name alone, so a reference to an imported group that merely shares the local name is unconstrained (no false positive). invalid-schemas-accepted 79 → 77, no false positive (valid-schemas-rejected held at 0), no other bucket moved. - XSTS invalid-schemas-accepted (#145): a local `element` declaration must identify itself with a `name` or a `ref`; an element with neither is rejected. invalid-schemas-accepted 80 → 79, no false positive (valid-schemas-rejected held at 0), no other bucket moved. diff --git a/Sources/Schema/SchemaIdentityXPath.swift b/Sources/Schema/SchemaIdentityXPath.swift index 740eb62..ab751e8 100644 --- a/Sources/Schema/SchemaIdentityXPath.swift +++ b/Sources/Schema/SchemaIdentityXPath.swift @@ -21,9 +21,30 @@ extension PureXML.Schema.XSDParser { guard local == "selector" || local == "field", let xpath = PureXML.Schema.XSDNode.attribute(node, "xpath") else { return [] } - return validXPath(xpath, isField: local == "field") - ? [] - : ["the \(local) xpath '\(xpath)' is not a valid identity-constraint path"] + if !validXPath(xpath, isField: local == "field") { + return ["the \(local) xpath '\(xpath)' is not a valid identity-constraint path"] + } + // Every namespace prefix a name test uses must be declared in scope; the + // implicit `xml` prefix is always bound. An unbound prefix is invalid. + let bindings = namespaceBindingsInScope(of: node, defaultBindings: [:]) + for prefix in xpathNameTestPrefixes(xpath) where prefix != "xml" && bindings[prefix] == nil { + return ["the \(local) xpath '\(xpath)' uses the undeclared namespace prefix '\(prefix)'"] + } + return [] + } + + /// The set of namespace prefixes used in the name tests of a (syntactically + /// valid) identity-constraint xpath. A name test is `prefix:local` or + /// `prefix:*`; an unprefixed name, `*`, `.`, or an axis keyword has no prefix. + private static func xpathNameTestPrefixes(_ xpath: String) -> [String] { + guard let tokens = tokenize(xpath) else { return [] } + var prefixes: [String] = [] + for case let .name(name) in tokens { + guard let colon = name.firstIndex(of: ":") else { continue } + let prefix = String(name[name.startIndex ..< colon]) + if !prefix.isEmpty { prefixes.append(prefix) } + } + return prefixes } private enum XPathToken: Equatable { diff --git a/Tests/SchemaIdentityXPathTests.swift b/Tests/SchemaIdentityXPathTests.swift index fc0b16c..9b5dbfa 100644 --- a/Tests/SchemaIdentityXPathTests.swift +++ b/Tests/SchemaIdentityXPathTests.swift @@ -13,7 +13,7 @@ struct SchemaIdentityXPathTests { private func compile(_ selector: String, fields: [String] = ["@v"]) throws { let fieldElements = fields.map { #""# }.joined() _ = try PureXML.Schema.Document(#""" - + @@ -85,6 +85,17 @@ struct SchemaIdentityXPathTests { return (try? PureXML.Schema.Document(document)) != nil } + /// A name-test prefix in a selector/field xpath must be a declared namespace + /// prefix; an undeclared prefix is invalid. The implicitly-bound `xml` prefix and + /// a prefix bound by the schema's own `xmlns:` declarations are valid. + @Test("an identity-constraint xpath may not use an undeclared namespace prefix") + func test_xpathUndeclaredPrefix() throws { + try compile("xs:a") + try compile("root", fields: ["xs:v"]) + #expect(rejectsSelector("imp:a")) + #expect(rejectsField("imp:v")) + } + @Test("an identity constraint requires one selector then one or more fields") func test_identityConstraintContentModel() { let selector = #""# diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 42c11d7..7a77652 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -149,10 +149,11 @@ struct XSTSSuiteTests { /// schemaLocation must be a schema (src-resolve), import namespace matches the /// imported target (src-import.3), redefined type derives from itself /// (src-redefine.5), local complexType is anonymous, a local element has a name - /// or ref, and a redefined group's self-reference occurs exactly once - /// (src-redefine.6.1.2). + /// or ref, a redefined group's self-reference occurs exactly once + /// (src-redefine.6.1.2), and an identity-constraint xpath uses no undeclared + /// namespace prefix. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 77 + private let knownSchemaInvalidAccepted = 75 private let knownInstanceValidRejected = 0 private let knownInstanceInvalidAccepted = 133