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.
135 lines
5.3 KiB
Swift
135 lines
5.3 KiB
Swift
extension PureXML.Schema {
|
|
/// A value compared for the ordering facets (`minInclusive` and friends).
|
|
enum OrderedValue: Equatable, Comparable {
|
|
case decimal(DecimalValue)
|
|
case double(Double)
|
|
case dateTime(DateTimeValue)
|
|
|
|
static func < (lhs: OrderedValue, rhs: OrderedValue) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.decimal(left), .decimal(right)): left < right
|
|
case let (.double(left), .double(right)): left < right
|
|
case let (.dateTime(left), .dateTime(right)): left < right
|
|
default: false
|
|
}
|
|
}
|
|
|
|
/// Value-space equality. A `double`/`float` value matches by IDENTITY, under which
|
|
/// `NaN` is identical to `NaN` (IEEE `==` reports `NaN != NaN`). This is what every
|
|
/// value-equality facet wants and what the reference (libxml2) and XSD 1.1 do: an
|
|
/// `enumeration` listing `NaN` admits `NaN`, two `NaN`-valued `key`/`unique` fields
|
|
/// collide as duplicates, and `fixed="NaN"` matches `NaN`. Ordering (`<`) keeps `NaN`
|
|
/// incomparable, so the range facets are unaffected.
|
|
static func == (lhs: OrderedValue, rhs: OrderedValue) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.decimal(left), .decimal(right)): left == right
|
|
case let (.double(left), .double(right)): left == right || (left.isNaN && right.isNaN)
|
|
case let (.dateTime(left), .dateTime(right)): left == right
|
|
default: false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An XSD primitive value space: the lexical recognizer, and for the ordered
|
|
/// spaces a value usable by the ordering facets.
|
|
enum Primitive: Sendable {
|
|
case string
|
|
case boolean
|
|
case decimal
|
|
case integer
|
|
case double
|
|
case float
|
|
case duration
|
|
case dateKind(DateKind)
|
|
case hexBinary
|
|
case base64Binary
|
|
case anyURI
|
|
case qName
|
|
case name
|
|
case ncName
|
|
case nmtoken
|
|
case language
|
|
case notation
|
|
|
|
func isValid(_ value: String) -> Bool {
|
|
stringValidity(value) ?? numericValidity(value)
|
|
}
|
|
|
|
/// Whether two valid values of this primitive compare by their
|
|
/// whitespace-processed lexical form rather than by a value space. These
|
|
/// are exactly the `default` cases of `SimpleType.atomicValueMatches`:
|
|
/// every primitive except the numeric, date, and boolean spaces, which
|
|
/// compare two distinct lexical forms (`3.0`/`3`, `true`/`1`) as equal.
|
|
var comparesLexically: Bool {
|
|
switch self {
|
|
case .decimal, .integer, .double, .float, .boolean, .dateKind: false
|
|
default: true
|
|
}
|
|
}
|
|
|
|
/// Validity for the string-like and boolean spaces; nil when this primitive
|
|
/// is handled by ``numericValidity(_:)``.
|
|
private func stringValidity(_ value: String) -> Bool? {
|
|
switch self {
|
|
case .string, .anyURI: true
|
|
case .boolean: Lexical.isBoolean(value)
|
|
case .qName, .notation: Lexical.isQName(value)
|
|
case .name: Lexical.isName(value)
|
|
case .ncName: Lexical.isNCName(value)
|
|
case .nmtoken: Lexical.isNMToken(value)
|
|
case .language: Lexical.isLanguage(value)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private func numericValidity(_ value: String) -> Bool {
|
|
switch self {
|
|
case .decimal: DecimalValue(value, allowFraction: true) != nil
|
|
case .integer: DecimalValue(value, allowFraction: false) != nil
|
|
case .double, .float: Lexical.isFloating(value)
|
|
case .duration: Lexical.isDuration(value)
|
|
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind) != nil
|
|
case .hexBinary: Lexical.isHexBinary(value)
|
|
case .base64Binary: Lexical.isBase64Binary(value)
|
|
default: false
|
|
}
|
|
}
|
|
|
|
/// The ordering value for a lexically valid string, or nil for the
|
|
/// unordered spaces.
|
|
func ordered(_ value: String) -> OrderedValue? {
|
|
switch self {
|
|
case .decimal: DecimalValue(value, allowFraction: true).map(OrderedValue.decimal)
|
|
case .integer: DecimalValue(value, allowFraction: false).map(OrderedValue.decimal)
|
|
case .double, .float: Self.floatingValue(value).map(OrderedValue.double)
|
|
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind).map(OrderedValue.dateTime)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private static func floatingValue(_ value: String) -> Double? {
|
|
switch value {
|
|
case "INF": .infinity
|
|
case "-INF": -.infinity
|
|
case "NaN": .nan
|
|
default: Double(value)
|
|
}
|
|
}
|
|
|
|
/// The unit `length`, `minLength`, and `maxLength` count: octets for binary
|
|
/// types, characters otherwise.
|
|
func measuredLength(_ value: String) -> Int {
|
|
switch self {
|
|
case .hexBinary: value.count / 2
|
|
case .base64Binary: Self.base64Octets(value)
|
|
default: value.count
|
|
}
|
|
}
|
|
|
|
private static func base64Octets(_ value: String) -> Int {
|
|
let padding = value.reversed().prefix { $0 == "=" }.count
|
|
return value.count / 4 * 3 - padding
|
|
}
|
|
}
|
|
}
|