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.)
This commit is contained in:
Mihaela Mihaljevic 2026-06-18 01:01:12 +02:00
parent 880137a955
commit 6379e5b25a
4 changed files with 42 additions and 7 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -13,7 +13,7 @@ struct SchemaIdentityXPathTests {
private func compile(_ selector: String, fields: [String] = ["@v"]) throws {
let fieldElements = fields.map { #"<xs:field xpath="\#($0)"/>"# }.joined()
_ = try PureXML.Schema.Document(#"""
<xs:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ns="urn:ns">
<xs:element name="root" type="xs:string"/>
<xs:element name="r">
<xs:complexType><xs:sequence><xs:element ref="root"/></xs:sequence></xs:complexType>
@ -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 = #"<xs:selector xpath="c"/>"#

View File

@ -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