diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f69a94..8c5dc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- An element or attribute declaration may no longer carry both a `default` and a `fixed` value constraint (XSD 1.0 `src-element.1` / `src-attribute.1`, #170). The two are mutually exclusive; such schemas were accepted. The check rides the existing structure walk, gated to XSD-namespace declarations and reading the unprefixed `default`/`fixed` (a foreign same-local-name attribute is not the value constraint), and is per declaration, so a `default` on a global declaration and a `fixed` on a reference to it (two separate items) is correctly allowed. XSTS: invalid schemas accepted 479 to 469, no other bucket raised. - A substitution-group member's type must now be validly derived from the type of its head (XSD 1.0 `e-props-correct.4`, #170). An element declaring `substitutionGroup="head"` whose own type is unrelated to the head's (for example a member typed `xs:int` under a head typed `xs:string`) was accepted; it is now rejected. The check runs after named types resolve and reuses the type-derivation oracle (the restriction/extension chain, continued through the built-in lattice; an ur-type head admits any member). To never reject a valid member it is conservative: it runs only for a self-contained schema (no `import`/`include`/`redefine`, where the derivation table is complete and single-namespace names are unambiguous), reads the member's type from its own `type` attribute and the head's from the top-level element declarations only (so a local element of the same name supplies neither), and stands down when either type is a list or union (the `cos-st-derived-ok` clauses 2.3/2.4 union-member derivation is not modelled). XSTS: invalid schemas accepted 480 to 479, no other bucket raised. Disclosed under-rejections: schemas that compose other documents, list/union-typed members or heads, and inline/untyped members. - An `xs:all` group referenced through a `` from inside a compositor is now rejected (XSD 1.0 `cos-all-limited.1.2`, #170): an `all` group may only be the whole content model of a complex type. A direct nested `all` was already rejected by the child-content table; this closes the group-reference path. The all-group pool is qualified by the defining schema's target namespace and a reference by its prefix, so an imported or redefined same-named group is not confused with a local one; a group redefined to a non-all content is exempt. XSTS: invalid schemas accepted 481 to 480, no other bucket raised. - An attribute or element whose type is or derives from `xs:ID` may no longer carry a `default` or `fixed` value constraint (XSD 1.0 `a-props-correct.3` / `e-props-correct.5`, #170). Such schemas were accepted. The check runs after named simple types resolve, so a named user type derived from `xs:ID` is recognized; the type reference's namespace is resolved from its prefix, so a user type named `ID` or an imported `foo:ID` is not mistaken for the built-in. XSTS: invalid schemas accepted 489 to 481, no other bucket raised. diff --git a/Sources/Schema/SchemaFacetApplicability.swift b/Sources/Schema/SchemaFacetApplicability.swift index 4e0cd56..5d62a82 100644 --- a/Sources/Schema/SchemaFacetApplicability.swift +++ b/Sources/Schema/SchemaFacetApplicability.swift @@ -29,15 +29,24 @@ extension PureXML.Schema.XSDParser { } /// A `default`/`fixed` value on an `element` or `attribute` must be a valid value - /// of its `type` (Attribute/Element Locally Valid, XSD 1.0). Only a type that - /// resolves to a built-in datatype is checked, since the check runs before the - /// types are compiled; a `fixed="abc"` on an `xs:integer` was wrongly accepted. - /// A named or inline user type, or a type in another namespace, is left alone, so - /// no valid value constraint is rejected. + /// of its `type` (Attribute/Element Locally Valid, XSD 1.0), and the two are + /// mutually exclusive (`src-element.1` / `src-attribute.1`: `default` and `fixed` + /// must not both be present). Only a type that resolves to a built-in datatype is + /// value-checked, since the check runs before the types are compiled; a + /// `fixed="abc"` on an `xs:integer` was wrongly accepted. A named or inline user + /// type, or a type in another namespace, is left alone, so no valid value + /// constraint is rejected. static func valueConstraintErrors(_ schema: XSDTree) -> [String] { let bindings = namespaceBindings(schema) var errors: [String] = [] forEachValueConstrained(schema) { node in + // The value constraint attributes are the unprefixed, no-namespace + // `default`/`fixed` on an XSD-namespace declaration; a foreign same-local + // attribute is not the value constraint and is not structurally checked. + if hasBothValueConstraints(node) { + errors.append("a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint") + return + } guard let typeName = PureXML.Schema.XSDNode.attribute(node, "type") else { return } let fixed = PureXML.Schema.XSDNode.attribute(node, "fixed") guard let value = fixed ?? PureXML.Schema.XSDNode.attribute(node, "default") else { return } @@ -60,6 +69,19 @@ extension PureXML.Schema.XSDParser { } } + /// Whether `node` carries the unprefixed (no-namespace) attribute named `local`. + private static func hasUnprefixedAttribute(_ node: XSDTree, _ local: String) -> Bool { + node.attributes.contains { $0.name.prefix == nil && $0.name.localName == local } + } + + /// Whether an XSD-namespace declaration carries both a `default` and a `fixed` + /// value constraint, which `src-element.1` / `src-attribute.1` forbid. + private static func hasBothValueConstraints(_ node: XSDTree) -> Bool { + node.name?.namespaceURI == xsdNamespace + && hasUnprefixedAttribute(node, "default") + && hasUnprefixedAttribute(node, "fixed") + } + private static let facetNames: Set = [ "minExclusive", "minInclusive", "maxExclusive", "maxInclusive", "totalDigits", "fractionDigits", "length", "minLength", "maxLength", "enumeration", "whiteSpace", "pattern", diff --git a/Tests/SchemaValueConstraintExclusionTests.swift b/Tests/SchemaValueConstraintExclusionTests.swift new file mode 100644 index 0000000..71b13ab --- /dev/null +++ b/Tests/SchemaValueConstraintExclusionTests.swift @@ -0,0 +1,32 @@ +@testable import PureXML +import Testing + +@Suite("XSD default/fixed value-constraint mutual exclusion (src-element.1 / src-attribute.1)") +struct SchemaValueConstraintExclusionTests { + private let xsd = "xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + private func compiles(_ body: String) -> Bool { + (try? PureXML.Schema.Document("\(body)")) != nil + } + + @Test("An element with both default and fixed is rejected") + func test_elementBothRejected() { + #expect(!compiles("")) + } + + @Test("An attribute with both default and fixed is rejected") + func test_attributeBothRejected() { + #expect(!compiles( + "" + + "" + + "", + )) + } + + @Test("An element or attribute with only one of default/fixed compiles") + func test_onlyOneAccepted() { + #expect(compiles("")) + #expect(compiles("")) + #expect(compiles("")) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 70d3df3..eca8200 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 = 1 - private let knownSchemaInvalidAccepted = 479 + private let knownSchemaInvalidAccepted = 469 private let knownInstanceValidRejected = 180 private let knownInstanceInvalidAccepted = 160 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index 447ac70..0579020 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 -> 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 -> 479** | 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 landed). | +| **invalid-schemas-accepted** | **2461 -> 469** | 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). | | valid-instances-rejected | **233 -> 180** | (instance side; #164's element-ref namespace fix is shared with instance validation, -34) | | invalid-instances-accepted | 160 | |