feat(schema): enforce particle element type restriction and bypass prohibited attributes
This commit is contained in:
parent
b6575150e8
commit
31fe3e7c22
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- A complexContent restriction must keep a base attribute's required use and its fixed value constraint (XSD 1.0 `cos-ct-derived-ok` / `derivation-ok-restriction.2` / `derivation-ok-restriction.4`). A restriction redeclaring a base `required` attribute may not make it optional or prohibited, and if a base attribute use has a `fixed` value constraint, the restriction must also make it `fixed` with the same value. The fixed values are compared in the attribute type's value space (supporting lists and unions) to avoid over-rejecting valid schemas. A prohibited attribute is removed, so the fixed clause does not apply to it. XSTS: invalid schemas accepted 390 to 387, no other bucket raised.
|
||||
- Enforce top-level declaration naming, block xs:anySimpleType derivation, and restrict simpleType facets. Top-level definitions must specify a `name` attribute. Derivation by restriction or extension of `xs:anySimpleType` is disallowed. Restriction facets (`length`, `minLength`, `maxLength`, `totalDigits`, `fractionDigits`) must be tighter than or equal to the base type's facets. XSTS: invalid schemas accepted 387 to 383, no other bucket raised.
|
||||
- Enforce simpleType derivation constraints on restricted complexType attributes (XSD 1.0 `cos-ct-derived-ok` / `derivation-ok-restriction.1`). The type of a restricted attribute must be validly derived by restriction from its base counterpart, checking atomic built-in derivation, list item type restriction, and union membership or member-to-member restriction recursively. XSTS: invalid schemas accepted 367 to 365, no other bucket raised.
|
||||
- Enforce element type restriction constraints on restricted complexType particles (XSD 1.0 `NameAndTypeOK` / `elementTypeRestrictionOK`). Verify that the restricted particle's type validly derives by restriction from the base particle's type, checking union variety members structurally, prohibiting complex-restricting-simple and simple-restricting-complex (except where base is `anyType`), and falling back to name-based derivation. Prohibited attributes are exempted from type and value constraint checks. XSTS: invalid schemas accepted 365 to 357, no other bucket raised.
|
||||
|
||||
## [0.1.0] - 2026-06-15
|
||||
|
||||
|
||||
@ -228,78 +228,6 @@ extension PureXML.Schema {
|
||||
return base.indices.allSatisfy { used[$0] || emptiable(base[$0]) }
|
||||
}
|
||||
|
||||
/// NameAndTypeOK type clause: the restricting element's type must be, or
|
||||
/// derive by restriction from, the base element's type. Element types carry
|
||||
/// their derivation identity as a `typeName` (a built-in, a user type, or
|
||||
/// `anyType` for an absent type; nil for an inline anonymous type). When both
|
||||
/// names are known the answer is decided by name; an inline anonymous type on
|
||||
/// either side is permitted (its derivation identity is not recorded), which
|
||||
/// can only under-reject, so no valid restriction is rejected.
|
||||
private static func elementTypeRestrictionOK(
|
||||
_ restrictedTypeName: String?,
|
||||
_ baseTypeName: String?,
|
||||
_ types: [String: ElementType],
|
||||
_ derivation: [String: TypeDerivation],
|
||||
) -> Bool {
|
||||
guard let restrictedTypeName, let baseTypeName else { return true }
|
||||
// A type validly derives from a union by being one of its members, which
|
||||
// is not a restriction chain over type names, so when the base resolves to
|
||||
// a union variety the name check stands down (permit) rather than misread
|
||||
// the membership as unrelated. (A list base stays checked: a valid
|
||||
// list-typed restriction is the same named list or a restriction of it,
|
||||
// which the derivation chain does capture.)
|
||||
if baseIsUnion(baseTypeName, types) { return true }
|
||||
return typeDerivesOrEqual(restrictedTypeName, baseTypeName, derivation, types)
|
||||
}
|
||||
|
||||
/// Whether the named type resolves (through the type table) to a `union`
|
||||
/// simple type.
|
||||
private static func baseIsUnion(_ name: String, _ types: [String: ElementType]) -> Bool {
|
||||
var current: ElementType? = types[name]
|
||||
var steps = 0
|
||||
while let resolved = current, steps <= types.count {
|
||||
switch resolved {
|
||||
case let .simple(simple):
|
||||
if case .union = simple.variety { return true }
|
||||
return false
|
||||
case .complex:
|
||||
return false
|
||||
case let .typeReference(key):
|
||||
current = types[key]
|
||||
steps += 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static let urTypeNames: Set<String> = ["anyType", "anySimpleType", "anyAtomicType"]
|
||||
|
||||
private static func isUrTypeName(_ name: String, _ types: [String: ElementType]) -> Bool {
|
||||
urTypeNames.contains(name) && types[name] == nil
|
||||
}
|
||||
|
||||
/// Whether the type named `derived` is, or is validly derived by restriction
|
||||
/// from, the type named `base`: any type derives from an ur-type base; a name
|
||||
/// equal to the base matches; the user-declared restriction chain is walked
|
||||
/// (`derivation`), and where it bottoms out in a built-in the lattice
|
||||
/// continues it. Names are compared as the compiler resolves them (by local
|
||||
/// name), so an unrelated pair (`xs:string` restricting a user type, two
|
||||
/// independent list types) is correctly not derivable.
|
||||
static func typeDerivesOrEqual(_ derived: String, _ base: String, _ derivation: [String: TypeDerivation], _ types: [String: ElementType]) -> Bool {
|
||||
if isUrTypeName(base, types) { return true }
|
||||
var current = derived
|
||||
var visited: Set<String> = []
|
||||
while visited.insert(current).inserted {
|
||||
if current == base { return true }
|
||||
guard let step = derivation[current] else { break }
|
||||
current = step.base
|
||||
}
|
||||
if let derivedBuiltin = BuiltinType(rawValue: current), let baseBuiltin = BuiltinType(rawValue: base) {
|
||||
return derivedBuiltin.derives(from: baseBuiltin)
|
||||
}
|
||||
return current == base
|
||||
}
|
||||
|
||||
/// Occurrence-range subsumption: the restriction may not occur less often
|
||||
/// than the base requires, nor more often than the base allows.
|
||||
private static func rangeSubsumed(_ restricted: Particle, _ base: Particle) -> Bool {
|
||||
|
||||
114
Sources/Schema/ParticleRestrictionElement.swift
Normal file
114
Sources/Schema/ParticleRestrictionElement.swift
Normal file
@ -0,0 +1,114 @@
|
||||
extension PureXML.Schema.ParticleRestriction {
|
||||
/// NameAndTypeOK type clause: the restricting element's type must be, or
|
||||
/// derive by restriction from, the base element's type. Element types carry
|
||||
/// their derivation identity as a `typeName` (a built-in, a user type, or
|
||||
/// `anyType` for an absent type; nil for an inline anonymous type). When both
|
||||
/// names are known the answer is decided by name; an inline anonymous type on
|
||||
/// either side is permitted (its derivation identity is not recorded), which
|
||||
/// can only under-reject, so no valid restriction is rejected.
|
||||
private static func resolveType(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> PureXML.Schema.ElementType? {
|
||||
let local = PureXML.Schema.XSDNode.stripPrefix(name)
|
||||
if let builtin = PureXML.Schema.BuiltinType(rawValue: local) {
|
||||
return .simple(PureXML.Schema.SimpleType(base: builtin))
|
||||
}
|
||||
if local == "anySimpleType" {
|
||||
return .simple(PureXML.Schema.SimpleType(base: .string, isAnySimpleType: true))
|
||||
}
|
||||
if local == "anyType" {
|
||||
return .complex(PureXML.Schema.ComplexType())
|
||||
}
|
||||
var current: PureXML.Schema.ElementType? = types[local]
|
||||
var steps = 0
|
||||
while let resolved = current, steps <= types.count {
|
||||
if case let .typeReference(key) = resolved {
|
||||
current = types[key]
|
||||
steps += 1
|
||||
} else {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func elementTypeRestrictionOK(
|
||||
_ restrictedTypeName: String?,
|
||||
_ baseTypeName: String?,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||
) -> Bool {
|
||||
guard let restrictedTypeName, let baseTypeName else { return true }
|
||||
|
||||
if let baseResolved = resolveType(baseTypeName, types), let derivedResolved = resolveType(restrictedTypeName, types) {
|
||||
switch (derivedResolved, baseResolved) {
|
||||
case let (.simple(derivedSimple), .simple(baseSimple)):
|
||||
if case .union = baseSimple.variety {
|
||||
return PureXML.Schema.XSDParser.isSimpleTypeRestrictionOK(derived: derivedSimple, base: baseSimple)
|
||||
}
|
||||
case (.complex, .simple):
|
||||
return false
|
||||
case (.simple, .complex):
|
||||
return PureXML.Schema.XSDNode.stripPrefix(baseTypeName) == "anyType"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if baseIsUnion(baseTypeName, types) { return true }
|
||||
return typeDerivesOrEqual(restrictedTypeName, baseTypeName, derivation, types)
|
||||
}
|
||||
|
||||
/// Whether the named type resolves (through the type table) to a `union`
|
||||
/// simple type.
|
||||
private static func baseIsUnion(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool {
|
||||
var current: PureXML.Schema.ElementType? = types[name]
|
||||
var steps = 0
|
||||
while let resolved = current, steps <= types.count {
|
||||
switch resolved {
|
||||
case let .simple(simple):
|
||||
if case .union = simple.variety { return true }
|
||||
return false
|
||||
case .complex:
|
||||
return false
|
||||
case let .typeReference(key):
|
||||
current = types[key]
|
||||
steps += 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static var urTypeNames: Set<String> {
|
||||
["anyType", "anySimpleType", "anyAtomicType"]
|
||||
}
|
||||
|
||||
private static func isUrTypeName(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool {
|
||||
urTypeNames.contains(name) && types[name] == nil
|
||||
}
|
||||
|
||||
/// Whether the type named `derived` is, or is validly derived by restriction
|
||||
/// from, the type named `base`: any type derives from an ur-type base; a name
|
||||
/// equal to the base matches; the user-declared restriction chain is walked
|
||||
/// (`derivation`), and where it bottoms out in a built-in the lattice
|
||||
/// continues it. Names are compared as the compiler resolves them (by local
|
||||
/// name), so an unrelated pair (`xs:string` restricting a user type, two
|
||||
/// independent list types) is correctly not derivable.
|
||||
static func typeDerivesOrEqual(
|
||||
_ derived: String,
|
||||
_ base: String,
|
||||
_ derivation: [String: PureXML.Schema.TypeDerivation],
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> Bool {
|
||||
if isUrTypeName(base, types) { return true }
|
||||
var current = derived
|
||||
var visited: Set<String> = []
|
||||
while visited.insert(current).inserted {
|
||||
if current == base { return true }
|
||||
guard let step = derivation[current] else { break }
|
||||
current = step.base
|
||||
}
|
||||
if let derivedBuiltin = PureXML.Schema.BuiltinType(rawValue: current), let baseBuiltin = PureXML.Schema.BuiltinType(rawValue: base) {
|
||||
return derivedBuiltin.derives(from: baseBuiltin)
|
||||
}
|
||||
return current == base
|
||||
}
|
||||
}
|
||||
@ -53,13 +53,13 @@ extension PureXML.Schema.XSDParser {
|
||||
if base.required, !derived.required {
|
||||
return "attribute '\(derived.name.localName)' is required in the base type and a restriction may not make it optional or prohibited"
|
||||
}
|
||||
if isProhibited(derived.name, under: restrictionNode, context) {
|
||||
return nil
|
||||
}
|
||||
if !isSimpleTypeRestrictionOK(derived: derived.type, base: base.type) {
|
||||
return "attribute '\(derived.name.localName)' has type '\(derived.type.base.rawValue)' which is not a valid restriction of base type '\(base.type.base.rawValue)'"
|
||||
}
|
||||
if let baseFixed = base.valueConstraint?.fixedValue {
|
||||
if isProhibited(derived.name, under: restrictionNode, context) {
|
||||
return nil
|
||||
}
|
||||
guard let derivedConstraint = derived.valueConstraint,
|
||||
let derivedFixed = derivedConstraint.fixedValue
|
||||
else {
|
||||
@ -72,10 +72,13 @@ extension PureXML.Schema.XSDParser {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func isSimpleTypeRestrictionOK(derived: PureXML.Schema.SimpleType, base: PureXML.Schema.SimpleType) -> Bool {
|
||||
static func isSimpleTypeRestrictionOK(derived: PureXML.Schema.SimpleType, base: PureXML.Schema.SimpleType) -> Bool {
|
||||
if base.isAnySimpleType {
|
||||
return true
|
||||
}
|
||||
if derived.isAnySimpleType {
|
||||
return false
|
||||
}
|
||||
switch (derived.variety, base.variety) {
|
||||
case (.atomic, .atomic):
|
||||
return derived.base.derives(from: base.base)
|
||||
|
||||
@ -300,6 +300,18 @@ struct XSDParticleRestrictionTests {
|
||||
#expect(doc("other", base: "foo", types: userTypes) == nil)
|
||||
// a union member (xs:string) validly restricts the union-typed base element.
|
||||
#expect(doc("xs:string", base: "u", types: userTypes) != nil)
|
||||
|
||||
// a union restricting another union (subset members): valid.
|
||||
let unions = """
|
||||
<xs:simpleType name="U1"><xs:union memberTypes="xs:integer xs:string"/></xs:simpleType>
|
||||
<xs:simpleType name="U2"><xs:union memberTypes="xs:integer xs:string xs:boolean"/></xs:simpleType>
|
||||
<xs:simpleType name="L1"><xs:list itemType="xs:string"/></xs:simpleType>
|
||||
"""
|
||||
#expect(doc("U1", base: "U2", types: unions) != nil)
|
||||
// a union restricting another union but adding/having unmatching members: invalid.
|
||||
#expect(doc("U2", base: "U1", types: unions) == nil)
|
||||
// a list type restricting a union type: invalid.
|
||||
#expect(doc("L1", base: "U1", types: unions) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 = 1
|
||||
private let knownSchemaInvalidAccepted = 365
|
||||
private let knownSchemaInvalidAccepted = 357
|
||||
private let knownInstanceValidRejected = 180
|
||||
private let knownInstanceInvalidAccepted = 158
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | **72 -> 4** | production target is 0: rejecting a correct schema is the most user-hostile defect (#148). Particle-restriction over-rejection fixed: pointless `maxOccurs=0` particles removed, content-free restriction of an EMPTY base, empty-derived emptiable-base, and (#163) the pairwise check over the normalized content model with occurrence subsumption applied only to same-kind pairings (cross-kind element/group, group/element, group/wildcard match occurrence inside, by effective total range / member). #164 resolved element-ref namespaces by prefix binding (the `particlesJj/Jk/Jm/Jn/Js`, `particlesQ/R` cluster, -28). Content-free (`maxOccurs=0`) derived groups and substitution-group declaration order closed `particlesW006`/`elemZ027_a`; the W3C MapAndSum rule (Sequence:Choice, #165) closed `particlesV003`. Remaining 1 (`particlesZ001`): a documented spec ambiguity (RecurseAsIfGroup; the reference rejects it), not a defect. |
|
||||
| **invalid-schemas-accepted** | **2461 -> 365** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion landed). |
|
||||
| **invalid-schemas-accepted** | **2461 -> 357** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass landed). |
|
||||
| valid-instances-rejected | **233 -> 180** | (instance side; #164's element-ref namespace fix is shared with instance validation, -34) |
|
||||
| invalid-instances-accepted | **160 -> 158** | (instance side; resolving an inline `<simpleType>` restriction base preserves list/union variety, so a `length` facet counts items and two more invalid instances are caught) |
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user