feat(schema): reject ur-type widening in element restriction
Extend NameAndTypeOK: restricting a concrete-typed element to an ur-type (xs:anyType / xs:anySimpleType / xs:anyAtomicType) is widening, hence an invalid restriction. The element-vs-element check now rejects it before the atomic derivation check. The ur-type is recognised by a type reference whose key is anyType / anySimpleType / anyAtomicType AND which is absent from the named-type table, so a user type that merely shares the local name (in another namespace) is not mistaken for the ur-type. The reverse (an ur-type base narrowed by the restriction) stays valid. XSTS: invalid schemas accepted 620 to 616, valid schemas rejected held at 72, instance buckets unchanged.
This commit is contained in:
parent
7a80410945
commit
711a50b8ea
@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Particle Valid (Restriction) now checks an element's type, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must derive from the base's; renaming an `xs:string` element to `xs:int` under restriction was accepted because only the names were compared. The check resolves both element types (following a type reference through the compiled type table) and, when both are atomic simple types, requires the restricting type's built-in base to derive from the base's; a complex, list, union, untyped, or unresolvable type is left alone, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 620, no other bucket raised (valid schemas rejected held at 72).
|
||||
- Particle Valid (Restriction) now checks an element's type, not just its name (NameAndTypeOK, #158). When a restriction maps a same-named element onto a base element, the restricting type must derive from the base's; renaming an `xs:string` element to `xs:int`, or widening a concrete-typed element to `xs:anyType`/`xs:anySimpleType`, under restriction was accepted because only the names were compared. The check resolves both element types (following a type reference through the compiled type table) and, when both are atomic simple types, requires the restricting type's built-in base to derive from the base's; widening to an ur-type over a concrete base is rejected outright. A complex, list, union, untyped, or unresolvable type is left alone, and a user type that merely shares the local name `anyType` (in another namespace) is not mistaken for the ur-type, so no valid restriction is rejected. XSTS: invalid schemas accepted 621 to 616, no other 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).
|
||||
- A constraining facet must apply to its simple type's variety (XSD 1.0 Part 2, #158). A `list` may be constrained only by `length`/`minLength`/`maxLength`, `pattern`, `enumeration`, and `whiteSpace`; a `union` only by `pattern` and `enumeration`. A value-bound facet (`maxInclusive`, `totalDigits`, ...) constrains an atomic value space, so restricting a list (`list of integer`) by `maxInclusive` was wrongly accepted. The base's variety is resolved from the raw tree (the check runs before the types are compiled), namespace-aware so a user type that shares a built-in's local name does not flip the variety: a base in the XSD namespace is a built-in (only the list datatypes are list-variety), a base in the schema's own target namespace resolves to a declared global, and an external or unresolvable base is skipped, so no valid restriction is rejected. XSTS: invalid schemas accepted 664 to 638, no other bucket raised (valid schemas rejected held at 72).
|
||||
- A complexType's shorthand content (without `simpleContent`/`complexContent`) is now checked for order, not just cardinality (#158). Its model is `(annotation?, (group | all | choice | sequence)?, (attribute | attributeGroup)*, anyAttribute?)`: an optional model group, then attributes, then an optional `anyAttribute`. The existing content-shape check caught the wrong number of model groups and the exclusivity of a content spec, but not the order, so a model group after an attribute, an attribute after `anyAttribute`, or a second `anyAttribute` (the ctB family) was accepted. The complexType's direct children, excluding `annotation` and the content specs, are matched against the same ordered slot model used for a `complexContent` derivation. XSTS: invalid schemas accepted 687 to 664, no other bucket raised (valid schemas rejected held at 72).
|
||||
|
||||
@ -115,19 +115,44 @@ extension PureXML.Schema {
|
||||
return base.indices.allSatisfy { used[$0] || emptiable(base[$0]) }
|
||||
}
|
||||
|
||||
/// A conservative slice of NameAndTypeOK: when a restricting element and the
|
||||
/// base element it maps onto both resolve to atomic simple types, the
|
||||
/// restricting type's built-in base must derive from the base's, so renaming
|
||||
/// an `xs:string` element to `xs:int` under restriction is rejected. Any other
|
||||
/// pairing (complex content, a list/union variety, an unresolvable or ur-type
|
||||
/// reference) is permitted here, so no valid restriction is rejected.
|
||||
/// A conservative slice of NameAndTypeOK. Two checks, both over-rejection-safe:
|
||||
/// restricting an element to an ur-type (`anyType`/`anySimpleType`) when the
|
||||
/// base has a concrete type is always widening, hence invalid; and when both
|
||||
/// elements resolve to atomic simple types, the restricting type's built-in
|
||||
/// base must derive from the base's (so an `xs:string` element renamed to
|
||||
/// `xs:int` is rejected). Any other pairing (complex content, a list/union
|
||||
/// variety, a named user type whose derivation the flattened particle model
|
||||
/// does not record) is permitted, so no valid restriction is rejected.
|
||||
private static func elementTypeRestrictionOK(_ restricted: ElementType?, _ base: ElementType?, _ types: [String: ElementType]) -> Bool {
|
||||
if isUrTypeReference(restricted, types), isConcreteType(base) { return false }
|
||||
guard let restrictedSimple = resolvedAtomic(restricted, types),
|
||||
let baseSimple = resolvedAtomic(base, types)
|
||||
else { return true }
|
||||
return restrictedSimple.base.derives(from: baseSimple.base)
|
||||
}
|
||||
|
||||
private static let urTypeNames: Set<String> = ["anyType", "anySimpleType", "anyAtomicType"]
|
||||
|
||||
/// Whether an element type is a reference to one of the XSD ur-types (the
|
||||
/// widest types, which nothing may be restricted *to* from a narrower base).
|
||||
/// A genuine ur-type is not in the named-type table; a user type that merely
|
||||
/// shares the local name (in another namespace) is, so checking absence keeps
|
||||
/// the namespace-blind reference key from misclassifying it.
|
||||
private static func isUrTypeReference(_ type: ElementType?, _ types: [String: ElementType]) -> Bool {
|
||||
if case let .typeReference(key) = type { return urTypeNames.contains(key) && types[key] == nil }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether an element type names a concrete type (a built-in or a non-ur user
|
||||
/// type), as opposed to an ur-type or an absent/unknown type.
|
||||
private static func isConcreteType(_ type: ElementType?) -> Bool {
|
||||
switch type {
|
||||
case .simple: true
|
||||
case let .typeReference(key): !urTypeNames.contains(key)
|
||||
case .complex, .none: false
|
||||
}
|
||||
}
|
||||
|
||||
/// The atomic ``SimpleType`` an element type resolves to (following
|
||||
/// ``ElementType/typeReference`` through `types`), or nil for a complex,
|
||||
/// list/union, or unresolvable type.
|
||||
|
||||
@ -145,4 +145,33 @@ struct XSDParticleRestrictionTests {
|
||||
restriction: "<xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>",
|
||||
))
|
||||
}
|
||||
|
||||
@Test("NameAndTypeOK: widening a concrete-typed element to the ur-type is rejected")
|
||||
func test_elementUrTypeWidening() {
|
||||
// Restricting a string element to anyType/anySimpleType is widening, invalid.
|
||||
#expect(!compiles(
|
||||
base: "<xs:choice><xs:element name=\"c1\" type=\"xs:string\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
restriction: "<xs:choice><xs:element name=\"c1\" type=\"xs:anyType\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
))
|
||||
#expect(!compiles(
|
||||
base: "<xs:choice><xs:element name=\"c1\" type=\"xs:string\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
restriction: "<xs:choice><xs:element name=\"c1\" type=\"xs:anySimpleType\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
))
|
||||
// The reverse (base is the ur-type, restriction narrows it) is valid.
|
||||
#expect(compiles(
|
||||
base: "<xs:choice><xs:element name=\"c1\" type=\"xs:anyType\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
restriction: "<xs:choice><xs:element name=\"c1\" type=\"xs:string\"/><xs:element name=\"c2\"/></xs:choice>",
|
||||
))
|
||||
// A user type whose local name is "anyType" (not the XSD ur-type) must not be
|
||||
// mistaken for it: this is a valid same-type restriction.
|
||||
#expect((try? PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:simpleType name="anyType"><xs:restriction base="xs:string"/></xs:simpleType>
|
||||
<xs:complexType name="B"><xs:choice><xs:element name="c1" type="t:anyType"/></xs:choice></xs:complexType>
|
||||
<xs:complexType name="R"><xs:complexContent><xs:restriction base="t:B">
|
||||
<xs:choice><xs:element name="c1" type="t:anyType"/></xs:choice>
|
||||
</xs:restriction></xs:complexContent></xs:complexType>
|
||||
</xs:schema>
|
||||
""")) != 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 = 72
|
||||
private let knownSchemaInvalidAccepted = 620
|
||||
private let knownSchemaInvalidAccepted = 616
|
||||
private let knownInstanceValidRejected = 214
|
||||
private let knownInstanceInvalidAccepted = 165
|
||||
|
||||
|
||||
@ -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 -> 620** | 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 -> 616** | 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