fix(schema): resolve defaulted/fixed IDREF attributes against IDs
cvc-id: when an instance omits an attribute, its default/fixed value applies, so a defaulted or fixed xs:IDREF/xs:IDREFS must still resolve to a matching xs:ID. The reference was recorded only for attributes physically present, so a dangling defaulted IDREF was wrongly accepted. The supplied value is now recorded for document-scoped ID/IDREF checking exactly as a present value would be (shared by the tree and streaming validators, which both call validateAttributes). A required absent attribute is still reported as missing, recordIDs is a no-op for non-ID types, and a defaulted IDREF that does resolve stays valid. Catches idZ012. XSTS invalid-instances-accepted 20 to 19, valid-instances-rejected held at 0.
This commit is contained in:
parent
c83e26a273
commit
9966e254e0
@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- A defaulted or fixed `xs:IDREF`/`xs:IDREFS` attribute now participates in document-scoped ID/IDREF resolution (cvc-id). When the instance omits the attribute its `default`/`fixed` value applies, so a defaulted IDREF must still resolve to a matching `xs:ID`; the reference was recorded only for attributes physically present, so a dangling defaulted IDREF was wrongly accepted. The supplied value is now recorded for ID/IDREF checking exactly as a present value would be (shared by the tree and streaming validators, which both call `validateAttributes`). A required absent attribute is still reported as missing, and a defaulted IDREF that does resolve to an ID stays valid. Catches idZ012. XSTS invalid-instances-accepted 20 → 19, valid-instances-rejected held at 0.
|
||||
- An `xs:all` group with `minOccurs="0"` now requires its members once the group is present in the instance. The group's `minOccurs="0"` makes the GROUP optional (it occurs zero or one times), not each member optional: an absent group (no matching children) is valid, but a present group (any matching child) must still satisfy every member's own `minOccurs`. The parser previously forced each member's `minOccurs` to 0 when the all-group was optional, so a non-empty but incomplete optional all-group (a required member missing) was wrongly accepted. Members now keep their declared occurrence, and the absent-group case is handled at validation time by skipping the required-member check only when the all-group contributes no children at all; a required all-group (`minOccurs="1"`) is unchanged. The fix is applied identically on the tree and streaming paths (both call `allStructureErrors`). Because members now keep their true `minOccurs`, the particle-restriction check (cos-group-restrict) also sees them correctly: a complexContent restriction that relaxes a required member of an optional all-group base is now rejected too, so this catches mgZ001 (instance) and particlesK006 (schema). XSTS invalid-instances-accepted 21 → 20 and invalid-schemas-accepted 18 → 17, valid-instances-rejected and valid-schemas-rejected both held at 0.
|
||||
- A nilled element (an `xsi:nil="true"` on a nillable declaration) whose declaration carries a fixed `{value constraint}` is now rejected during instance validation (cvc-elt.3.2.2). A nillable element MAY declare a fixed value (the schema is valid); the conflict arises only when an instance actually nils such an element, since a nilled element has no value and so cannot take its fixed one. The check rides the existing `nilErrors` chokepoint (shared by the tree and streaming validators), using the matched particle's constraint with the by-local-name element-constraint map as a fallback, so it is enforced identically on both paths; a nillable element that supplies its fixed value, and a nilled element with no fixed value constraint, are both unaffected. Catches addB065. XSTS invalid-instances-accepted 22 → 21, valid-instances-rejected held at 0.
|
||||
- A top-level (global) element or attribute declaration carrying a `form` attribute is now rejected even when it lives in an INCLUDED or IMPORTED document. The `form` attribute is permitted only on a local declaration (XSD 1.0 schema-for-schemas: `topLevelAttribute`/`topLevelElement` have no `form`, since a global component's qualification is fixed); the check (`topLevelDeclarationFindings`, which also enforces a global needing a `name` and forbids the other local-only `use`/`ref`/`minOccurs`/`maxOccurs`) already existed but ran only over the document being compiled, so a violation in an `xs:include`/`xs:import`/`xs:redefine` target was missed and the composed schema wrongly accepted. The same global-component check now runs over the other schema-document roots in the composition (the main document is excluded as already covered, and `redefine` entries are skipped because each schema root walks its own `redefine` children). The constraint is absolute and document-position-independent, so no valid schema is newly rejected; libxml2 rejects both cases for the same reason. Catches attQ016 (import) and attQ018 (include). XSTS invalid-schemas-accepted 20 → 18, valid-schemas-rejected held at 0.
|
||||
|
||||
@ -18,6 +18,12 @@ extension PureXML.Schema.ComplexValidator {
|
||||
recordIDs(use.type, value: match.value, at: path)
|
||||
} else if use.required {
|
||||
errors.append(PureXML.Validation.ValidationError(reason: "missing required attribute '\(use.name.localName)'", at: path))
|
||||
} else if let constraint = use.valueConstraint {
|
||||
// An absent attribute takes its `default`/`fixed` value, so a
|
||||
// defaulted or fixed IDREF/IDREFS must still resolve to a matching
|
||||
// ID (cvc-id): the supplied value participates in document-scoped
|
||||
// ID/IDREF checking exactly as a present value would.
|
||||
recordIDs(use.type, value: constraint.value, at: path)
|
||||
}
|
||||
}
|
||||
validateWildcardAttributes(present, element: element, type: type, at: path, into: &errors)
|
||||
|
||||
56
Tests/SchemaIDREFDefaultTests.swift
Normal file
56
Tests/SchemaIDREFDefaultTests.swift
Normal file
@ -0,0 +1,56 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// A defaulted or fixed `xs:IDREF`/`xs:IDREFS` attribute supplies its value when
|
||||
/// the instance omits the attribute, so that value must still resolve to a
|
||||
/// matching `xs:ID` (cvc-id). The reference was recorded only for present
|
||||
/// attributes, so a dangling defaulted IDREF was wrongly accepted. Mirrors XSTS
|
||||
/// idZ012.
|
||||
@Suite("defaulted/fixed IDREF resolution")
|
||||
struct SchemaIDREFDefaultTests {
|
||||
private func errors(_ xsd: String, _ xml: String) throws -> [PureXML.Validation.ValidationError] {
|
||||
try PureXML.Schema.Document(xsd).validate(xml)
|
||||
}
|
||||
|
||||
private let defaultedIDREFS = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="drefs" type="xs:IDREFS" default="abc"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
|
||||
@Test("a defaulted IDREFS with no matching ID is rejected")
|
||||
func test_danglingDefaultedIDREFS() throws {
|
||||
#expect(try !errors(defaultedIDREFS, "<root/>").isEmpty)
|
||||
}
|
||||
|
||||
private let resolvable = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="t" maxOccurs="unbounded">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="id" type="xs:ID"/>
|
||||
<xs:attribute name="dref" type="xs:IDREF" default="abc"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
|
||||
@Test("a defaulted IDREF that resolves to a present ID stays valid")
|
||||
func test_resolvableDefaultedIDREF() throws {
|
||||
#expect(try errors(resolvable, #"<root><t id="abc"/></root>"#).isEmpty)
|
||||
}
|
||||
|
||||
@Test("a present IDREF with no matching ID is still rejected (unchanged)")
|
||||
func test_danglingPresentIDREF() throws {
|
||||
#expect(try !errors(resolvable, #"<root><t dref="zzz"/></root>"#).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -40,8 +40,9 @@ struct XSTSSuiteTests {
|
||||
/// identity fields compare in decimal value space (default/fixed when absent) and must be simple-content,
|
||||
/// element-children (idK012) or empty-complex (idG006); an attr wildcard is the INTERSECTION; year 0000 invalid;
|
||||
/// a nilled element may not have a fixed value constraint, cvc-elt.3.2.2 (addB065);
|
||||
/// an optional xs:all group present in the instance still requires its members (mgZ001).
|
||||
private let knownInstanceInvalidAccepted = 20
|
||||
/// an optional xs:all group present in the instance still requires its members (mgZ001);
|
||||
/// a defaulted/fixed IDREF/IDREFS must resolve to a matching ID, cvc-id (idZ012).
|
||||
private let knownInstanceInvalidAccepted = 19
|
||||
/// Suspect instance tests excluded from the counts: the Ethiopic-digit `\d`
|
||||
/// cases contradict `\d` = `\p{Nd}` at Unicode 3.2 and are tracked as disputed.
|
||||
private static let specDivergentInstances: Set<String> = ["reS17.v", "reS38.v", "reZ004v.v"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user