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.
81 lines
4.0 KiB
Swift
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
|
|
}
|
|
}
|