diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbd287..67f9891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Sources/Schema/ParticleRestriction.swift b/Sources/Schema/ParticleRestriction.swift index 9ef0e8c..281feeb 100644 --- a/Sources/Schema/ParticleRestriction.swift +++ b/Sources/Schema/ParticleRestriction.swift @@ -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 = ["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. diff --git a/Tests/XSDParticleRestrictionTests.swift b/Tests/XSDParticleRestrictionTests.swift index d16ee89..2dd9c92 100644 --- a/Tests/XSDParticleRestrictionTests.swift +++ b/Tests/XSDParticleRestrictionTests.swift @@ -145,4 +145,33 @@ struct XSDParticleRestrictionTests { restriction: "", )) } + + @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: "", + restriction: "", + )) + #expect(!compiles( + base: "", + restriction: "", + )) + // The reverse (base is the ur-type, restriction narrows it) is valid. + #expect(compiles( + base: "", + restriction: "", + )) + // 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(""" + + + + + + + + """)) != nil) + } } diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 41e2eaf..f74b850 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -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 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index 09ed3a5..1f29b5d 100644 --- a/docs/schema-validity-burndown.md +++ b/docs/schema-validity-burndown.md @@ -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 | |