fix(schema): reject a simpleContent restriction that relaxes a required attribute (#326)
derivation-ok-restriction.2 (a base required attribute may not become optional in a restriction) ran only over complexContent restrictions, so a simpleContent restriction that relaxed a required attribute was accepted. The attribute-use restriction check now runs over both derivation roots. Closes XSTS particlesZ030_d, which libxml2 also rejects, so this aligns with the reference rather than exceeding it. XSTS invalid-schemas-accepted 10 to 9 with valid-schemas-rejected held at 0 (full archive). Guarded by SimpleContentAttrRestrictionTests; the XSTSSuiteTests baseline is updated.
This commit is contained in:
parent
22d86fc22d
commit
ad44fe77b9
@ -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.)
|
||||
|
||||
@ -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,
|
||||
|
||||
46
Tests/SimpleContentAttrRestrictionTests.swift
Normal file
46
Tests/SimpleContentAttrRestrictionTests.swift
Normal file
@ -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 {
|
||||
"""
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<xsd:complexType name="base">
|
||||
<xsd:simpleContent>
|
||||
<xsd:extension base="xsd:string">
|
||||
<xsd:attribute name="a" use="required"/>
|
||||
</xsd:extension>
|
||||
</xsd:simpleContent>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="derived">
|
||||
<xsd:simpleContent>
|
||||
<xsd:restriction base="base">
|
||||
<xsd:attribute name="a" use="\(derivedUse)"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleContent>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
"""
|
||||
}
|
||||
|
||||
@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")))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user