From b6e5cd8f3fae597271d7c238db1693f3bda838ad Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 22 Jun 2026 02:26:38 +0200 Subject: [PATCH] fix(schema): reject redefined attribute group that widens a re-declared attribute's type A redefinition of an attributeGroup is a restriction of the original (src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive by restriction from the original's. The attribute-restriction checks covered drop-required, re-introduce-prohibited, add-attribute, and relax-fixed, but not a type change that widens rather than restricts. Add a built-in-only, namespace-gated check: it fires only when both the original and the re-declared attribute name a built-in type resolving to the XSD namespace and the re-declared built-in does not derive from the original's in the XSD derivation lattice (int does not derive from boolean). A user, inline, or absent type on either side is not compared, so the check declines and stays lenient: an absent original type is anySimpleType (which any type restricts), a user/inline type's derivation is not resolvable here, and a user type whose local name coincides with a built-in resolves to a different namespace and is not mistaken for the built-in. This catches W3C XSTS schM4 (a redefinition re-typing an attribute from boolean to int). XSTS schema invalid-accepted 24 -> 23, valid-rejected held at 0. --- CHANGELOG.md | 1 + .../Schema/SchemaRedefineSelfReference.swift | 34 ++++++++++++++ Tests/SchemaRedefineAttributeGroupTests.swift | 46 +++++++++++++++++++ Tests/XSTSSuiteTests.swift | 4 +- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60db601..5587af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A redefinition of an `attributeGroup` that WIDENS a re-declared attribute's type is now rejected. A redefinition is a restriction (src-redefine / cos-ct-restricts), so a re-declared attribute's type must derive by restriction from the original's. The check is built-in-only and therefore certain: it fires only when both the original and the re-declared attribute name a built-in type (resolved to the XSD namespace) and the re-declared built-in does not derive from the original's in the XSD derivation lattice (`int` does not derive from `boolean`). A user, inline, or absent type on either side is not compared (declines, staying lenient): an absent original type is `anySimpleType`, which any type validly restricts, a user/inline type's derivation is not resolvable here, and a user type whose local name coincides with a built-in (a `{urn:x}int`) resolves to a different namespace and is not mistaken for the built-in, so no valid restriction is rejected. Catches schM4 (`b1` re-typed `boolean` → `int`). XSTS invalid-schemas-accepted 24 → 23 (schM4), valid-schemas-rejected held at 0. - A redefinition of an `attributeGroup` that adds an attribute the original lacks through a NON-self `attributeGroup` reference is now rejected. A redefinition is a restriction of the original (src-redefine / cos-ct-restricts), so it may not introduce an attribute the original does not declare unless the original carries an attribute wildcard. The add-attribute check previously resolved only the redefinition's DIRECT declarations and was suppressed whenever the redefinition carried any `attributeGroup` reference, which conflated a self-reference (re-importing the original's own attributes, valid) with a non-self reference (injecting foreign attributes, invalid). The redefinition's contributed attribute names are now resolved through its non-self references and the suppression keys on a genuine self-reference (matched by namespace and local name); resolution declines, staying lenient, whenever the contributed set is not fully knowable (an unresolved group, an attribute-by-`ref`, an `anyAttribute` wildcard, or a further nested reference). This catches attgC028 (a redefinition whose `ref="car"` replaces the original's attributes with foreign ones) while keeping attgC007/attgC038/groupB018 (added attributes alongside a self-reference) valid. XSTS invalid-schemas-accepted 25 → 24 (attgC028), valid-schemas-rejected held at 0. - An unprefixed `type`/`base`/`itemType`/`memberTypes` reference to a type declared in a non-empty target namespace is now rejected as undeclared. With no default `xmlns`, an unprefixed QName resolves to the absent namespace, so `type="t"` is `{}t`, not `{urn:x}t` where the type lives; `isUndeclaredReferenceType` matched the type by local name and accepted it. It now mirrors `isUndeclaredReferenceRef`: a no-namespace reference is undeclared when the schema has a non-empty target namespace (a chameleon-included global, and an imported no-namespace type via the foreign pools, remain accepted). XSTS invalid-schemas-accepted 27 → 25 (addB009/test66059 and one more), valid-schemas-rejected held at 0. - A schema reference (`ref`/`substitutionGroup`) that resolves to the XSD namespace is now rejected as undeclared. The XSD namespace declares no referenceable user component, so `isUndeclaredReferenceType` already rejected a `type`/`base` in it, but `isUndeclaredReferenceRef` fell through to a lenient accept. This is reached when an unprefixed reference picks up a default `xmlns` of the XSD namespace, so it is not in the target namespace where the schema's own components live: e.g. `xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="foo"` with `` resolves to `{XSD}root`, not `{foo}root`, and is undeclared. A schema that itself targets the XSD namespace, and a genuinely loaded XSD-namespace document, are unaffected. XSTS invalid-schemas-accepted 29 → 27 (xsd013/xsd014), valid-schemas-rejected held at 0. diff --git a/Sources/Schema/SchemaRedefineSelfReference.swift b/Sources/Schema/SchemaRedefineSelfReference.swift index c11d105..0778458 100644 --- a/Sources/Schema/SchemaRedefineSelfReference.swift +++ b/Sources/Schema/SchemaRedefineSelfReference.swift @@ -194,6 +194,9 @@ extension PureXML.Schema.XSDParser { if relaxesFixedValue(base: attribute, redefined: redefined[attrName], use: use(attrName)) { errors.append("a redefined attribute group '\(name)' may not relax the fixed value of '\(attrName)'") } + if use(attrName) != "prohibited", widensBuiltinType(base: attribute, redefined: redefined[attrName]) { + errors.append("a redefined attribute group '\(name)' may not widen the type of '\(attrName)'") + } switch RedefineNode.attribute(attribute, "use") { case "required" where !hasReference && redefined[attrName] == nil: errors.append("a redefined attribute group '\(name)' may not eliminate the required attribute '\(attrName)'") @@ -230,6 +233,37 @@ extension PureXML.Schema.XSDParser { .map { "a redefined attribute group '\(name)' may not add the attribute '\($0)'" } } + /// Whether a redefined attribute WIDENS the original's type. A redefinition is a + /// restriction, so a re-declared attribute's type must derive by restriction from + /// the original's. The check is BUILT-IN-only and therefore certain: it fires only + /// when both the original and the re-declared attribute name a built-in type and the + /// re-declared built-in does not derive from the original's in the XSD derivation + /// lattice (e.g. `int` does not derive from `boolean`, XSTS schM4). A user, inline, + /// or absent type on either side is not compared (returns false, staying lenient): + /// an absent original type is `anySimpleType`, which any type validly restricts, and + /// a user/inline type's derivation is not resolvable here, so no valid restriction + /// is rejected. + private static func widensBuiltinType(base: XSDTree, redefined: XSDTree?) -> Bool { + guard let redefined, + let baseBuiltin = builtinType(of: base), + let redefinedBuiltin = builtinType(of: redefined) + else { return false } + return !redefinedBuiltin.derives(from: baseBuiltin) + } + + /// The built-in type an attribute names, ONLY when its `type` QName resolves to the + /// XSD namespace. A user type whose local name coincides with a built-in (a + /// `{urn:x}int`) resolves to a different namespace and returns nil, so it is not + /// mistaken for the built-in and `widensBuiltinType` declines rather than reject a + /// valid restriction. The QName is resolved against the owning schema's bindings, + /// the same context `redefineSelfReferenceFindings` uses for redefine references. + private static func builtinType(of attribute: XSDTree) -> PureXML.Schema.BuiltinType? { + guard let type = RedefineNode.attribute(attribute, "type") else { return nil } + let bindings = RedefineNode.namespaceBindings(of: RedefineNode.schemaOwner(attribute)) + guard RedefineNode.referenceNamespace(type, bindings) == xsdNamespace else { return nil } + return PureXML.Schema.BuiltinType(rawValue: RedefineNode.stripPrefix(type)) + } + /// A base attribute with a `fixed` value is relaxed when the redefinition declares /// a matching, non-prohibited attribute whose `fixed` value differs (or is absent). /// Prohibiting or omitting the attribute is not relaxation, so it is left alone. diff --git a/Tests/SchemaRedefineAttributeGroupTests.swift b/Tests/SchemaRedefineAttributeGroupTests.swift index c11ccc5..02800f0 100644 --- a/Tests/SchemaRedefineAttributeGroupTests.swift +++ b/Tests/SchemaRedefineAttributeGroupTests.swift @@ -88,6 +88,52 @@ struct SchemaRedefineAttributeGroupTests { ) } + @Test("widening a re-declared attribute's built-in type is rejected (schM4)") + func test_widenBuiltinTypeRejected() throws { + // int does not derive by restriction from boolean, so re-typing the attribute + // from boolean to int widens it: a redefinition must restrict, not widen. + #expect(throws: (any Error).self) { + try redefine( + group: "", + base: "", + ) + } + } + + @Test("narrowing a re-declared attribute's built-in type within the lattice is valid") + func test_narrowBuiltinTypeAccepted() throws { + // int derives from integer, so re-typing integer -> int is a valid restriction. + _ = try redefine( + group: "", + base: "", + ) + } + + @Test("re-typing an originally untyped attribute is valid (anySimpleType base)") + func test_retypeUntypedBaseAccepted() throws { + // An absent base type is anySimpleType, which any type validly restricts. + _ = try redefine( + group: "", + base: "", + ) + } + + @Test("a user type whose local name coincides with a built-in is not mistaken for it (no false positive)") + func test_userTypeNamedLikeBuiltinNotWidened() throws { + // The redefinition re-types `a` to `tns:int`, a USER simpleType that restricts + // xs:string. Its local name is the built-in keyword `int`, but it resolves to + // the target namespace, not the XSD namespace, so the type-widening check must + // not treat it as xsd:int (which would not derive from xsd:string) and reject. + let main = "" + + "" + + "" + + "" + + "" + let baseDoc = "" + + "" + _ = try PureXML.Schema.Document(main, schemaLoader: { _ in baseDoc }) + } + @Test("a non-self reference injecting a foreign attribute is rejected (attgC028)") func test_nonSelfReferenceAddRejected() throws { let main = "" diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 57dc060..71af5c9 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -146,9 +146,9 @@ struct XSTSSuiteTests { /// model-group occurrence, identity-constraint content, derivation base kind, attribute/simple-type, /// composition, simpleContent inline-type, complexContent mixedness, and substitution-group anySimpleType checks. /// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35. - /// UPA `##other` 35->31; UPA ext-model 31->29; ref-to-XSD-ns 29->27; unprefixed type-ref 27->25; redefine attrGroup non-self-ref add (attgC028) 25->24; valid-rejected 0. + /// UPA `##other` 31; ext-model 29; XSD-ns-ref 27; unprefixed-type-ref 25; redefine non-self-add attgC028 24; redefine type-widen schM4 23; valid-rejected 0. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 24 + private let knownSchemaInvalidAccepted = 23 private let knownInstanceValidRejected = 0 /// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the /// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in