fix(schema): exclude use="prohibited" attributes from effective uses at instance time; invalid-instances-accepted 43 to 39 (#147)
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
<attribute ref> 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.
This commit is contained in:
parent
91894d5759
commit
488f58701a
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) }) {
|
||||
|
||||
@ -47,39 +47,12 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
|
||||
static func attributeUse(_ node: XSDTree, _ context: PureXML.Schema.XSDContext) -> PureXML.Schema.AttributeUse? {
|
||||
// An `<attribute ref="...">` 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 `<attribute ref="...">` 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 }
|
||||
|
||||
41
Tests/SchemaProhibitedAttributeTests.swift
Normal file
41
Tests/SchemaProhibitedAttributeTests.swift
Normal file
@ -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("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="e"><xs:complexType>
|
||||
<xs:attribute name="gone" use="prohibited" type="xs:string"/>
|
||||
<xs:attribute name="ok" type="xs:string"/>
|
||||
</xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try !schema.validate(#"<e gone="y"/>"#).isEmpty)
|
||||
// Omitting the prohibited attribute, with the allowed one, is valid.
|
||||
#expect(try schema.validate(#"<e ok="x"/>"#).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("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified">
|
||||
<xs:element name="root"><xs:complexType>
|
||||
<xs:attribute name="attr" use="prohibited"/>
|
||||
<xs:anyAttribute namespace="##local" processContents="lax"/>
|
||||
</xs:complexType></xs:element>
|
||||
</xs:schema>
|
||||
""")
|
||||
#expect(try schema.validate(#"<root attr="123"/>"#).isEmpty)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user