fix(schema): enforce block="substitution" on substitution-group heads
The substitution filter dropped only members whose type derived from the head's type by a blocked method (extension/restriction). The `substitution` keyword in a head's block forbids substitution-group substitution outright, whatever the member's type, and was ignored, so a same-type member still substituted for a head declared block="substitution". Members are now dropped entirely when the head blocks substitution. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74. Baseline updated; regression tests cover the substitution block rejecting a member, plain heads still validating, and the no-block control. Refs #147, #129.
This commit is contained in:
parent
c9f41c387e
commit
47e20ff90b
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- `block="substitution"` on a substitution-group head is now enforced (#147). The substitution filter only dropped members whose type derived from the head's type by a blocked method (`extension`/`restriction`); the `substitution` keyword, which forbids substitution-group substitution outright regardless of the member's type, was ignored, so a same-type member still substituted under a substitution block. XSTS: invalid instances accepted 238 to 231, valid schemas rejected 75 to 74.
|
||||
- Element `default` and `fixed` values are honored on empty elements (#147). An empty element with a value constraint takes that value, so the empty content is valid even when the type would reject the empty string; the constraint value (not the empty string) is what is validated against the type, including any `xsi:type` override. A restriction's `enumeration` now also replaces the inherited enumeration rather than unioning with it (the new set is the value space; `pattern` still accumulates and is ANDed), which a value-constraint case under `xsi:type` exposed. XSTS: valid instances rejected 395 to 369, invalid instances accepted 240 to 238.
|
||||
- Element-level `block` is now enforced against `xsi:type` substitution (#147). The `block` on an element declaration (distinct from `block` on its type) was parsed but never threaded to the instance validator, so a document substituting a blocked-derivation type via `xsi:type` was wrongly accepted. The element block is now carried through to `blockedSubstitutionError`, which also resolves the element-ref type indirection (`element:foo` to its concrete type) so the check fires for referenced children, not only the root. XSTS: invalid instances accepted 261 to 240.
|
||||
- The regex engine now recognizes the full XSD `\p{Is<Block>}` Unicode block set (#147). Only about fifteen blocks were tabulated, so a pattern over any other block (`\p{IsArmenian}`, `\p{IsTamil}`, `\p{IsHangulJamo}`, and so on) failed to compile and rejected valid instances. The table now carries the Unicode 3.1 blocks the XSD regular-expression appendix fixes (surrogate blocks omitted, since surrogates are not scalar values); each range is verified bidirectionally by the XSTS corpus (valid in-block instances pass, invalid out-of-block instances still fail). XSTS: valid instances rejected 502 to 395.
|
||||
|
||||
@ -137,6 +137,9 @@ extension PureXML.Schema.XSDParser {
|
||||
continue
|
||||
}
|
||||
result[head] = members.filter { member in
|
||||
// `block="substitution"` forbids substitution-group substitution
|
||||
// outright: no member may stand in for the head, whatever its type.
|
||||
guard !blocked.contains(.substitution) else { return false }
|
||||
guard let memberType = tables.elementTypeNames[member],
|
||||
let methods = derivationMethods(from: memberType, to: headType, tables.typeDerivation)
|
||||
else {
|
||||
|
||||
42
Tests/SchemaSubstitutionBlockTests.swift
Normal file
42
Tests/SchemaSubstitutionBlockTests.swift
Normal file
@ -0,0 +1,42 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// `block="substitution"` on a substitution-group head forbids any member from
|
||||
/// standing in for it, regardless of the member's type derivation (#147, XSTS
|
||||
/// disallowedSubst set). Only type-derivation blocks (`extension`/`restriction`)
|
||||
/// had been applied, so a same-type member still substituted under a
|
||||
/// substitution block.
|
||||
@Suite("Substitution-group block")
|
||||
struct SchemaSubstitutionBlockTests {
|
||||
private func schema(headBlock: String) throws -> PureXML.Schema.Document {
|
||||
let attribute = headBlock.isEmpty ? "" : " block=\"\(headBlock)\""
|
||||
return try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="urn:s" targetNamespace="urn:s" elementFormDefault="qualified">
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence>
|
||||
<xs:element ref="Head" maxOccurs="unbounded"/>
|
||||
</xs:sequence></xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="Head" type="T"\(attribute)/>
|
||||
<xs:complexType name="T"><xs:sequence><xs:element name="x" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:element name="Member" type="T" substitutionGroup="Head"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
}
|
||||
|
||||
private let withMember = #"<root xmlns="urn:s"><Head><x>a</x></Head><Member><x>b</x></Member></root>"#
|
||||
private let headsOnly = #"<root xmlns="urn:s"><Head><x>a</x></Head><Head><x>b</x></Head></root>"#
|
||||
|
||||
@Test("block='substitution' on the head rejects a substituting member")
|
||||
func test_substitutionBlocked() throws {
|
||||
#expect(try !schema(headBlock: "substitution").validate(withMember).isEmpty)
|
||||
// Plain heads are still fine.
|
||||
#expect(try schema(headBlock: "substitution").validate(headsOnly).isEmpty)
|
||||
}
|
||||
|
||||
@Test("without a substitution block the member may substitute")
|
||||
func test_memberAllowedWithoutBlock() throws {
|
||||
#expect(try schema(headBlock: "").validate(withMember).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -28,10 +28,10 @@ struct XSTSSuiteTests {
|
||||
/// `swift test` are unaffected; run it with `swift test -c release --filter
|
||||
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
private let knownSchemaValidRejected = 75
|
||||
private let knownSchemaValidRejected = 74
|
||||
private let knownSchemaInvalidAccepted = 2467
|
||||
private let knownInstanceValidRejected = 369
|
||||
private let knownInstanceInvalidAccepted = 238
|
||||
private let knownInstanceInvalidAccepted = 231
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user