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.
This commit is contained in:
parent
81805ad53e
commit
b6e5cd8f3f
@ -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 `<element ref="root"/>` 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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: "<xs:attribute name='a' type='xs:int'/>",
|
||||
base: "<xs:attribute name='a' type='xs:boolean'/>",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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: "<xs:attribute name='a' type='xs:int'/>",
|
||||
base: "<xs:attribute name='a' type='xs:integer'/>",
|
||||
)
|
||||
}
|
||||
|
||||
@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: "<xs:attribute name='a' type='xs:int'/>",
|
||||
base: "<xs:attribute name='a'/>",
|
||||
)
|
||||
}
|
||||
|
||||
@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 = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:tns='urn:t' targetNamespace='urn:t'>"
|
||||
+ "<xs:redefine schemaLocation='b.xsd'><xs:attributeGroup name='g'>"
|
||||
+ "<xs:attribute name='a' type='tns:int'/></xs:attributeGroup></xs:redefine>"
|
||||
+ "<xs:simpleType name='int'><xs:restriction base='xs:string'/></xs:simpleType>"
|
||||
+ "<xs:element name='e'/></xs:schema>"
|
||||
let baseDoc = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema' targetNamespace='urn:t'>"
|
||||
+ "<xs:attributeGroup name='g'><xs:attribute name='a' type='xs:string'/></xs:attributeGroup></xs:schema>"
|
||||
_ = 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 = "<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>"
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user