feat(schema): validate final/block derivation-control value space
The final and block attributes are #all or a whitespace list of method tokens, but the admitted tokens depend on the component: element final is extension/restriction, element block adds substitution, complexType final/block are extension/restriction, simpleType final is list/union/restriction. The tokens were parsed leniently (unknown ignored, substitution admitted everywhere), so final="substitution" on an element, a misspelled or miscased token, or #all mixed with other tokens was accepted. The list is validated against the component's exact set; finalDefault/ blockDefault (the wider schema-level value space) and foreign attributes are left alone. XSTS: invalid schemas accepted 580 to 537, valid schemas rejected held at 72, instance buckets unchanged.
This commit is contained in:
parent
6a7fb1d594
commit
11c4eacdff
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- The `final` and `block` derivation-control attributes are now checked against their value space (#158). Each is `#all` or a whitespace list of method tokens, but the admitted tokens depend on the component: an element's `final` is `extension`/`restriction` and its `block` adds `substitution`; a complexType's `final`/`block` are `extension`/`restriction`; a simpleType's `final` is `list`/`union`/`restriction`. The tokens were parsed leniently (unknown tokens ignored, `substitution` admitted everywhere), so `final="substitution"` on an element, a misspelled or miscased token, or `#all` mixed with other tokens was accepted (the elemF/st_final families). The list is now validated against the component's exact set; `finalDefault`/`blockDefault` (the wider schema-level value space) and foreign attributes are left alone. XSTS: invalid schemas accepted 580 to 537, no other bucket raised (valid schemas rejected held at 72).
|
||||
- A `simpleContent` derivation's content is now checked for order and disallowed children (#158). A `restriction` is `(annotation?, (simpleType?, facet*), (attribute | attributeGroup)*, anyAttribute?)` and an `extension` is `(annotation?, (attribute | attributeGroup)*, anyAttribute?)`: simpleContent has no model group, an extension has no facets or `simpleType`, and the attributes precede a single trailing `anyAttribute`. The membership table admitted the names but not the model, so a model group inside simpleContent, a facet in an extension, a facet before its `simpleType`, an attribute after `anyAttribute`, or a second `anyAttribute` (the ctD family) was accepted. The derivation's children are matched against the context's ordered slot model, reusing the slot walk built for complexContent. XSTS: invalid schemas accepted 594 to 580, no other bucket raised (valid schemas rejected held at 72).
|
||||
- Particle Valid (Restriction) now checks an element's type by derivation, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must be, or derive by restriction from, the base element's type; renaming `xs:string` to `xs:int`, widening a concrete type to `xs:anyType`, or naming an unrelated type (a different user type, a wider ancestor, an absent/`anyType` type over a concrete base) was accepted because only the names were compared. Each element particle now carries its type's derivation identity (the resolved type name, `anyType` for an absent type, or nil for an inline anonymous type), named `simpleType` restriction derivations are recorded alongside complex ones, and the check walks the user restriction chain and then the built-in lattice to decide derivation by name; an ur-type base, a union base (membership is not a restriction chain), and an inline anonymous type are permitted, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 594, invalid instances accepted 165 to 160 (more invalid schemas rejected up front), no bucket raised (valid schemas rejected held at 72).
|
||||
- An element/attribute `default`/`fixed` value must be a valid value of its `type` (Attribute/Element Locally Valid, XSD 1.0, #158). A `fixed="abc"` on an `xs:integer` or a `default="1 2"` on an `xs:int` was accepted because the value constraint was never checked at compile time. The value is now validated against its type when the type resolves to a built-in datatype (namespace-aware: only a `type` in the XSD namespace), normalising whitespace per the type as XSD requires (so `default=" 5 "` on an integer is valid); a named or inline user type is left to its own rules, so no valid value constraint is rejected. XSTS: invalid schemas accepted 638 to 621, no other bucket raised (valid schemas rejected held at 72).
|
||||
|
||||
51
Sources/Schema/SchemaDerivationControl.swift
Normal file
51
Sources/Schema/SchemaDerivationControl.swift
Normal file
@ -0,0 +1,51 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// Value-space validity of the `final` and `block` derivation-control
|
||||
/// attributes (XSD 1.0 Structures). Each is `#all` or a whitespace-separated
|
||||
/// list of method tokens, but the admitted tokens depend on the component:
|
||||
///
|
||||
/// - element `final`: `extension`, `restriction`
|
||||
/// - element `block`: `extension`, `restriction`, `substitution`
|
||||
/// - complexType `final`/`block`: `extension`, `restriction`
|
||||
/// - simpleType `final`: `list`, `union`, `restriction`
|
||||
///
|
||||
/// `methodSet` parses these leniently (ignoring unknown tokens and admitting
|
||||
/// `substitution` everywhere), so `final="substitution"` on an element, a
|
||||
/// misspelled or miscased token (`foo`, `#All`, `Extension`), or `#all` mixed
|
||||
/// with other tokens was accepted (the elemF / st_final families). The token
|
||||
/// list is now checked against the component's exact set.
|
||||
static func derivationControlErrors(_ node: XSDTree, local: String) -> [String] {
|
||||
var errors: [String] = []
|
||||
for attribute in ["final", "block"] {
|
||||
guard let allowed = derivationControlTokens["\(local)|\(attribute)"],
|
||||
let raw = node.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == attribute })?.value
|
||||
else { continue }
|
||||
if let reason = derivationListError(raw, allowed: allowed) {
|
||||
errors.append("the '\(attribute)' value '\(raw)' on '\(local)' is not valid: \(reason)")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// The exact method tokens each component admits in `final`/`block`.
|
||||
private static let derivationControlTokens: [String: Set<String>] = [
|
||||
"element|final": ["extension", "restriction"],
|
||||
"element|block": ["extension", "restriction", "substitution"],
|
||||
"complexType|final": ["extension", "restriction"],
|
||||
"complexType|block": ["extension", "restriction"],
|
||||
"simpleType|final": ["list", "union", "restriction"],
|
||||
]
|
||||
|
||||
/// The reason a `final`/`block` value is invalid, or nil. `#all` stands alone;
|
||||
/// otherwise every whitespace-separated token must be in `allowed` (exact case),
|
||||
/// and `#all` may not be mixed with other tokens. An empty value is valid (no
|
||||
/// control).
|
||||
private static func derivationListError(_ raw: String, allowed: Set<String>) -> String? {
|
||||
let tokens = raw.split(whereSeparator: \.isWhitespace).map(String.init)
|
||||
if tokens == ["#all"] { return nil }
|
||||
for token in tokens {
|
||||
if token == "#all" { return "'#all' may not be combined with other tokens" }
|
||||
if !allowed.contains(token) { return "'\(token)' is not one of \(allowed.sorted().joined(separator: ", "))" }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -156,6 +156,7 @@ extension PureXML.Schema.XSDParser {
|
||||
if let allowed = allowedChildren[local] {
|
||||
errors += childErrors(local: local, children: names, allowed: allowed)
|
||||
}
|
||||
errors += derivationControlErrors(node, local: local)
|
||||
switch local {
|
||||
case "group" where PureXML.Schema.XSDNode.attribute(node, "name") != nil:
|
||||
errors += namedGroupErrors(names)
|
||||
|
||||
50
Tests/SchemaDerivationControlTests.swift
Normal file
50
Tests/SchemaDerivationControlTests.swift
Normal file
@ -0,0 +1,50 @@
|
||||
@testable import PureXML
|
||||
import Testing
|
||||
|
||||
/// Value-space validity of the `final`/`block` derivation-control attributes
|
||||
/// (XSD 1.0): `#all` or a whitespace list of method tokens, with the admitted
|
||||
/// tokens depending on the component (element/complexType/simpleType) and on
|
||||
/// whether it is `final` or `block`. The elemF / st_final families.
|
||||
@Suite("Derivation control (final/block) value space")
|
||||
struct SchemaDerivationControlTests {
|
||||
private func rejects(_ body: String) -> Bool {
|
||||
do {
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
\(body)
|
||||
</xs:schema>
|
||||
""")
|
||||
return false
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@Test("element final admits only extension/restriction; block also substitution")
|
||||
func test_element() {
|
||||
// substitution is a block token, not a final token.
|
||||
#expect(rejects(#"<xs:element name="e" type="xs:string" final="substitution"/>"#))
|
||||
#expect(rejects(#"<xs:element name="e" type="xs:string" final="extension restriction substitution"/>"#))
|
||||
// unknown token and wrong case.
|
||||
#expect(rejects(#"<xs:element name="e" type="xs:string" final="foo"/>"#))
|
||||
#expect(rejects(##"<xs:element name="e" type="xs:string" final="#All"/>"##))
|
||||
#expect(rejects(#"<xs:element name="e" type="xs:string" final="Extension"/>"#))
|
||||
// valid finals and a valid block with substitution.
|
||||
#expect(!rejects(##"<xs:element name="e" type="xs:string" final="#all"/>"##))
|
||||
#expect(!rejects(#"<xs:element name="e" type="xs:string" final="extension restriction"/>"#))
|
||||
#expect(!rejects(#"<xs:element name="e" type="xs:string" block="substitution"/>"#))
|
||||
}
|
||||
|
||||
@Test("simpleType final admits list/union/restriction, not extension")
|
||||
func test_simpleType() {
|
||||
#expect(rejects(#"<xs:simpleType name="t" final="extension"><xs:restriction base="xs:string"/></xs:simpleType>"#))
|
||||
#expect(!rejects(#"<xs:simpleType name="t" final="list union restriction"><xs:restriction base="xs:string"/></xs:simpleType>"#))
|
||||
#expect(!rejects(##"<xs:simpleType name="t" final="#all"><xs:restriction base="xs:string"/></xs:simpleType>"##))
|
||||
}
|
||||
|
||||
@Test("complexType block/final admit only extension/restriction")
|
||||
func test_complexType() {
|
||||
#expect(rejects(#"<xs:complexType name="t" block="substitution"><xs:sequence/></xs:complexType>"#))
|
||||
#expect(!rejects(#"<xs:complexType name="t" final="restriction"><xs:sequence/></xs:complexType>"#))
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ struct XSTSSuiteTests {
|
||||
/// 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 = 72
|
||||
private let knownSchemaInvalidAccepted = 580
|
||||
private let knownSchemaInvalidAccepted = 537
|
||||
private let knownInstanceValidRejected = 214
|
||||
private let knownInstanceInvalidAccepted = 160
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | 72 | we rarely reject a good schema |
|
||||
| **invalid-schemas-accepted** | **2461 -> 580** | we rarely catch a bad one (facet definition + applicability, default/fixed value validity, particle-restriction name+type, id, structural content-model, UPA determinism, complexContent + complexType content order, identity-constraint XPath-subset, and wildcard-namespace validity landed) |
|
||||
| **invalid-schemas-accepted** | **2461 -> 537** | we rarely catch a bad one (facet definition + applicability, default/fixed value validity, particle-restriction name+type, id, structural content-model, UPA determinism, complexContent + complexType content order, identity-constraint XPath-subset, and wildcard-namespace validity landed) |
|
||||
| valid-instances-rejected | 233 | (instance side, separately tracked) |
|
||||
| invalid-instances-accepted | 165 | |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user