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