PureXML/Tests/SchemaDoubleEnumerationTests.swift
Mihaela Mihaljevic 7be7fa25f3
fix(schema): match a NaN value against a NaN enumeration (value-space identity) (#376)
OrderedValue.== compared doubles with IEEE equality (NaN != NaN), so a NaN value never matched a NaN enumeration member and was rejected: <v>NaN</v> against xs:double restricted by <xs:enumeration value="NaN"/> is valid, and PureXML rejected it (a cardinal-rule false positive). NaN is now treated as identical to itself in value-space equality, the identity the reference (libxml2) and XSD 1.1 use throughout; an adversarial critic flagged the shared comparison (also used by key/unique and fixed), so the key/fixed behaviour was verified against xmllint: libxml2 collides two NaN unique keys and matches fixed="NaN", i.e. the same identity, so one identity == matches the reference everywhere. Ordering (<) keeps NaN incomparable, so range facets are unaffected. Found by a new generated-value facet differential (FacetValidationDifferentialTests, opt-in via FACET_DIFF) over ~10000 datatype/facet values: one FP found, zero after; the four W3C XSTS buckets held, full suite passes, SchemaDoubleEnumerationTests locks all three relations.
2026-06-29 05:20:07 +02:00

81 lines
4.0 KiB
Swift

import Testing
@testable import PureXML
/// A `double`/`float` value matches by IDENTITY in every value-equality context, under
/// which `NaN` is identical to `NaN` even though IEEE `==` reports `NaN != NaN`. The prior
/// comparison used IEEE equality and so dropped `NaN`, a false positive the libxml2 facet
/// differential caught. The identity reading is what the reference (libxml2) and XSD 1.1
/// do across the board, verified against `xmllint`: an `enumeration` listing `NaN` admits
/// `NaN`; two `NaN`-valued `unique` fields collide as duplicates; and `fixed="NaN"` matches
/// `NaN`. (A strict XSD-1.0 `·equal·` reading would instead leave `NaN` unequal to itself
/// for `fixed`/`key`; an adversarial critic flagged that path, but libxml2 rejects two
/// `NaN` keys and accepts `fixed="NaN"`, so a single identity `==` matches it.) Ordering
/// (`<`) keeps `NaN` incomparable, so the range facets are unaffected.
@Suite("Double NaN value-space identity")
struct SchemaDoubleEnumerationTests {
private func schema(_ base: String, _ facets: String) throws -> PureXML.Schema.Document {
try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="v"><xs:simpleType>
<xs:restriction base="\(base)">\(facets)</xs:restriction>
</xs:simpleType></xs:element>
</xs:schema>
""")
}
@Test("NaN is admitted by an enumeration that lists NaN")
func test_nanInEnumeration() throws {
let doc = try schema("xs:double", #"<xs:enumeration value="NaN"/><xs:enumeration value="3.14"/>"#)
#expect(try doc.validate("<v>NaN</v>").isEmpty) // NaN is identical to NaN in the value space
#expect(try doc.validate("<v>3.14</v>").isEmpty)
#expect(try !doc.validate("<v>2.0</v>").isEmpty) // a value not enumerated is still rejected
}
@Test("NaN is rejected by an enumeration that does not list it")
func test_nanNotInEnumeration() throws {
let doc = try schema("xs:double", #"<xs:enumeration value="3.14"/>"#)
#expect(try !doc.validate("<v>NaN</v>").isEmpty)
}
@Test("INF and -INF match their enumerations")
func test_infinityEnumeration() throws {
let doc = try schema("xs:double", #"<xs:enumeration value="INF"/><xs:enumeration value="-INF"/>"#)
#expect(try doc.validate("<v>INF</v>").isEmpty)
#expect(try doc.validate("<v>-INF</v>").isEmpty)
#expect(try !doc.validate("<v>5.0</v>").isEmpty)
}
@Test("an xs:float NaN enumeration matches too")
func test_floatNaN() throws {
let doc = try schema("xs:float", #"<xs:enumeration value="NaN"/>"#)
#expect(try doc.validate("<v>NaN</v>").isEmpty)
}
@Test("two NaN unique keys collide as duplicates (identity, matching libxml2)")
func test_nanUniqueKeysCollide() throws {
let doc = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root"><xs:complexType><xs:sequence>
<xs:element name="item" maxOccurs="unbounded"><xs:complexType>
<xs:attribute name="v" type="xs:double"/></xs:complexType></xs:element>
</xs:sequence></xs:complexType>
<xs:unique name="u"><xs:selector xpath="item"/><xs:field xpath="@v"/></xs:unique>
</xs:element>
</xs:schema>
""")
// libxml2 reports "Duplicate key-sequence ['nan']"; NaN is identical to NaN here.
#expect(try !doc.validate("<root><item v='NaN'/><item v='NaN'/></root>").isEmpty)
}
@Test("a NaN fixed value matches a NaN instance (identity, matching libxml2)")
func test_nanFixedMatches() throws {
let doc = try PureXML.Schema.Document("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="v" type="xs:double" fixed="NaN"/>
</xs:schema>
""")
#expect(try doc.validate("<v>NaN</v>").isEmpty) // fixed="NaN" matches NaN
#expect(try !doc.validate("<v>3.0</v>").isEmpty) // a non-NaN value does not
}
}