fix(schema): reject substitution-member swaps in particle restriction
NameAndTypeOK clause 1 (XSD 1.0 section 3.9.6, Elt:Elt) pairs the declared
element declarations and requires the same {name} and {target namespace};
no clause grants substitution an allowance. The substitution-group
expansion rewrites the compiled term (an abstract head is dropped, members
join a synthetic choice), so the checker previously compared the expanded
names and a head-for-member swap looked name-identical.
Particle.declaredReference now preserves the pre-expansion identity of an
element-ref particle, and the restriction check pairs the declared
references before the structural pairing table (extracted as
structuralValid). Closes XSTS elemZ026 and particlesV020:
invalid-schemas-accepted 8 -> 6, valid buckets unchanged at zero.
Stricter than libxml2, which does not implement this edge; elemZ026 is
pinned in the differential baseline accordingly.
particlesZ008, byte-for-byte the same construct as elemZ026 but carrying
a settled valid expectation contradicting elemZ026's settled invalid, is
reclassified as a spec-divergent schema test with the adjudication
recorded in the suite (exclusion tables split into
Tests/XSTSSuiteExclusions.swift for file length).
Separately attributed: the differential baseline gains ten further pinned
entries (addB070a, addB108, addB177, attO025, elemZ027_c, errC008,
groupH021v, notatB005, notatE002, particlesEb040) that reproduce with
this change stashed. They are pre-existing drift from earlier main-line
fixes, not effects of this commit. Baseline 231 -> 242, differential
green.
This commit is contained in:
parent
f7d8fa16d1
commit
2e3d2e25f8
17
CHANGELOG.md
17
CHANGELOG.md
@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- A complex-type restriction that swaps an element reference for one of its
|
||||
substitution-group members is now rejected (NameAndTypeOK, XSD 1.0 section
|
||||
3.9.6 Elt:Elt clause 1: the declared names must match, and no clause grants
|
||||
substitution an allowance in particle restriction). The substitution-group
|
||||
expansion rewrites the compiled term (an abstract head is dropped, members
|
||||
join a synthetic choice), so the checker previously compared the expanded
|
||||
names and the swap looked name-identical; `Particle.declaredReference` now
|
||||
preserves the pre-expansion identity and the restriction check pairs the
|
||||
declared references first. Closes XSTS elemZ026 and particlesV020
|
||||
(invalid-schemas-accepted 8 -> 6); stricter than libxml2, which does not
|
||||
implement this edge. particlesZ008, byte-for-byte the same construct with a
|
||||
settled `valid` expectation contradicting elemZ026's settled `invalid`, is
|
||||
reclassified as a spec-divergent schema test with the adjudication recorded
|
||||
in the suite. Valid buckets are unchanged at zero.
|
||||
|
||||
### Changed
|
||||
|
||||
- Rebuild the issue tracker on Codeberg after the GitHub tracker loss: the
|
||||
|
||||
@ -224,6 +224,12 @@ public extension PureXML.Schema {
|
||||
struct Particle: Sendable {
|
||||
public var occurrenceRange: OccurrenceRange
|
||||
public var term: Term
|
||||
/// The declared global-element reference this particle was compiled from, when it
|
||||
/// came from `<xs:element ref="..."/>`. Substitution-group expansion rewrites the
|
||||
/// term (an abstract head is dropped, members join a synthetic choice), but XSD 1.0
|
||||
/// section 3.9.6 defines particle restriction over the DECLARED particles, so the
|
||||
/// restriction check needs the pre-expansion identity the term no longer carries.
|
||||
public var declaredReference: PureXML.Model.QualifiedName?
|
||||
|
||||
public var minOccurs: Int {
|
||||
get { occurrenceRange.minimum.clamped(to: Int.max) }
|
||||
@ -235,14 +241,16 @@ public extension PureXML.Schema {
|
||||
set { occurrenceRange.maximum = OccurrenceUpper(newValue) }
|
||||
}
|
||||
|
||||
public init(minOccurs: Int = 1, maxOccurs: Int? = 1, term: Term) {
|
||||
public init(minOccurs: Int = 1, maxOccurs: Int? = 1, term: Term, declaredReference: PureXML.Model.QualifiedName? = nil) {
|
||||
occurrenceRange = OccurrenceRange(minimum: minOccurs, maximum: maxOccurs)
|
||||
self.term = term
|
||||
self.declaredReference = declaredReference
|
||||
}
|
||||
|
||||
public init(occurrenceRange: OccurrenceRange, term: Term) {
|
||||
public init(occurrenceRange: OccurrenceRange, term: Term, declaredReference: PureXML.Model.QualifiedName? = nil) {
|
||||
self.occurrenceRange = occurrenceRange
|
||||
self.term = term
|
||||
self.declaredReference = declaredReference
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,20 @@ extension PureXML.Schema {
|
||||
if contentFree(restricted) {
|
||||
return emptiable(base)
|
||||
}
|
||||
// NameAndTypeOK clause 1 over the DECLARED references: when both particles were
|
||||
// compiled from `<xs:element ref="..."/>`, section 3.9.6 pairs the declared
|
||||
// element declarations, and Elt:Elt requires the same {name} and {target
|
||||
// namespace}. Substitution has no allowance here: a restriction that swaps a
|
||||
// head for one of its substitution-group members names a different declaration
|
||||
// and is invalid (elemZ026), even though the expanded terms would line up.
|
||||
return declaredReferencesCompatible(restricted, base)
|
||||
&& structuralValid(restricted, base, types, derivation)
|
||||
}
|
||||
|
||||
/// The section 3.9.6 pairing table over the compiled terms, dispatched by the
|
||||
/// (derived, base) term kinds once the outer gates (content-free, declared
|
||||
/// references) have run.
|
||||
private static func structuralValid(_ restricted: Particle, _ base: Particle, _ types: [String: ElementType], _ derivation: [String: TypeDerivation]) -> Bool {
|
||||
switch (restricted.term, base.term) {
|
||||
case let (.element(rName, _, rTypeName, rValue, rBlock, rNil), .element(bName, bType, bTypeName, bValue, bBlock, bNil)):
|
||||
// NameAndTypeOK: beyond name, occurrence, and type derivation, the
|
||||
@ -285,6 +299,19 @@ extension PureXML.Schema {
|
||||
}
|
||||
}
|
||||
|
||||
/// NameAndTypeOK clause 1 over the DECLARED references: when both particles were
|
||||
/// compiled from `<xs:element ref="..."/>`, the declared names must match. A
|
||||
/// particle without a declared reference (a local element, a group written out
|
||||
/// in place) is unconstrained here and checked by its structural case instead.
|
||||
private static func declaredReferencesCompatible(_ restricted: Particle, _ base: Particle) -> Bool {
|
||||
guard let restrictedDeclared = restricted.declaredReference,
|
||||
let baseDeclared = base.declaredReference
|
||||
else {
|
||||
return true
|
||||
}
|
||||
return sameName(restrictedDeclared, baseDeclared)
|
||||
}
|
||||
|
||||
private static func sameName(_ lhs: PureXML.Model.QualifiedName, _ rhs: PureXML.Model.QualifiedName) -> Bool {
|
||||
lhs.localName == rhs.localName && (lhs.namespaceURI ?? "") == (rhs.namespaceURI ?? "")
|
||||
}
|
||||
|
||||
@ -176,11 +176,22 @@ extension PureXML.Schema.XSDParser {
|
||||
// and namespace so the choice over the head and its members keeps every
|
||||
// member's own namespace, even across imported namespaces.
|
||||
let alternatives = head + (context.substitutions[headKey] ?? []).map { PureXML.Schema.XSDParser.unpackElementName($0) }
|
||||
// The declared identity survives the substitution expansion: section 3.9.6 checks
|
||||
// particle restriction over the declared references, not the expanded terms.
|
||||
let declared = PureXML.Model.QualifiedName(localName: name, namespaceURI: refNamespace)
|
||||
if alternatives.count == 1 {
|
||||
return PureXML.Schema.Particle(occurrenceRange: occurrence, term: elementReferenceTerm(alternatives[0].0, alternatives[0].1, context))
|
||||
return PureXML.Schema.Particle(
|
||||
occurrenceRange: occurrence,
|
||||
term: elementReferenceTerm(alternatives[0].0, alternatives[0].1, context),
|
||||
declaredReference: declared,
|
||||
)
|
||||
}
|
||||
let members = alternatives.map { PureXML.Schema.Particle(term: elementReferenceTerm($0.0, $0.1, context)) }
|
||||
return PureXML.Schema.Particle(occurrenceRange: occurrence, term: .group(.init(compositor: .choice, particles: members)))
|
||||
return PureXML.Schema.Particle(
|
||||
occurrenceRange: occurrence,
|
||||
term: .group(.init(compositor: .choice, particles: members)),
|
||||
declaredReference: declared,
|
||||
)
|
||||
}
|
||||
|
||||
/// The term for a reference to a global element, in the resolved `namespace`.
|
||||
|
||||
@ -12,17 +12,20 @@ addB033:../msData/additional/test71774.xsd
|
||||
addB036:../msData/additional/test71847.xsd
|
||||
addB053:../msData/additional/test73457.xsd
|
||||
addB064:../msData/additional/test73722.xsd
|
||||
addB070a:../msData/additional/test64756.xsd
|
||||
addB071:../msData/additional/test65026.xsd
|
||||
addB082:../msData/additional/addB082.xsd
|
||||
addB089:../msData/additional/addB089.xsd
|
||||
addB099:../msData/additional/addB099.xsd
|
||||
addB100:../msData/additional/addB100.xsd
|
||||
addB106:../msData/additional/addB106.xsd
|
||||
addB108:../msData/additional/addB108.xsd
|
||||
addB113:../msData/additional/addB113.xsd
|
||||
addB118:../msData/additional/test74966.xsd
|
||||
addB121:../msData/additional/addB121.xsd
|
||||
addB141:../msData/additional/test82919.xsd
|
||||
addB153:../msData/additional/test86490_a.xsd
|
||||
addB177:../msData/additional/test102850_2.xsd
|
||||
addB178:../msData/additional/test102850_3.xsd
|
||||
addB194:../msData/additional/addB194.xsd
|
||||
addB32:../msData/additional/test67764.xsd
|
||||
@ -42,6 +45,7 @@ attKb015:../msData/attribute/attKb015.xsd
|
||||
attKb018a:../msData/attribute/attKb018a.xsd
|
||||
attKc012:../msData/attribute/attKc012.xsd
|
||||
attKc015:../msData/attribute/attKc015.xsd
|
||||
attO025:../msData/attribute/attO025.xsd
|
||||
attQ011:../msData/attribute/attQ011.xsd
|
||||
attQ016:../msData/attribute/attQ016.xsd
|
||||
attQ017:../msData/attribute/attQ017.xsd
|
||||
@ -95,11 +99,14 @@ elemG004:../msData/element/elemG004.xsd
|
||||
elemP005:../msData/element/elemP005.xsd
|
||||
elemZ004:../msData/element/elemZ004.xsd
|
||||
elemZ012:../msData/element/elemZ012.xsd
|
||||
elemZ026:../msData/element/elemZ026.xsd
|
||||
elemZ027_c:../msData/element/elemZ027c.xsd
|
||||
elemZ028b:../msData/element/elemZ028b.xsd
|
||||
elemZ028e:../msData/element/elemZ028e.xsd
|
||||
elemZ032b:../msData/element/elemZ032b.xsd
|
||||
errC005:../msData/errata10/errC005.xsd
|
||||
errC006:../msData/errata10/errC006.xsd
|
||||
errC008:../msData/errata10/errC008.xsd
|
||||
errC009:../msData/errata10/errC009.xsd
|
||||
gMonth_enumeration001_1275:../msData/datatypes/Facets/gMonth/gMonth_enumeration001.xsd
|
||||
gMonth_enumeration002_1276:../msData/datatypes/Facets/gMonth/gMonth_enumeration002.xsd
|
||||
@ -132,6 +139,7 @@ groupF009v:../msData/group/groupF009.xsd
|
||||
groupG002:../msData/group/groupG002.xsd
|
||||
groupH007v:../msData/group/groupH007.xsd
|
||||
groupH009v:../msData/group/groupH009.xsd
|
||||
groupH021v:../msData/group/groupH021.xsd
|
||||
groupI002:../msData/group/groupI002.xsd
|
||||
groupJ009v:../msData/group/groupJ009.xsd
|
||||
groupK002:../msData/group/groupK002.xsd
|
||||
@ -193,12 +201,14 @@ nonPositiveInteger_fractionDigits007_602:../msData/datatypes/Facets/Schemas/nonP
|
||||
normalizedString_minLength006_334:../msData/datatypes/Facets/Schemas/normalizedString_minLength006.xsd
|
||||
normalizedString_whitespace001_344:../msData/datatypes/Facets/Schemas/normalizedString_whitespace001.xsd
|
||||
notatB001:../msData/notations/notatB001.xsd
|
||||
notatB005:../msData/notations/notatB005.xsd
|
||||
notatB008:../msData/notations/notatB008.xsd
|
||||
notatB009:../msData/notations/notatB009.xsd
|
||||
notatB010:../msData/notations/notatB010.xsd
|
||||
notatB011:../msData/notations/notatB011.xsd
|
||||
notatB012:../msData/notations/notatB012.xsd
|
||||
notatB013:../msData/notations/notatB013.xsd
|
||||
notatE002:../msData/notations/notatE002.xsd
|
||||
notatE003:../msData/notations/notatE003.xsd
|
||||
notatG001:../msData/notations/notatG001.xsd
|
||||
notatG002:../msData/notations/notatG002.xsd
|
||||
@ -207,6 +217,7 @@ particlesDa011:../msData/particles/particlesDa011.xsd
|
||||
particlesEa023:../msData/particles/particlesEa023.xsd
|
||||
particlesEa025:../msData/particles/particlesEa025.xsd
|
||||
particlesEb015:../msData/particles/particlesEb015.xsd
|
||||
particlesEb040:../msData/particles/particlesEb040.xsd
|
||||
particlesEc009:../msData/particles/particlesEc009.xsd
|
||||
positiveInteger_fractionDigits004_1014:../msData/datatypes/Facets/Schemas/positiveInteger_fractionDigits004.xsd
|
||||
positiveInteger_fractionDigits007_1017:../msData/datatypes/Facets/Schemas/positiveInteger_fractionDigits007.xsd
|
||||
|
||||
61
Tests/XSDParticleSubstitutionRestrictionTests.swift
Normal file
61
Tests/XSDParticleSubstitutionRestrictionTests.swift
Normal file
@ -0,0 +1,61 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
@Suite("XSD Particle Restriction over substitution groups")
|
||||
struct XSDParticleSubstitutionRestrictionTests {
|
||||
private let xsNamespace = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
|
||||
|
||||
@Test("A restriction may not swap an element ref for a substitution-group member")
|
||||
func test_substitutionMemberSwapRejected() {
|
||||
// elemZ026: NameAndTypeOK clause 1 pairs the DECLARED references, and requires
|
||||
// the same name and target namespace; substitution has no allowance in Elt:Elt.
|
||||
// Swapping the head ref for a member ref names a different declaration.
|
||||
let schema = """
|
||||
<xs:schema \(xsNamespace)>
|
||||
<xs:complexType name="headType" abstract="true">
|
||||
<xs:sequence><xs:element name="leaf" type="xs:token"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:element name="head" type="headType" abstract="true"/>
|
||||
<xs:element name="member" type="headType" substitutionGroup="head"/>
|
||||
<xs:complexType name="B">
|
||||
<xs:sequence><xs:element ref="head" maxOccurs="unbounded"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="R">
|
||||
<xs:complexContent><xs:restriction base="B">
|
||||
<xs:sequence><xs:element ref="member" maxOccurs="unbounded"/></xs:sequence>
|
||||
</xs:restriction></xs:complexContent>
|
||||
</xs:complexType>
|
||||
<xs:element name="r" type="R"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
#expect(throws: (any Error).self) {
|
||||
try PureXML.Schema.Document(schema)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A restriction that repeats the declared ref and narrows occurrence compiles")
|
||||
func test_sameRefNarrowedOccurrenceCompiles() throws {
|
||||
// The positive control for the declared-reference check: the same head ref with
|
||||
// a narrowed range is the spec's idiom, and the substitution expansion behind
|
||||
// both sides must not disturb it.
|
||||
let schema = """
|
||||
<xs:schema \(xsNamespace)>
|
||||
<xs:complexType name="headType" abstract="true">
|
||||
<xs:sequence><xs:element name="leaf" type="xs:token"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:element name="head" type="headType" abstract="true"/>
|
||||
<xs:element name="member" type="headType" substitutionGroup="head"/>
|
||||
<xs:complexType name="B">
|
||||
<xs:sequence><xs:element ref="head" minOccurs="0" maxOccurs="unbounded"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="R">
|
||||
<xs:complexContent><xs:restriction base="B">
|
||||
<xs:sequence><xs:element ref="head" maxOccurs="2"/></xs:sequence>
|
||||
</xs:restriction></xs:complexContent>
|
||||
</xs:complexType>
|
||||
<xs:element name="r" type="R"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
_ = try PureXML.Schema.Document(schema)
|
||||
}
|
||||
}
|
||||
85
Tests/XSTSSuiteExclusions.swift
Normal file
85
Tests/XSTSSuiteExclusions.swift
Normal file
@ -0,0 +1,85 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// The suite exclusions: tests whose expected validity is not authoritative,
|
||||
/// mirroring how the suite's own `status="queried"` entries are skipped. Kept in
|
||||
/// their own file so each entry's spec-grounded justification has room to be full.
|
||||
extension XSTSSuiteTests {
|
||||
/// Instance tests excluded from the counts because their expected validity is not
|
||||
/// authoritative, mirroring how the suite's own `status="queried"` entries are
|
||||
/// skipped. Each must carry a spec-grounded justification; this is never a place to
|
||||
/// hide a real deviation.
|
||||
///
|
||||
/// - `anyURI_a004_1339.i`: the suite pairs a `valid` schemaTest with an `invalid`
|
||||
/// instanceTest, but the two are mutually inconsistent. The schema's `include`,
|
||||
/// `import`, and `redefine` have unresolvable schemaLocations; XSD 1.0 §4.2.1
|
||||
/// makes an unresolvable `include` "not an error" (no inclusion performed), the
|
||||
/// `import` likewise, and the empty `redefine` imposes no resolution requirement
|
||||
/// (`src-redefine` clause 1 gates that on non-annotation children), so the schema
|
||||
/// is conformantly valid (agreeing with the schemaTest). The only components the
|
||||
/// instance exercises (`root`, `bar`, `st`, `att`) are fully locally defined, and
|
||||
/// every instance value is byte-identical to one of the type's six anyURI
|
||||
/// `enumeration` members, so a strict assessment of the document element is
|
||||
/// valid. There is no spec path to valid-schema + invalid-instance. Confirmed by
|
||||
/// two independent adversarial analyses; libxml2 is no authority here (it
|
||||
/// non-conformantly fails to compile the schema on the unresolvable include).
|
||||
/// Matching the suite would mean rejecting a valid instance whenever an include
|
||||
/// cannot be resolved, a false positive an IDE editing loop hits constantly.
|
||||
///
|
||||
/// - `schA2.i`, `schA5.i`: schema-location precedence. Each group's documentation
|
||||
/// states the inline `xsi:schemaLocation` hint "is ignored" and the instance
|
||||
/// "must conform to" the collection schema, but under exactly that prescribed
|
||||
/// behavior the instances are valid (libxml2 validates schA2.xml against its
|
||||
/// `a.xsd`, and schA5.xml against its `a.xsd`+`b.xsd` collection), so the
|
||||
/// `invalid` expectation contradicts the group's own documentation. Reaching
|
||||
/// `invalid` requires letting the instance's hint override a schema already
|
||||
/// loaded for that namespace, which XSD 1.0 §4.3.2 makes optional (strategy 1:
|
||||
/// "do nothing ... because a schema ... is already known") and which would be a
|
||||
/// direct false positive for an IDE (an instance file forcing rejection against
|
||||
/// the project's own schema). PureXML's policy (honor hints but keep the
|
||||
/// collection's declarations on conflict) is conformant. Confirmed by an
|
||||
/// adversarial analysis that separated these from schA7.i, whose `invalid` IS
|
||||
/// spec-grounded (a genuine policy deviation, left in the count).
|
||||
///
|
||||
/// - `mgO013.i`: a `redefine` whose group `all{e1(1,1)}` redefines a base
|
||||
/// `all{e1(0,0)}` without referencing itself. `src-redefine` clause 6.2.2 makes
|
||||
/// that a restriction obligation, and `all{e1(1,1)}` is not a valid restriction
|
||||
/// of `all{e1(0,0)}` (Occurrence Range OK: derived max 1 > base max 0), so
|
||||
/// enforcing it rejects the schema, contradicting the `valid` schemaTest and
|
||||
/// producing a valid-schema-rejected false positive. Yet §4.2.2 makes the type's
|
||||
/// `group` reference resolve to the redefined `all{e1(1,1)}`, against which the
|
||||
/// single-`e1` instance is valid. The `valid` schema and `invalid` instance
|
||||
/// expectations are mutually exclusive; libxml2 accepts both. Confirmed by an
|
||||
/// adversarial analysis that found no third path.
|
||||
///
|
||||
/// - `particlesZ001.i`: a `complexContent` restriction `Derived` of `Base`, with an
|
||||
/// instance valid against `Derived`'s own content model. cvc-complex-type (§3.4.4)
|
||||
/// validates an instance against the element's own `{content type}` and never
|
||||
/// re-runs Particle Valid (Restriction) / RecurseAsIfGroup (§3.9.6, a schema-
|
||||
/// assembly constraint), so schema-valid + instance-invalid is unreachable. The
|
||||
/// manifest itself labels the governing rule ambiguous; libxml2 accepts both.
|
||||
/// Reaching `invalid` would require rejecting the schema (a valid-schema-rejected
|
||||
/// false positive) and would still fail the suite's own `valid` schemaTest.
|
||||
static let specDivergentInstances: Set<String> = [
|
||||
"anyURI_a004_1339.i", "schA2.i", "schA5.i", "mgO013.i", "particlesZ001.i",
|
||||
]
|
||||
|
||||
/// Schema tests excluded from the counts for the same reason: the expectation is
|
||||
/// not authoritative. Each entry must name the settled expectation it is jointly
|
||||
/// unsatisfiable with; this is never a place to hide a real deviation.
|
||||
///
|
||||
/// - `particlesZ008`: byte-for-byte the same construct as `elemZ026` (an abstract
|
||||
/// head element, a member via `substitutionGroup`, a base type whose sequence
|
||||
/// references the head, and a restriction that references the member instead),
|
||||
/// yet Z008's settled expectation is `valid` while elemZ026's settled expectation
|
||||
/// is `invalid`, both accepted on the same date. One expectation must be wrong,
|
||||
/// and the spec decides which: NameAndTypeOK (section 3.9.6, Elt:Elt) clause 1
|
||||
/// requires the declared {name}s and {target namespace}s to be the same, and no
|
||||
/// clause grants substitution an allowance in particle restriction, so the member
|
||||
/// swap is invalid and Z008's `valid` has no spec text behind it (its annotation
|
||||
/// asserts "should be valid" without citing one). The schema is still compiled
|
||||
/// so the group's instance tests run; only its verdict is excluded.
|
||||
static let specDivergentSchemas: Set<String> = [
|
||||
"particlesZ008",
|
||||
]
|
||||
}
|
||||
@ -50,7 +50,16 @@ struct XSTSSuiteTests {
|
||||
/// or of one of its members still passes. (Ik025/Ik027, where the restricting
|
||||
/// type derives from a union member, are accepted per cos-st-derived-ok 2.2.4,
|
||||
/// a disclosed divergence from their contested XSTS `invalid` expectation.)
|
||||
private let knownSchemaInvalidAccepted = 8
|
||||
/// 8 -> 6: NameAndTypeOK clause 1 now pairs the DECLARED element references. The
|
||||
/// substitution-group expansion rewrites the compiled term (an abstract head is
|
||||
/// dropped, members join a synthetic choice), so the checker saw the expanded
|
||||
/// names and a restriction that swapped a head reference for a member reference
|
||||
/// looked name-identical. `Particle.declaredReference` preserves the
|
||||
/// pre-expansion identity and the restriction check rejects a declared-reference
|
||||
/// mismatch (elemZ026, particlesV020). particlesZ008, the identical construct
|
||||
/// with a settled `valid` expectation contradicting elemZ026's settled `invalid`,
|
||||
/// is reclassified into `specDivergentSchemas` below.
|
||||
private let knownSchemaInvalidAccepted = 6
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared
|
||||
/// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's
|
||||
@ -99,64 +108,6 @@ struct XSTSSuiteTests {
|
||||
/// `invalid` is unsatisfiable jointly with its own `valid` schemaTest, and the
|
||||
/// manifest itself labels the governing restriction rule ambiguous.
|
||||
private let knownInstanceInvalidAccepted = 4
|
||||
/// Instance tests excluded from the counts because their expected validity is not
|
||||
/// authoritative, mirroring how the suite's own `status="queried"` entries are
|
||||
/// skipped. Each must carry a spec-grounded justification; this is never a place to
|
||||
/// hide a real deviation.
|
||||
///
|
||||
/// - `anyURI_a004_1339.i`: the suite pairs a `valid` schemaTest with an `invalid`
|
||||
/// instanceTest, but the two are mutually inconsistent. The schema's `include`,
|
||||
/// `import`, and `redefine` have unresolvable schemaLocations; XSD 1.0 §4.2.1
|
||||
/// makes an unresolvable `include` "not an error" (no inclusion performed), the
|
||||
/// `import` likewise, and the empty `redefine` imposes no resolution requirement
|
||||
/// (`src-redefine` clause 1 gates that on non-annotation children), so the schema
|
||||
/// is conformantly valid (agreeing with the schemaTest). The only components the
|
||||
/// instance exercises (`root`, `bar`, `st`, `att`) are fully locally defined, and
|
||||
/// every instance value is byte-identical to one of the type's six anyURI
|
||||
/// `enumeration` members, so a strict assessment of the document element is
|
||||
/// valid. There is no spec path to valid-schema + invalid-instance. Confirmed by
|
||||
/// two independent adversarial analyses; libxml2 is no authority here (it
|
||||
/// non-conformantly fails to compile the schema on the unresolvable include).
|
||||
/// Matching the suite would mean rejecting a valid instance whenever an include
|
||||
/// cannot be resolved, a false positive an IDE editing loop hits constantly.
|
||||
///
|
||||
/// - `schA2.i`, `schA5.i`: schema-location precedence. Each group's documentation
|
||||
/// states the inline `xsi:schemaLocation` hint "is ignored" and the instance
|
||||
/// "must conform to" the collection schema, but under exactly that prescribed
|
||||
/// behavior the instances are valid (libxml2 validates schA2.xml against its
|
||||
/// `a.xsd`, and schA5.xml against its `a.xsd`+`b.xsd` collection), so the
|
||||
/// `invalid` expectation contradicts the group's own documentation. Reaching
|
||||
/// `invalid` requires letting the instance's hint override a schema already
|
||||
/// loaded for that namespace, which XSD 1.0 §4.3.2 makes optional (strategy 1:
|
||||
/// "do nothing ... because a schema ... is already known") and which would be a
|
||||
/// direct false positive for an IDE (an instance file forcing rejection against
|
||||
/// the project's own schema). PureXML's policy (honor hints but keep the
|
||||
/// collection's declarations on conflict) is conformant. Confirmed by an
|
||||
/// adversarial analysis that separated these from schA7.i, whose `invalid` IS
|
||||
/// spec-grounded (a genuine policy deviation, left in the count).
|
||||
///
|
||||
/// - `mgO013.i`: a `redefine` whose group `all{e1(1,1)}` redefines a base
|
||||
/// `all{e1(0,0)}` without referencing itself. `src-redefine` clause 6.2.2 makes
|
||||
/// that a restriction obligation, and `all{e1(1,1)}` is not a valid restriction
|
||||
/// of `all{e1(0,0)}` (Occurrence Range OK: derived max 1 > base max 0), so
|
||||
/// enforcing it rejects the schema, contradicting the `valid` schemaTest and
|
||||
/// producing a valid-schema-rejected false positive. Yet §4.2.2 makes the type's
|
||||
/// `group` reference resolve to the redefined `all{e1(1,1)}`, against which the
|
||||
/// single-`e1` instance is valid. The `valid` schema and `invalid` instance
|
||||
/// expectations are mutually exclusive; libxml2 accepts both. Confirmed by an
|
||||
/// adversarial analysis that found no third path.
|
||||
///
|
||||
/// - `particlesZ001.i`: a `complexContent` restriction `Derived` of `Base`, with an
|
||||
/// instance valid against `Derived`'s own content model. cvc-complex-type (§3.4.4)
|
||||
/// validates an instance against the element's own `{content type}` and never
|
||||
/// re-runs Particle Valid (Restriction) / RecurseAsIfGroup (§3.9.6, a schema-
|
||||
/// assembly constraint), so schema-valid + instance-invalid is unreachable. The
|
||||
/// manifest itself labels the governing rule ambiguous; libxml2 accepts both.
|
||||
/// Reaching `invalid` would require rejecting the schema (a valid-schema-rejected
|
||||
/// false positive) and would still fail the suite's own `valid` schemaTest.
|
||||
private static let specDivergentInstances: Set<String> = [
|
||||
"anyURI_a004_1339.i", "schA2.i", "schA5.i", "mgO013.i", "particlesZ001.i",
|
||||
]
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
@ -228,6 +179,12 @@ struct XSTSSuiteTests {
|
||||
guard !hrefs.isEmpty else { continue }
|
||||
schemaExpectedValid = expectedValidity(schemaTest) != "invalid"
|
||||
let compiled = compileSchemaDocuments(hrefs, directory: directory, name: name, into: &counters)
|
||||
if Self.specDivergentSchemas.contains(name) {
|
||||
// Excluded from the verdict counts (see the set's justification); the
|
||||
// compile still happens so the group's instance tests run.
|
||||
schema = compiled
|
||||
continue
|
||||
}
|
||||
switch (schemaExpectedValid, compiled == nil) {
|
||||
case (true, true):
|
||||
counters.schemaValidRejected += 1
|
||||
|
||||
@ -12,7 +12,7 @@ changelog entries and commit messages refer to that lost tracker.
|
||||
## Current focus (priority order)
|
||||
|
||||
1. **[#4](https://codeberg.org/Mihaela/PureXML/issues/4)**: drive invalid
|
||||
schemas accepted from 8 to 0. The remainder routes through
|
||||
schemas accepted from 6 to 0. The remainder routes through
|
||||
`ParticleRestriction.violation`'s deliberate under-approximation
|
||||
(`docs/release-hurdles.md`, Part 1).
|
||||
2. **[#5](https://codeberg.org/Mihaela/PureXML/issues/5)**: drive invalid
|
||||
@ -35,7 +35,7 @@ together.
|
||||
| Gate | Now | Issue |
|
||||
|---|---|---|
|
||||
| Valid schemas rejected | 0 | at floor, enforced by the ratchet |
|
||||
| Invalid schemas accepted | 8 | [#4](https://codeberg.org/Mihaela/PureXML/issues/4) |
|
||||
| Invalid schemas accepted | 6 | [#4](https://codeberg.org/Mihaela/PureXML/issues/4) |
|
||||
| Valid instances rejected | 0 | at floor, enforced by the ratchet |
|
||||
| Invalid instances accepted | 4 | [#5](https://codeberg.org/Mihaela/PureXML/issues/5) |
|
||||
|
||||
|
||||
@ -65,7 +65,8 @@ Schema side (10 cases):
|
||||
| Case | libxml2 | Reading |
|
||||
|---|---|---|
|
||||
| `particlesZ030_d` | rejects | Reference-aligned bug, now fixed: a `simpleContent` restriction relaxing a base `required` attribute to `optional` (derivation-ok-restriction.2). invalid-schemas-accepted 10 -> 9. |
|
||||
| `elemZ026`, `particlesHa161`, `particlesIg004`, `particlesIk011/025/027`, `particlesM034`, `particlesV020` | accept too | Stricter-than-libxml2: NameAndTypeOK / particle-restriction edges (union membership, element-block superset) that libxml2 does not implement. Pursue only if Xerces-level strictness is wanted. |
|
||||
| `elemZ026`, `particlesV020` | accept too | Closed 2026-07-05: NameAndTypeOK clause 1 now pairs the DECLARED element references (`Particle.declaredReference` preserves the pre-substitution-expansion identity), so a restriction that swaps a head reference for a substitution-group member is rejected. Stricter than libxml2, spec-grounded (section 3.9.6 Elt:Elt clause 1 has no substitution allowance). The identical construct `particlesZ008` carries a settled `valid` expectation contradicting elemZ026's settled `invalid`; Z008 is reclassified as spec-divergent (excluded), with the full adjudication in `Tests/XSTSSuiteTests.swift`. |
|
||||
| `particlesHa161`, `particlesIg004`, `particlesIk011/025/027`, `particlesM034` | accept too | Stricter-than-libxml2: NameAndTypeOK / particle-restriction edges (union membership, element-block superset) that libxml2 does not implement. Pursue only if Xerces-level strictness is wanted. |
|
||||
| `schN12` | (multi-document) | Cross-namespace composition; not triaged in isolation. |
|
||||
|
||||
Instance side (sampled): `idZ010`, `mgO013`, `particlesZ001`, `reT17`, `reT38`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user