diff --git a/CHANGELOG.md b/CHANGELOG.md index b51eac4..505edaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A complex type with simple content that restricts a base may no longer relax a base `required` attribute to `optional` (XSD 1.0 `cos-ct-derived-ok` / `derivation-ok-restriction.2`). The attribute-use restriction check ran only over `complexContent` restrictions, so a `simpleContent` restriction that relaxed a required attribute (or added a non-base attribute, or widened the attribute wildcard) was accepted; it now runs over both derivation roots. Closes XSTS `particlesZ030_d`, which libxml2 also rejects, so the fix aligns with the reference rather than exceeding it. XSTS invalid-schemas-accepted 10 -> 9 with valid-schemas-rejected held at 0 (full archive). Guarded by `SimpleContentAttrRestrictionTests`. + - `xpointer(string-range(...))` no longer uses a quadratic substring search. It matched the search string at every position of each node's value and re-allocated the whole needle-width window per position (O(text * needle), a denial-of-service vector on a long node value with a near-matching needle); it now uses a linear-time Knuth-Morris-Pratt search with the failure function built once. The matches are unchanged: same non-overlapping, left-to-right occurrences, same offset/length. A near-matching needle over a 16000-character node drops from about 0.18 s to about 0.002 s (~100x), and the cost is now linear. Guarded by the new non-overlapping and backtracking cases in `XPointerRangeTests`. - The XSLT `id()` function no longer scans the whole document ID index on every call. It filtered the entire index by `tokens.contains(...)` (O(index) per call), so a stylesheet calling `id()` across a wide document was quadratic; it now looks each requested token up directly (O(tokens)). The result is unchanged: the same elements in document order. A stylesheet resolving 4000 `id()` references against a 4000-entry index drops from about 0.34 s to about 0.03 s (~13x), and the cost is now linear. (`key()` already used direct lookups.) diff --git a/Sources/Schema/SchemaAttributeRestriction.swift b/Sources/Schema/SchemaAttributeRestriction.swift index bc35c7b..9b55cdd 100644 --- a/Sources/Schema/SchemaAttributeRestriction.swift +++ b/Sources/Schema/SchemaAttributeRestriction.swift @@ -27,7 +27,11 @@ extension PureXML.Schema.XSDParser { guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] } let bindings = context.namespaceBindings var findings: [PureXML.Schema.SchemaLocatedFinding] = [] - for content in descendants(schema, named: "complexContent") { + // The attribute-use restriction rules apply to a restriction of either + // content kind: a `simpleContent` restriction (a complex type with simple + // content plus attributes) narrows its base attributes by the same rules as + // a `complexContent` one, so both derivation roots are checked here. + for content in descendants(schema, named: "complexContent") + descendants(schema, named: "simpleContent") { guard let restriction = AttrRestrictNode.firstChild(content, named: "restriction"), let base = AttrRestrictNode.attribute(restriction, "base"), AttrRestrictNode.referenceNamespace(base, bindings) == context.targetNamespace, diff --git a/Tests/SimpleContentAttrRestrictionTests.swift b/Tests/SimpleContentAttrRestrictionTests.swift new file mode 100644 index 0000000..305c487 --- /dev/null +++ b/Tests/SimpleContentAttrRestrictionTests.swift @@ -0,0 +1,46 @@ +import Testing +@testable import PureXML + +/// derivation-ok-restriction.2 for simpleContent: a complex type with simple +/// content that restricts a base may not relax a base `required` attribute to +/// `optional`. The attribute-use restriction check previously ran only over +/// `complexContent` restrictions, so a `simpleContent` restriction that relaxed a +/// required attribute was wrongly accepted (XSTS particlesZ030_d, which libxml2 +/// also rejects). Corpus-free pin of the rule. +@Suite("simpleContent attribute-use restriction") +struct SimpleContentAttrRestrictionTests { + private func compiles(_ schema: String) -> Bool { + (try? PureXML.Schema.Document(schema)) != nil + } + + private func schema(derivedUse: String) -> String { + """ + + + + + + + + + + + + + + + + + """ + } + + @Test("a simpleContent restriction may not relax a required attribute to optional") + func test_requiredToOptionalRejected() { + #expect(!compiles(schema(derivedUse: "optional"))) + } + + @Test("a simpleContent restriction that keeps the attribute required compiles") + func test_requiredKeptAccepted() { + #expect(compiles(schema(derivedUse: "required"))) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 436a231..2327b9a 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -39,8 +39,12 @@ struct XSTSSuiteTests { /// extension that is not expressible per Errata E1-10 is rejected (wildZ013); /// 11 -> 10, a redefine self-reference's expansion is now seen by the UPA check /// and that check runs over every composition root, so a redefined group that - /// duplicates a particle across the redefine chain is rejected (schN10). - private let knownSchemaInvalidAccepted = 10 + /// duplicates a particle across the redefine chain is rejected (schN10); + /// 10 -> 9, the attribute-use restriction check (derivation-ok-restriction.2: + /// a base `required` attribute may not become `optional`) now also runs over a + /// `simpleContent` restriction, not only `complexContent` (particlesZ030_d, + /// which libxml2 also rejects). + private let knownSchemaInvalidAccepted = 9 private let knownInstanceValidRejected = 0 /// Instance bucket (133 -> 22), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared /// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its head's