From b0479fd3299beaed87ff49eaee212ddebec411bc Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 17 Jun 2026 14:58:58 +0200 Subject: [PATCH] fix(schema): require a content derivation in simpleContent/complexContent (#145) A complexContent or simpleContent's content model requires exactly one restriction or extension. An empty declares no derivation and is now rejected as a structural error. XSTS invalid-schemas-accepted 200 to 197, no other bucket moved. --- CHANGELOG.md | 2 ++ Sources/Schema/SchemaContentOrder.swift | 16 ++++++++++++++++ Tests/XSTSSuiteTests.swift | 6 +++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9b512..ebd5565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ Second pre-release. Continues the W3C XSTS schema-validity campaign: **invalid-s ### Fixed +- XSTS invalid-schemas-accepted (#145): a `complexContent` or `simpleContent` must contain exactly one `restriction` or `extension`. An empty `` (or ``), which declares no derivation, is now rejected as a structural error. Caught three invalid schemas: invalid-schemas-accepted 200 → 197, no other bucket moved. + - XSTS invalid-schemas-accepted (#145): a `pattern` facet whose `\p{...}` or `\P{...}` property name can be no XSD `charProp` (`IsCategory | IsBlock`) under any repertoire is now rejected as a schema-validity error: an empty name, the bare prefix `Is`, a non-block name carrying a non-letter (`\p{\L}`), or `Is` followed by a non-block character. A well-formed but unrecognised category or block name (such as an unknown `Is`-block) stays a lenient engine limitation rather than an error, so only the unambiguously malformed forms reject. Caught three invalid schemas: invalid-schemas-accepted 203 → 200, no other bucket moved. - XSTS invalid-schemas-accepted (#145): two attribute-declaration validity rules. An attribute `ref` may not also carry `form` (the form comes from the referenced global declaration; `src-attribute.3.2`), and no attribute declaration may be named `xmlns` (that name is reserved for namespace declarations; no-xmlns). Caught four invalid schemas: invalid-schemas-accepted 207 → 203, no other bucket moved. diff --git a/Sources/Schema/SchemaContentOrder.swift b/Sources/Schema/SchemaContentOrder.swift index 290d632..163deda 100644 --- a/Sources/Schema/SchemaContentOrder.swift +++ b/Sources/Schema/SchemaContentOrder.swift @@ -22,8 +22,21 @@ extension PureXML.Schema.XSDParser { /// Only the `complexContent` context is checked, where a model group is part of /// the model; a `simpleContent` derivation (facets, no model group) has a /// different model and is left to its own rules. + /// The number of `restriction`/`extension` children of a `simpleContent` or + /// `complexContent`. Its content model requires exactly one, so zero (an empty + /// ``) or more than one is a structural error. + private static func derivationCount(_ content: XSDTree) -> Int { + PureXML.Schema.XSDNode.elementChildren(content).count { child in + guard child.name?.namespaceURI == xsdNamespace, let local = PureXML.Schema.XSDNode.localName(child) else { return false } + return local == "restriction" || local == "extension" + } + } + static func complexContentOrderErrors(_ complexContent: XSDTree) -> [String] { var errors: [String] = [] + if derivationCount(complexContent) != 1 { + errors.append("a complexContent must contain exactly one 'restriction' or 'extension'") + } for derivation in PureXML.Schema.XSDNode.elementChildren(complexContent) { guard let local = PureXML.Schema.XSDNode.localName(derivation), local == "restriction" || local == "extension" @@ -65,6 +78,9 @@ extension PureXML.Schema.XSDParser { /// table but is invalid (the ctD family). static func simpleContentOrderErrors(_ simpleContent: XSDTree) -> [String] { var errors: [String] = [] + if derivationCount(simpleContent) != 1 { + errors.append("a simpleContent must contain exactly one 'restriction' or 'extension'") + } for derivation in PureXML.Schema.XSDNode.elementChildren(simpleContent) { guard let local = PureXML.Schema.XSDNode.localName(derivation) else { continue } let slots: [(members: Set, max: Int?)] diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 7d7b0f8..59bf6f4 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -125,8 +125,12 @@ struct XSTSSuiteTests { /// followed by a non-block character: `\p{\L}`, `\p{Is}`) while staying lenient /// on a well-formed but unrecognised block name then caught three more invalid /// schemas: invalid-schemas accepted 203 → 200, no other bucket moved. + /// Requiring a `complexContent` or `simpleContent` to contain exactly one + /// `restriction` or `extension` (an empty `` has none) then + /// caught three more invalid schemas: invalid-schemas accepted 200 → 197, no + /// other bucket moved. private let knownSchemaValidRejected = 1 - private let knownSchemaInvalidAccepted = 200 + private let knownSchemaInvalidAccepted = 197 private let knownInstanceValidRejected = 3 private let knownInstanceInvalidAccepted = 132