fix(schema): enforce identity-constraint content model; invalid-schemas-accepted 110 to 104 (#145)
An xs:unique / xs:key / xs:keyref content model is (annotation?, selector,
field+): exactly one selector, then one or more fields, in that order. The
structure walk previously checked only that the children were drawn from
{annotation, selector, field}, not their cardinality or order, so a field
before the selector, a second selector, or a missing field was accepted.
identityConstraintContentErrors enforces it, wired into the per-component
structural dispatch. XSTS: invalid-schemas-accepted 110 to 104 (six caught:
idA044/046, idB044/046, idC047/049), valid-schemas-rejected held at 0, no
other bucket moved.
This commit is contained in:
parent
b391db6a74
commit
606bc35a45
@ -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's content model is enforced. `unique`, `key`, and `keyref` must contain exactly one `selector` followed by one or more `field`s (after an optional `annotation`); a `field` before the `selector`, a second `selector`, or no `field` is now rejected (previously only child-element membership was checked, not their cardinality or order). invalid-schemas-accepted 110 → 104, no false positive (valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-schemas-accepted (#145): the occurrence-range constraints on model groups are enforced. A particle's `minOccurs` may not exceed its `maxOccurs`, both defaulting to 1 when absent (`p-props-correct.1`), so `<xs:sequence minOccurs="2">` (2 > the default 1) and `<xs:all maxOccurs="0">` (the default 1 > 0) are rejected; and an `all` group's `maxOccurs` must be 1 and `minOccurs` 0 or 1, with every particle it contains limited to `maxOccurs` 0 or 1 (`cos-all-limited.2`). The previous check fired only when both attributes were present, missing the common default-`maxOccurs` cases. Triple-confirmed against the W3C-labelled suite and libxml2: invalid-schemas-accepted 130 → 110, with no false positive (valid-schemas-rejected held at 0) and no other bucket moved.
|
||||
- 2026-06-16
|
||||
|
||||
|
||||
@ -136,6 +136,8 @@ extension PureXML.Schema.XSDParser {
|
||||
errors += simpleContentOrderErrors(node)
|
||||
case "selector", "field":
|
||||
errors += identityXPathErrors(node, local: local)
|
||||
case "unique", "key", "keyref":
|
||||
errors += identityConstraintContentErrors(names)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -227,6 +227,19 @@ extension PureXML.Schema.XSDParser {
|
||||
return errors
|
||||
}
|
||||
|
||||
/// The content model of an identity constraint (`unique`/`key`/`keyref`) is
|
||||
/// `(annotation?, selector, field+)`: exactly one `selector`, then one or more
|
||||
/// `field`s, in that order. A `field` before the `selector`, a second
|
||||
/// `selector`, or no `field` is invalid. (`names` is already the XSD-namespace
|
||||
/// children; misplaced annotation is checked by the general annotation rule.)
|
||||
static func identityConstraintContentErrors(_ names: [String]) -> [String] {
|
||||
let meaningful = names.filter { $0 != "annotation" }
|
||||
let wellFormed = meaningful.first == "selector"
|
||||
&& meaningful.count >= 2
|
||||
&& meaningful.dropFirst().allSatisfy { $0 == "field" }
|
||||
return wellFormed ? [] : ["an identity constraint must contain one selector followed by one or more fields"]
|
||||
}
|
||||
|
||||
static func groupChildrenErrors(_: XSDTree, names: [String]) -> [String] {
|
||||
var errors: [String] = []
|
||||
let compositors = names.count { $0 == "all" || $0 == "choice" || $0 == "sequence" }
|
||||
|
||||
@ -68,4 +68,38 @@ struct SchemaIdentityXPathTests {
|
||||
#expect(rejectsField(field), "expected rejection: '\(field)'")
|
||||
}
|
||||
}
|
||||
|
||||
/// The content model of `unique`/`key`/`keyref` is `(annotation?, selector,
|
||||
/// field+)`: a field before the selector, a second selector, or no field is
|
||||
/// invalid, while one selector followed by one or more fields (optionally after
|
||||
/// an annotation) is valid.
|
||||
private func compilesConstraint(_ body: String) -> Bool {
|
||||
let document = #"""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="r">
|
||||
<xs:complexType><xs:sequence><xs:element name="c" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:unique name="u">\#(body)</xs:unique>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""#
|
||||
return (try? PureXML.Schema.Document(document)) != nil
|
||||
}
|
||||
|
||||
@Test("an identity constraint requires one selector then one or more fields")
|
||||
func test_identityConstraintContentModel() {
|
||||
let selector = #"<xs:selector xpath="c"/>"#
|
||||
let field = #"<xs:field xpath="@v"/>"#
|
||||
let annotation = "<xs:annotation><xs:documentation>x</xs:documentation></xs:annotation>"
|
||||
// Valid shapes.
|
||||
#expect(compilesConstraint(selector + field))
|
||||
#expect(compilesConstraint(selector + field + field))
|
||||
#expect(compilesConstraint(annotation + selector + field))
|
||||
// A field before the selector (idA044 shape).
|
||||
#expect(!compilesConstraint(field + selector))
|
||||
// Two selectors (idA046 shape).
|
||||
#expect(!compilesConstraint(selector + selector + field))
|
||||
// No field, or no selector.
|
||||
#expect(!compilesConstraint(selector))
|
||||
#expect(!compilesConstraint(field))
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,8 +145,11 @@ struct XSTSSuiteTests {
|
||||
/// minOccurs may not exceed maxOccurs, both defaulting to 1; cos-all-limited.2: an
|
||||
/// all group's maxOccurs is 1 and minOccurs 0 or 1, its particles' maxOccurs 0 or
|
||||
/// 1) then caught twenty more invalid schemas with no false positive: 130 → 110.
|
||||
/// Enforcing the identity-constraint content model (`unique`/`key`/`keyref` is
|
||||
/// `(annotation?, selector, field+)`: exactly one selector, then one or more
|
||||
/// fields, in order) caught six more with no false positive: 110 → 104.
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 110
|
||||
private let knownSchemaInvalidAccepted = 104
|
||||
private let knownInstanceValidRejected = 0
|
||||
private let knownInstanceInvalidAccepted = 133
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user