fix(schema): enforce element order and import constraints
This commit is contained in:
parent
434bfc85ea
commit
5cdcde9055
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Enforce schema element order (imports, includes, and redefines must precede other global declarations) and validate import constraints (namespace attribute must not match the schema's targetNamespace, and if missing, the schema must have a targetNamespace) (XSD 1.0 Structures §3.10.6, §3.15.2). XSTS: invalid schemas accepted 302 to 296, no other bucket raised.
|
||||
- Validate duplicate schema container names across separate files with identical content (XSD 1.0 Structures §3.14). Deduplicate schema include/import containers during compilation using both targetNamespace/structural equality and their relative schemaLocation paths to prevent false deduplication of separate documents. Keep circular self-imports/includes of the root schema valid. XSTS: invalid schemas accepted 304 to 302, no other bucket raised.
|
||||
- Validate standard `xml:lang` and `xml:space` attributes on XSD schema elements (like `<xs:documentation>` and `<xs:appinfo>`). Traversal is adjusted to process attributes on these documentation-related elements before skipping their foreign children, and validation checks the lexical pattern of `xml:lang` (using BCP 47) and restricts `xml:space` to 'default' or 'preserve'. XSTS: invalid schemas accepted 306 to 304, no other bucket raised.
|
||||
- Validate notation declarations and notation enumeration constraints on simpleTypes (XSD 1.0 Structures §3.12 / §3.14). Every `<xs:notation>` declaration must specify at least one of `public` or `system` attributes. For any simpleType restricting `xs:NOTATION`, all `enumeration` values must name a declared notation in the schema. Resolved prefix bindings are traversed recursively up the ancestor path to handle default namespaces and local namespace declarations. XSTS: invalid schemas accepted 309 to 306, no other bucket raised.
|
||||
|
||||
@ -89,15 +89,19 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// The structural findings specific to one XSD component: its admitted children
|
||||
/// (the content-model membership table) plus the per-component content-order,
|
||||
/// identity-XPath, wildcard-namespace, and consistency rules.
|
||||
private static func componentSpecificErrors(_ node: XSDTree, local: String, names: [String]) -> [String] {
|
||||
var errors: [String] = []
|
||||
if let allowed = allowedChildren(for: local, node: node) {
|
||||
errors += childErrors(local: local, children: names, allowed: allowed)
|
||||
}
|
||||
errors += derivationControlErrors(node, local: local)
|
||||
errors += typeAndGroupErrors(node, local: local, names: names)
|
||||
errors += structuralErrors(node, local: local, names: names)
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func typeAndGroupErrors(_ node: XSDTree, local: String, names: [String]) -> [String] {
|
||||
var errors: [String] = []
|
||||
switch local {
|
||||
case "group" where PureXML.Schema.XSDNode.attribute(node, "name") != nil:
|
||||
errors += namedGroupErrors(names)
|
||||
@ -110,12 +114,25 @@ extension PureXML.Schema.XSDParser {
|
||||
errors += simpleContentOrderErrors(node)
|
||||
case "selector", "field":
|
||||
errors += identityXPathErrors(node, local: local)
|
||||
default:
|
||||
break
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
private static func structuralErrors(_ node: XSDTree, local: String, names: [String]) -> [String] {
|
||||
var errors: [String] = []
|
||||
switch local {
|
||||
case "any", "anyAttribute":
|
||||
errors += wildcardNamespaceErrors(node)
|
||||
case "restriction":
|
||||
errors += restrictionErrors(node, names: names)
|
||||
case "list":
|
||||
errors += listErrors(node, names: names)
|
||||
case "schema":
|
||||
errors += schemaChildrenOrderErrors(names)
|
||||
case "import":
|
||||
errors += importErrors(node)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@ -98,4 +98,44 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
static func schemaChildrenOrderErrors(_ names: [String]) -> [String] {
|
||||
var errors: [String] = []
|
||||
var seenDeclaration = false
|
||||
for name in names {
|
||||
if ["simpleType", "complexType", "group", "attributeGroup", "element", "attribute", "notation"].contains(name) {
|
||||
seenDeclaration = true
|
||||
} else if ["include", "import", "redefine"].contains(name) {
|
||||
if seenDeclaration {
|
||||
errors.append("schema element '\(name)' must appear before any global declarations")
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
static func importErrors(_ node: XSDTree) -> [String] {
|
||||
var errors: [String] = []
|
||||
let namespaceAttr = node.attributes.first { $0.name.prefix == nil && $0.name.localName == "namespace" }?.value
|
||||
var current: XSDTree? = node.parent
|
||||
var schemaNode: XSDTree?
|
||||
while let ancestor = current {
|
||||
if ancestor.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(ancestor) == "schema" {
|
||||
schemaNode = ancestor
|
||||
break
|
||||
}
|
||||
current = ancestor.parent
|
||||
}
|
||||
if let schemaNode {
|
||||
let targetNamespace = schemaNode.attributes.first { $0.name.prefix == nil && $0.name.localName == "targetNamespace" }?.value
|
||||
if namespaceAttr == nil {
|
||||
if targetNamespace == nil {
|
||||
errors.append("import element without namespace attribute requires targetNamespace on the schema element")
|
||||
}
|
||||
} else if let namespaceAttr, namespaceAttr == targetNamespace {
|
||||
errors.append("import element's namespace attribute must not be the same as the targetNamespace of the importing schema")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,4 +89,59 @@ struct AdversarialCriticTests {
|
||||
</schema>
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("schema element order: accepts foreign elements interspersed and before imports/includes/redefines")
|
||||
func test_schemaChildrenOrder_foreignElements() throws {
|
||||
// 1. Foreign elements before imports
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ext="http://example.com/ext">
|
||||
<ext:meta info="1"/>
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="some.xsd"/>
|
||||
<xs:element name="myElement" type="xs:string"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
|
||||
// 2. Foreign elements interspersed
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ext="http://example.com/ext">
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="some.xsd"/>
|
||||
<ext:meta info="1"/>
|
||||
<xs:element name="myElement" type="xs:string"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
|
||||
// 3. Foreign elements with same local names as global declarations (e.g. element/attribute)
|
||||
// should not trigger seenDeclaration = true for the schemaChildrenOrderErrors check.
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ext="http://example.com/ext">
|
||||
<ext:element name="fake"/>
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="some.xsd"/>
|
||||
<xs:element name="myElement" type="xs:string"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
}
|
||||
|
||||
@Test("import constraints: valid import cases")
|
||||
func test_importConstraints_valid() throws {
|
||||
// 1. Importing a namespace with a schema that has a targetNamespace (different from imported namespace)
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/main">
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="other.xsd"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
|
||||
// 2. Importing a namespace with a schema that has NO targetNamespace
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="other.xsd"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
|
||||
// 3. Importing no-namespace schema with a schema that HAS targetNamespace
|
||||
_ = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/main">
|
||||
<xs:import schemaLocation="no-namespace.xsd"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,4 +234,45 @@ struct SchemaStructureTests {
|
||||
<xs:element name="a"><xs:complexType><xs:attribute name="id" type="xs:string"/></xs:complexType></xs:element>
|
||||
"""#))
|
||||
}
|
||||
|
||||
@Test("schema elements must follow order: imports/includes/redefines before declarations")
|
||||
func test_schemaChildrenOrder() throws {
|
||||
// Redefine after element is rejected
|
||||
#expect(rejects(#"""
|
||||
<xs:element name="t" type="xs:string" />
|
||||
<xs:redefine schemaLocation="redefinebug.red">
|
||||
<xs:complexType name="tabletype"><xs:sequence/></xs:complexType>
|
||||
</xs:redefine>
|
||||
"""#))
|
||||
// Import after element is rejected
|
||||
#expect(rejects(#"""
|
||||
<xs:element name="t" type="xs:string" />
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="some.imp" />
|
||||
"""#))
|
||||
// Include after element is rejected
|
||||
#expect(rejects(#"""
|
||||
<xs:element name="t" type="xs:string" />
|
||||
<xs:include schemaLocation="some.inc" />
|
||||
"""#))
|
||||
// Correct order is compiled successfully
|
||||
try compile(#"""
|
||||
<xs:import namespace="http://example.com/other" schemaLocation="some.imp" />
|
||||
<xs:element name="t" type="xs:string" />
|
||||
"""#)
|
||||
}
|
||||
|
||||
@Test("import constraints: namespace attribute presence and matching targetNamespace")
|
||||
func test_importConstraints() throws {
|
||||
// Import without namespace attribute in a schema with NO targetNamespace is rejected
|
||||
#expect(rejects(#"<xs:import schemaLocation="test.imp" />"#))
|
||||
|
||||
// Import whose namespace is the same as the targetNamespace is rejected
|
||||
#expect(throws: Error.self) {
|
||||
_ = try PureXML.Schema.Document(#"""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com">
|
||||
<xs:import namespace="http://example.com" schemaLocation="test.imp" />
|
||||
</xs:schema>
|
||||
"""#)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ struct XSTSSuiteTests {
|
||||
/// XSTS` (debug is far slower and the corpus is large). Per-case deviations
|
||||
/// are written to /tmp/xsts-failures.txt for the burn-down.
|
||||
private let knownSchemaValidRejected = 1
|
||||
private let knownSchemaInvalidAccepted = 302
|
||||
private let knownSchemaInvalidAccepted = 296
|
||||
private let knownInstanceValidRejected = 171
|
||||
private let knownInstanceInvalidAccepted = 155
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ irreducible tail. Do not deviate to new features while this is open.
|
||||
| Bucket | Count | Reading |
|
||||
|---|---|---|
|
||||
| valid-schemas-rejected | **72 -> 4** | production target is 0: rejecting a correct schema is the most user-hostile defect (#148). Particle-restriction over-rejection fixed: pointless `maxOccurs=0` particles removed, content-free restriction of an EMPTY base, empty-derived emptiable-base, and (#163) the pairwise check over the normalized content model with occurrence subsumption applied only to same-kind pairings (cross-kind element/group, group/element, group/wildcard match occurrence inside, by effective total range / member). #164 resolved element-ref namespaces by prefix binding (the `particlesJj/Jk/Jm/Jn/Js`, `particlesQ/R` cluster, -28). Content-free (`maxOccurs=0`) derived groups and substitution-group declaration order closed `particlesW006`/`elemZ027_a`; the W3C MapAndSum rule (Sequence:Choice, #165) closed `particlesV003`. Remaining 1 (`particlesZ001`): a documented spec ambiguity (RecurseAsIfGroup; the reference rejects it), not a defect. |
|
||||
| **invalid-schemas-accepted** | **2461 -> 302** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes, cross-container component-name and identity-constraint uniqueness checks, notation declarations and notation enumeration restrictions, xml:lang and xml:space on documentation/appinfo elements, duplicate schema container validation across separate files with identical content checks landed). |
|
||||
| **invalid-schemas-accepted** | **2461 -> 296** | we rarely catch a bad one (facet, default/fixed value, particle-restriction name+type, id, structural content-model, UPA determinism, content order, identity-constraint XPath-subset, wildcard-namespace, final/block, simpleType content, group/wildcard and group/element cardinality, element-ref namespace, sequence/choice MapAndSum, circular type derivation, circular group/attributeGroup/substitution references, attribute-use uniqueness and single-ID, type-excludes-inline-type, ID-typed value-constraint exclusion, all-group reference placement, substitution-member type derivation, default/fixed mutual exclusion, elementTypeRestrictionOK NameAndTypeOK union checks, prohibited attribute type-check bypass, keyref arity verification, top-level group/attributeGroup local-attribute checks, simpleType restriction structure and prefix binding checks, namespace-aware reference resolution checks, simpleType variety constraints on list itemType and union memberTypes, targetNamespace scope matching on local schema references, substitution group exclusions based on element final and schema finalDefault attributes, cross-container component-name and identity-constraint uniqueness checks, notation declarations and notation enumeration restrictions, xml:lang and xml:space on documentation/appinfo elements, duplicate schema container validation across separate files with identical content, schema element order, and import constraints validation landed). |
|
||||
| valid-instances-rejected | **233 -> 171** | (instance side; #164's element-ref namespace fix is shared with instance validation, complexContent restriction attributes inheritance and namespace fixes) |
|
||||
| invalid-instances-accepted | **160 -> 155** | (instance side; resolving inline `<simpleType>` restriction base, complexContent restriction attributes prohibition) |
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ entries removed about 70 apparent deviations that were never bugs.
|
||||
| Kind | First measurement | Now |
|
||||
|---|---|---|
|
||||
| valid schema rejected | 75 | 1 |
|
||||
| invalid schema accepted | 2467 | 318 |
|
||||
| invalid schema accepted | 2467 | 296 |
|
||||
| valid instance rejected | 604 | 171 |
|
||||
| invalid instance accepted | 582 | 155 |
|
||||
|
||||
@ -65,7 +65,7 @@ validity (`min`/`maxInclusive`, `min`/`maxExclusive`, `enumeration` values valid
|
||||
in the base space; inclusive/exclusive exclusions; bound ordering) · `gMonth`
|
||||
`--MM--` lexical form · schema `id` attribute validity (`xs:ID`: NCName and
|
||||
unique within the document). The schema-validity work brought invalid schemas
|
||||
accepted 2461 to 318 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas, plus simpleType variety constraints on list itemType and union memberTypes, plus targetNamespace scope matching on local schema references, plus substitution group exclusions based on element final and schema finalDefault attributes); see `schema-validity-burndown.md`.
|
||||
accepted 2461 to 296 (schema-document structural validity: content model + complexType content shape, attribute values, name/reference + pattern-regex lexical validity, and reference resolution, against the schema-for-schemas, plus simpleType variety constraints on list itemType and union memberTypes, plus targetNamespace scope matching on local schema references, plus substitution group exclusions based on element final and schema finalDefault attributes, duplicate schema container validation across separate files with identical content, schema element order, and import constraints validation); see `schema-validity-burndown.md`.
|
||||
Plus the measurement fix (exclude W3C-disputed `status="queried"` entries).
|
||||
|
||||
### Remaining, root-caused (the hard tail)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user