fix(schema): compare simpleContent fixed value in value space

A fixed value on an element whose type is a complex type with simpleContent
was compared as a raw string, so value-space-equal content was wrongly
rejected (e.g. "05" against an xs:int fixed "5"). The complex-element fixed
check on both the tree path (validate(_:against:)) and the streaming path
passed no value type to elementFixedErrors, falling through to a string
compare; both now pass the simpleContent simple type as the value type, in
lockstep, so the comparison is in value space (cvc-elt.5.2.2.1) as the
simple-typed-element path already does.

XSTS gate held at 0/37/0/26 across all 14383 groups.
This commit is contained in:
Mihaela Mihaljevic 2026-06-20 17:47:08 +02:00
parent aeff7869f9
commit f6159191db
4 changed files with 52 additions and 2 deletions

View File

@ -28,6 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A `fixed` value on an element whose type is a complex type with `simpleContent` is
now compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw string
(#202), so `05` satisfies an `xs:int` `fixed="5"`. The complex-element fixed check on
both the tree and streaming paths now passes the `simpleContent` type as the value
type, in lockstep, as the simple-typed-element path already does.
- The streaming XSD validator now applies an element's `default`/`fixed` value
constraint to its simple content (#200): an empty element takes its constraint
value, and that value (not the empty string) is validated against the type

View File

@ -82,7 +82,10 @@ public extension PureXML.Schema {
return errors + nilErrors
}
validateContent(element, type.content, at: path, namespaceBindings: namespaceBindings, into: &errors)
errors += elementFixedErrors(element, at: path)
// A simpleContent fixed value is compared in its simple type's value space
// (cvc-elt.5.2.2.1), e.g. "05" equals an xs:int fixed "5".
let simpleContentType: SimpleType? = if case let .simpleContent(simple) = type.content { simple } else { nil }
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
return errors
}

View File

@ -168,7 +168,10 @@ public extension PureXML.Schema.ComplexValidator {
validateAttributes(element, complex, at: path, into: &errors)
if let nilErrors = nilErrors(element, at: path) { return errors + nilErrors }
shallowContent(element, complex.content, at: path, into: &errors)
errors += elementFixedErrors(element, at: path)
// A simpleContent fixed value is compared in its simple type's value space
// (cvc-elt.5.2.2.1), matching the tree path's validate(_:against:).
let simpleContentType: PureXML.Schema.SimpleType? = if case let .simpleContent(simple) = complex.content { simple } else { nil }
errors += elementFixedErrors(element, valueType: simpleContentType, at: path)
case .typeReference:
// The shared resolver also guards a circular chain, which would
// otherwise recurse here without terminating.

View File

@ -0,0 +1,38 @@
import Testing
@testable import PureXML
/// A `fixed` value on an element whose type is a complex type with `simpleContent`
/// must be compared in the simple type's value space (cvc-elt.5.2.2.1), not as a raw
/// string (#202). Both the tree and streaming paths must agree, so "05" satisfies an
/// xs:int `fixed="5"` and is not falsely rejected.
@Suite("XSD simpleContent fixed value-space (#202)")
struct SchemaSimpleContentFixedTests {
private let schema = """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
<xs:element name="r" type="t:R" fixed="5"/>
<xs:complexType name="R">
<xs:simpleContent>
<xs:extension base="xs:int"><xs:attribute name="u" type="xs:string"/></xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:schema>
"""
private func agreement(_ instance: String) throws -> [String] {
let document = try PureXML.Schema.Document(schema)
let tree = try document.validate(instance).map(\.reason).sorted()
let streamed = try document.validate(streaming: instance).map(\.reason).sorted()
#expect(tree == streamed, "streaming disagreed with tree:\n tree: \(tree)\n stream: \(streamed)")
return streamed
}
@Test("a value-space-equal fixed simpleContent value is accepted on both paths")
func test_valueSpaceEqualAccepted() throws {
#expect(try agreement("<t:r xmlns:t=\"urn:t\" u=\"x\">05</t:r>").isEmpty)
}
@Test("a value-space-different fixed simpleContent value is rejected on both paths")
func test_valueSpaceDifferentRejected() throws {
#expect(try !agreement("<t:r xmlns:t=\"urn:t\" u=\"x\">6</t:r>").isEmpty)
}
}