diff --git a/CHANGELOG.md b/CHANGELOG.md index e96fb5b..67b0374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A particle whose `minOccurs` exceeds its `maxOccurs` is now rejected at compile time (#147). An occurrence range like `minOccurs="5" maxOccurs="2"` is empty and illegal, but was accepted; the two values are compared by canonical magnitude (independent of machine-integer range), with `unbounded` never exceeded and a single-point range (`minOccurs="2" maxOccurs="2"`) allowed. XSTS: invalid schemas accepted 997 to 977, no other bucket raised. - A complexType's content-model shape is now enforced at compile time (#147). XSD 1.0 allows a complexType's content to be `simpleContent`, or `complexContent`, or a model group followed by attributes, but never a mix: so at most one of `simpleContent`/`complexContent`, neither beside a model group or a direct attribute (those belong inside the derivation), and at most one model group. The `allowedChildren` set-membership check did not catch these combinations, so a malformed complexType (two `simpleContent`, `simpleContent` plus a `sequence`, two model groups) was accepted. Only the complexType's direct children are examined, so attributes legitimately nested inside an extension/restriction are not flagged. XSTS: invalid schemas accepted 1051 to 997, no other bucket raised. - Unresolvable references are now rejected at compile time (#147). Every QName a schema names (a `type`/`base`/`itemType`/`memberTypes` type, an `element`/`attribute`/`group`/`attributeGroup` `ref`, an element `substitutionGroup`) must resolve to a declared component or a built-in; an undeclared reference was previously accepted and only surfaced (if at all) lazily during instance validation. The reference is matched by local name with whitespace trimmed, as the rest of the compiler resolves it. The check is skipped when the document pulls in external definitions through `import`/`include`/`redefine`, which the default compile does not load, so a reference into them is never flagged. XSTS: invalid schemas accepted 1092 to 1051, no other bucket raised. - `pattern` facet regular expressions are now validated at compile time (#147). Each `pattern` value is compiled when the schema is; a structurally malformed one (unbalanced parentheses `((a)`, a quantifier with nothing to repeat `?a`) is rejected, where before it was only compiled lazily at instance time (`try?`) and so silently ignored. The check is conservative: a construct the engine merely does not support on an otherwise-valid pattern (the empty pattern, an untabulated `\p{Is...}` block such as a surrogate or historic block, the lenient `{,m}` quantifier) is tolerated, so no valid schema is rejected. XSTS: invalid schemas accepted 1129 to 1092, no other bucket raised. diff --git a/Sources/Schema/SchemaStructure.swift b/Sources/Schema/SchemaStructure.swift index 08473bb..bb4ceda 100644 --- a/Sources/Schema/SchemaStructure.swift +++ b/Sources/Schema/SchemaStructure.swift @@ -65,6 +65,7 @@ extension PureXML.Schema.XSDParser { let children = PureXML.Schema.XSDNode.elementChildren(node) if node.name?.namespaceURI == xsdNamespace { errors += attributeValueErrors(node) + errors += occurrenceOrderErrors(node) if let local, let allowed = allowedChildren[local] { let names = children.filter { $0.name?.namespaceURI == xsdNamespace }.compactMap(PureXML.Schema.XSDNode.localName) errors += childErrors(local: local, children: names, allowed: allowed) @@ -138,6 +139,37 @@ extension PureXML.Schema.XSDParser { return !digits.isEmpty && digits.allSatisfy { $0.isASCII && $0.isNumber } } + /// The finding, if any, for a particle whose `minOccurs` exceeds its + /// `maxOccurs`. `unbounded` is never exceeded, and a malformed value is left + /// to ``attributeValueError``; the comparison is by canonical magnitude, so it + /// holds for occurrence counts beyond a machine integer. + private static func occurrenceOrderErrors(_ node: XSDTree) -> [String] { + guard let minRaw = PureXML.Schema.XSDNode.attribute(node, "minOccurs"), + let maxRaw = PureXML.Schema.XSDNode.attribute(node, "maxOccurs") + else { return [] } + let minimum = minRaw.trimmingXMLWhitespace() + let maximum = maxRaw.trimmingXMLWhitespace() + guard maximum != "unbounded", isNonNegativeInteger(minimum), isNonNegativeInteger(maximum), + exceeds(minimum, maximum) + else { return [] } + return ["minOccurs (\(minRaw)) exceeds maxOccurs (\(maxRaw))"] + } + + /// Whether nonNegativeInteger lexical `lhs` is strictly greater than `rhs`, + /// comparing canonical magnitude (sign and leading zeros stripped) by length + /// then lexically, so it is independent of machine-integer range. + private static func exceeds(_ lhs: String, _ rhs: String) -> Bool { + let left = canonicalMagnitude(lhs), right = canonicalMagnitude(rhs) + return left.count != right.count ? left.count > right.count : left > right + } + + private static func canonicalMagnitude(_ value: String) -> Substring { + var digits = Substring(value) + if digits.first == "+" { digits = digits.dropFirst() } + let trimmed = digits.drop { $0 == "0" } + return trimmed.isEmpty ? "0" : trimmed + } + private static func childErrors(local: String, children: [String], allowed: Set) -> [String] { var errors: [String] = [] for name in children where !allowed.contains(name) { diff --git a/Tests/SchemaStructureTests.swift b/Tests/SchemaStructureTests.swift index 4b59901..68f45d0 100644 --- a/Tests/SchemaStructureTests.swift +++ b/Tests/SchemaStructureTests.swift @@ -91,6 +91,19 @@ struct SchemaStructureTests { #expect(rejects(#""#)) } + @Test("a particle's minOccurs may not exceed its maxOccurs") + func test_occurrenceOrder() throws { + func wrap(_ particle: String) -> String { + "\(particle)" + } + #expect(rejects(wrap(#""#))) + #expect(rejects(wrap(#""#))) + // Equal bounds, and unbounded, are fine. + try compile(wrap(#""#)) + try compile(wrap(#""#)) + try compile(wrap(#""#)) + } + @Test("valid enumerated and occurrence attribute values compile") func test_validAttributeValues() throws { try compile(#""#) diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 54d4a7f..17f6e3f 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 = 997 + private let knownSchemaInvalidAccepted = 977 private let knownInstanceValidRejected = 214 private let knownInstanceInvalidAccepted = 165 diff --git a/docs/schema-validity-burndown.md b/docs/schema-validity-burndown.md index f1779c4..7d23851 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 -> 997** | we rarely catch a bad one (facet, id, and structural content-model validity landed) | +| **invalid-schemas-accepted** | **2461 -> 977** | we rarely catch a bad one (facet, id, and structural content-model validity landed) | | valid-instances-rejected | 233 | (instance side, separately tracked) | | invalid-instances-accepted | 165 | | @@ -75,7 +75,7 @@ Each iteration targets one self-contained rule family with a clean root cause. (1688 -> 1313, -375)._ A data-driven `allowedChildren` table (the schema-for- schemas child content model) rejects disallowed children, multi/misplaced `annotation`, and identity constraints missing selector/field, via one general - mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness). + mechanism (`SchemaStructure.swift`). Attribute **value** validity also landed (enumerated attributes, `minOccurs`/`maxOccurs`; 1313 -> 1190), plus **name/reference lexical validity** (`name` an NCName; `type`/`base`/`ref`/`itemType`/`refer`/`substitutionGroup` QNames; 1190 -> 1129), plus **`pattern` regex validity** (compile each pattern; reject only unambiguous syntax errors, tolerating engine gaps; 1129 -> 1092), plus **complexType content-model shape** (one simpleContent/complexContent, exclusive with model groups/attributes; one model group; 1051 -> 997), plus **particle minOccurs <= maxOccurs** (997 -> 977). **Still open:** attribute **applicability** (which attributes each component admits, e.g. `form` is prohibited on a global declaration, `ref` excludes `name`); child *order* beyond the leading annotation; and particle / model-group semantic rules (Unique Particle Attribution, Particle-Valid-Restriction completeness). ## Per-iteration protocol (the PR-critic-loop) diff --git a/docs/xsts-deviations.md b/docs/xsts-deviations.md index 0ac5f1b..f339978 100644 --- a/docs/xsts-deviations.md +++ b/docs/xsts-deviations.md @@ -47,7 +47,7 @@ and to document the remainder as spec-justified, deliberate exclusions. ## Category A: genuine correctness bugs -### Fixed (twenty-three root causes, ~2138 deviations cleared) +### Fixed (twenty-four root causes, ~2158 deviations cleared) list-datatype length facets (#146) · union pattern/enumeration facets · duration partial-order facets · QName length non-constraining · the full XSD `\p{Is...}` @@ -65,7 +65,7 @@ validity (`min`/`maxInclusive`, `min`/`maxExclusive`, `enumeration` values valid in the base space; inclusive/exclusive exclusions; bound ordering) · `gMonth` `--MM--` lexical form · schema `id` attribute validity (`xs:ID`: NCName and unique within the document). The schema-validity work brought invalid schemas -accepted 2461 to 997 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`. +accepted 2461 to 977 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas); see `schema-validity-burndown.md`. Plus the measurement fix (exclude W3C-disputed `status="queried"` entries). ### Remaining, root-caused (the hard tail)