From 488f58701adecd218c0dc4c281fc6598db324ef4 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Thu, 18 Jun 2026 15:28:35 +0200 Subject: [PATCH] fix(schema): exclude use="prohibited" attributes from effective uses at instance time; invalid-instances-accepted 43 to 39 (#147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use="prohibited" removes an attribute from a complex type's effective {attribute uses} (XSD 1.0 §3.4.2): the attribute is then undeclared, so its presence is rejected when no attribute wildcard admits it (attJ), but is still admitted (and validated per the wildcard's processContents) when one does (attZ002). The use had been parsed as merely optional, so a prohibited attribute was always allowed. AttributeUse gains a `prohibited` flag, set for both a local declaration and an use site. The declaration is kept in the type's attribute list so schema-validity checks still see it, while instance validation excludes prohibited uses: the matching loop and the wildcard's declared-set skip them, and the ID-type count ignores them. There is no explicit "prohibited attribute present" error, so a wildcard that admits the attribute name still governs it (an earlier attempt to reject outright wrongly overrode the wildcard). invalid-instances-accepted 43 to 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0, invalid-schemas-accepted unchanged at 69). An adversarial review confirmed prohibited attributes admitted by a wildcard (every namespace/processContents variation, local and ref), restriction removal, and the ID-count interaction are all handled without over-rejection. --- CHANGELOG.md | 2 + Sources/Schema/ComplexType.swift | 6 ++ .../Schema/ComplexValidatorAttributes.swift | 9 ++- Sources/Schema/XSDAttributeParser.swift | 65 ++++++++++--------- Tests/SchemaProhibitedAttributeTests.swift | 41 ++++++++++++ Tests/XSTSSuiteTests.swift | 4 +- 6 files changed, 92 insertions(+), 35 deletions(-) create mode 100644 Tests/SchemaProhibitedAttributeTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feb7bf..31356ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- XSTS invalid-instances-accepted (#147): `use="prohibited"` on an attribute now excludes it from the complex type's effective `{attribute uses}` (XSD 1.0 §3.4.2) during instance validation, so a prohibited attribute is undeclared: its presence is rejected when no attribute wildcard admits it (attJ), and still admitted (and validated per the wildcard's `processContents`) when one does (attZ002). The prohibited declaration is kept in the type rather than dropped, so schema-validity checks still see it (the invalid-schemas-accepted count is unchanged). invalid-instances-accepted 43 → 39, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. + - XSTS invalid-instances-accepted (#147): an element may now carry at most one attribute whose type is `xs:ID` (XSD 1.0 §3.4.6). Two ID-typed attributes admitted through an `xs:anyAttribute` wildcard (the schema itself stays valid, as a type with two declared ID attribute uses is already a compile error) are rejected at instance time. The count covers both a matched declared use's type and the global declaration a matching wildcard admits, including a restriction of `xs:ID`; `xs:IDREF`/`xs:ENTITY` and a lone ID attribute are unaffected. invalid-instances-accepted 45 → 43, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. - XSTS invalid-instances-accepted (#146, #147): an identity-constraint (`unique`/`key`/`keyref`) field that selects an absent attribute or an empty element whose declaration carries a `default`/`fixed` now takes that value as its identity-tuple component, so two members can collide on the defaulted value (idG011/idG012 default, idF016/idF017 fixed). The schema's `default`/`fixed` value constraints on identity-field targets are gathered at compile time, keyed the same way as the field types, and threaded through to the identity validator. The substitution is exact about absent versus present-empty: an `@attr` field is defaulted only when the attribute is ABSENT (empty node-set), never when it is present with value `""`; a `.` element field is defaulted only when the selected element has no character content. Applying a default only adds an identity value where the schema declares one, so it can create a genuine duplicate-key detection but never reject a valid instance. invalid-instances-accepted 50 → 45, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. diff --git a/Sources/Schema/ComplexType.swift b/Sources/Schema/ComplexType.swift index d114aee..45e9b31 100644 --- a/Sources/Schema/ComplexType.swift +++ b/Sources/Schema/ComplexType.swift @@ -240,6 +240,10 @@ public extension PureXML.Schema { /// When true, an unprefixed instance attribute may match this use if the /// element is in the same namespace (chameleon include only). public var chameleonUnprefixed: Bool + /// `use="prohibited"`: the attribute is excluded from the type's effective + /// {attribute uses}, so it must not appear on an instance. The use is kept + /// (rather than dropped) so schema-validity checks still see the declaration. + public var prohibited: Bool public init( name: PureXML.Model.QualifiedName, @@ -247,12 +251,14 @@ public extension PureXML.Schema { required: Bool = false, valueConstraint: ValueConstraint? = nil, chameleonUnprefixed: Bool = false, + prohibited: Bool = false, ) { self.name = name self.type = type self.required = required self.valueConstraint = valueConstraint self.chameleonUnprefixed = chameleonUnprefixed + self.prohibited = prohibited } } diff --git a/Sources/Schema/ComplexValidatorAttributes.swift b/Sources/Schema/ComplexValidatorAttributes.swift index 9c408db..81c8088 100644 --- a/Sources/Schema/ComplexValidatorAttributes.swift +++ b/Sources/Schema/ComplexValidatorAttributes.swift @@ -6,7 +6,7 @@ extension PureXML.Schema.ComplexValidator { into errors: inout [PureXML.Validation.ValidationError], ) { let present = element.attributes.filter { !Self.isNamespaceDeclaration($0) && !Self.isSchemaInstanceAttribute($0) } - for use in type.attributes { + for use in type.attributes where !use.prohibited { let match = present.first { Self.attributeMatches($0.name, declared: use.name, use: use, on: element.name) } if let match { if let error = use.type.validate(match.value) { @@ -50,7 +50,7 @@ extension PureXML.Schema.ComplexValidator { _ type: PureXML.Schema.ComplexType, on element: PureXML.Model.Element, ) -> Bool { - if let use = type.attributes.first(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: element.name) }) { + if let use = type.attributes.first(where: { !$0.prohibited && Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: element.name) }) { return use.type.isID } guard let wildcard = type.attributeWildcard, wildcard.admits(attribute.name), @@ -82,7 +82,10 @@ extension PureXML.Schema.ComplexValidator { at path: [PureXML.Validation.PathKey], into errors: inout [PureXML.Validation.ValidationError], ) { - let declared = type.attributes + // A `use="prohibited"` use is excluded from the effective {attribute uses}, + // so a prohibited attribute is treated as undeclared and left to the + // attribute wildcard (which may still admit it) or reported as undeclared. + let declared = type.attributes.filter { !$0.prohibited } let elementName = element.name let wildcard = type.attributeWildcard for attribute in present where !declared.contains(where: { Self.attributeMatches(attribute.name, declared: $0.name, use: $0, on: elementName) }) { diff --git a/Sources/Schema/XSDAttributeParser.swift b/Sources/Schema/XSDAttributeParser.swift index 97d620a..ea86c93 100644 --- a/Sources/Schema/XSDAttributeParser.swift +++ b/Sources/Schema/XSDAttributeParser.swift @@ -47,39 +47,12 @@ extension PureXML.Schema.XSDParser { } static func attributeUse(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.AttributeUse? { - // An `` references a global attribute declaration: - // take its name (always target-namespace qualified) and type from the - // global node, but `use`/`default`/`fixed` from this reference site. if let ref = PureXML.Schema.XSDNode.attribute(node, "ref") { - let refName = PureXML.Schema.XSDNode.stripPrefix(ref) - let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings) - let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings) - if namespace == "http://www.w3.org/XML/1998/namespace", refName == "base" { - return PureXML.Schema.AttributeUse( - name: PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace), - type: PureXML.Schema.SimpleType(base: .anyURI), - required: PureXML.Schema.XSDNode.attribute(node, "use") == "required", - valueConstraint: valueConstraint(of: node), - ) - } - guard let declaration = context.globalAttributes[refName], - var use = attributeUse(declaration, context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration))) - else { return nil } - let declContext = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration)) - if let target = declContext.targetNamespace, !target.isEmpty { - use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: target) - use.chameleonUnprefixed = isChameleonSchema(PureXML.Schema.XSDNode.schemaOwner(declaration)) - } else { - let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings) - let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings) - use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace) - } - if PureXML.Schema.XSDNode.attribute(node, "use") == "required" { use.required = true } - if let constraint = valueConstraint(of: node) { use.valueConstraint = constraint } - return use + return referencedAttributeUse(node, ref: ref, context) } guard let name = PureXML.Schema.XSDNode.attribute(node, "name") else { return nil } - let required = PureXML.Schema.XSDNode.attribute(node, "use") == "required" + let use = PureXML.Schema.XSDNode.attribute(node, "use") + let required = use == "required" let type: PureXML.Schema.SimpleType = if let typeName = PureXML.Schema.XSDNode.attribute(node, "type") { PureXML.Schema.XSDSimpleParser.simpleTypeReference(typeName, context) } else if let inline = PureXML.Schema.XSDNode.firstChild(node, named: "simpleType") { @@ -98,9 +71,41 @@ extension PureXML.Schema.XSDParser { required: required, valueConstraint: valueConstraint(of: node), chameleonUnprefixed: qualified && isChameleonSchema(owner), + prohibited: use == "prohibited", ) } + /// An `` use: it takes its name (always target-namespace + /// qualified) and type from the referenced global declaration, but its + /// `use`/`default`/`fixed` from this reference site. + private static func referencedAttributeUse(_ node: XSDTree, ref: String, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.AttributeUse? { + let refName = PureXML.Schema.XSDNode.stripPrefix(ref) + let bindings = namespaceBindingsInScope(of: node, defaultBindings: context.namespaceBindings) + let namespace = PureXML.Schema.XSDNode.referenceNamespace(ref, bindings) + if namespace == "http://www.w3.org/XML/1998/namespace", refName == "base" { + return PureXML.Schema.AttributeUse( + name: PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace), + type: PureXML.Schema.SimpleType(base: .anyURI), + required: PureXML.Schema.XSDNode.attribute(node, "use") == "required", + valueConstraint: valueConstraint(of: node), + ) + } + guard let declaration = context.globalAttributes[refName], + var use = attributeUse(declaration, context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration))) + else { return nil } + let declContext = context.scoped(for: PureXML.Schema.XSDNode.schemaOwner(declaration)) + if let target = declContext.targetNamespace, !target.isEmpty { + use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: target) + use.chameleonUnprefixed = isChameleonSchema(PureXML.Schema.XSDNode.schemaOwner(declaration)) + } else { + use.name = PureXML.Model.QualifiedName(localName: refName, namespaceURI: namespace) + } + if PureXML.Schema.XSDNode.attribute(node, "use") == "required" { use.required = true } + if PureXML.Schema.XSDNode.attribute(node, "use") == "prohibited" { use.prohibited = true } + if let constraint = valueConstraint(of: node) { use.valueConstraint = constraint } + return use + } + /// A schema document with no explicit `targetNamespace` (a chameleon include). private static func isChameleonSchema(_ schema: XSDTree) -> Bool { guard PureXML.Schema.XSDNode.localName(schema) == "schema" else { return false } diff --git a/Tests/SchemaProhibitedAttributeTests.swift b/Tests/SchemaProhibitedAttributeTests.swift new file mode 100644 index 0000000..296d183 --- /dev/null +++ b/Tests/SchemaProhibitedAttributeTests.swift @@ -0,0 +1,41 @@ +@testable import PureXML +import Testing + +/// `use="prohibited"` excludes an attribute from a complex type's effective +/// {attribute uses} (XSD 1.0 §3.4.2): the attribute is then undeclared, so its +/// presence is rejected when no wildcard admits it, but is still admitted (and +/// validated per `processContents`) when an attribute wildcard covers it. The +/// prohibited declaration is kept for schema-validity checks, not dropped. +@Suite("prohibited attribute use") +struct SchemaProhibitedAttributeTests { + @Test("a prohibited attribute with no wildcard is rejected when present") + func test_prohibitedNoWildcardRejected() throws { + let schema = try PureXML.Schema.Document(""" + + + + + + + """) + #expect(try !schema.validate(#""#).isEmpty) + // Omitting the prohibited attribute, with the allowed one, is valid. + #expect(try schema.validate(#""#).isEmpty) + } + + /// The W3C attZ002 shape: a prohibited attribute is still admitted by a lax + /// `anyAttribute` wildcard, so the instance is valid (the prohibition removes + /// the use, the wildcard independently admits the name). + @Test("a prohibited attribute admitted by a wildcard is accepted") + func test_prohibitedAdmittedByWildcardAccepted() throws { + let schema = try PureXML.Schema.Document(""" + + + + + + + """) + #expect(try schema.validate(#""#).isEmpty) + } +} diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 8e0231b..6027988 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 69 private let knownInstanceValidRejected = 0 - /// Instance bucket (133 -> 43), per-step deltas in CHANGELOG.md: xsi:type must derive from the + /// Instance bucket (133 -> 39), per-step deltas in CHANGELOG.md: xsi:type must derive from the /// declared type (complex, then atomic and list/union, resolved through the element-ref chain); /// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the /// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`; /// identity fields compare in the decimal value space and take a declared default/fixed when /// absent or empty; a complex type's attribute wildcard is the INTERSECTION of its sources. - private let knownInstanceInvalidAccepted = 43 + private let knownInstanceInvalidAccepted = 39 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {