Consolidates the in-progress #146 work; valid-instances-rejected reaches 4 on the 2006-11-06 XSTS archive (buckets now 1/262/4/147). Regex: match Unicode scalars rather than grapheme clusters; the empty pattern matches only the empty string; pin the category repertoire to Unicode 3.2, the version the corpus is consistent at, so code points added in 3.2 (Tagalog, Cyrillic Komi) are word characters while later additions (the Tamil digit zero) stay unassigned and match \D. New XSDCategory and a 3.2 assignment table derived from DerivedAge, replacing the old Unicode31 files. Content model: assess each child against the particle the content automaton matched, not a by-name lookup (#180). Two same-named particles keep their own fixed values, and an element matching a skip wildcard is no longer validated against its global declaration. Selection is made reproducible when an ambiguous schema slips past the UPA check. XSTS runner: compile and merge every schemaDocument in a schemaTest, not just the first; this also surfaced one previously-masked invalid-instance acceptance (recorded in the baselines). Also folds in attribute-group anyAttribute inheritance, extension wildcard union, namespace-qualified type tables for multi-document xsi:schemaLocation, per-container elementFormDefault, strict and lax attribute wildcard validation, built-in type resolution, and xs:redefine simpleType overrides.
348 lines
17 KiB
Swift
348 lines
17 KiB
Swift
public extension PureXML.Schema {
|
|
/// The constraining facets of a simple type. Every facet is optional; an unset
|
|
/// facet imposes no constraint. Multiple patterns on one restriction step are
|
|
/// OR'd (libxml2/xmlschemas.c); each group is AND'd across derivation steps.
|
|
struct Facets: Sendable {
|
|
public var length: Int?
|
|
public var minLength: Int?
|
|
public var maxLength: Int?
|
|
public var patternGroups: [[String]]
|
|
public var enumeration: [String]?
|
|
public var whiteSpace: WhiteSpace?
|
|
public var minInclusive: String?
|
|
public var maxInclusive: String?
|
|
public var minExclusive: String?
|
|
public var maxExclusive: String?
|
|
public var totalDigits: Int?
|
|
public var fractionDigits: Int?
|
|
|
|
public init(
|
|
length: Int? = nil,
|
|
minLength: Int? = nil,
|
|
maxLength: Int? = nil,
|
|
patternGroups: [[String]] = [],
|
|
enumeration: [String]? = nil,
|
|
whiteSpace: WhiteSpace? = nil,
|
|
minInclusive: String? = nil,
|
|
maxInclusive: String? = nil,
|
|
minExclusive: String? = nil,
|
|
maxExclusive: String? = nil,
|
|
totalDigits: Int? = nil,
|
|
fractionDigits: Int? = nil,
|
|
) {
|
|
self.length = length
|
|
self.minLength = minLength
|
|
self.maxLength = maxLength
|
|
self.patternGroups = patternGroups
|
|
self.enumeration = enumeration
|
|
self.whiteSpace = whiteSpace
|
|
self.minInclusive = minInclusive
|
|
self.maxInclusive = maxInclusive
|
|
self.minExclusive = minExclusive
|
|
self.maxExclusive = maxExclusive
|
|
self.totalDigits = totalDigits
|
|
self.fractionDigits = fractionDigits
|
|
}
|
|
|
|
/// Whether no facet narrows the base type's value space: the type is the
|
|
/// bare built-in. Used to decide whether an `xsi:type` built-in may validly
|
|
/// substitute for a declared type (a faceted restriction may not be the
|
|
/// target of a built-in substitution).
|
|
public var isUnconstrained: Bool {
|
|
length == nil && minLength == nil && maxLength == nil && patternGroups.isEmpty
|
|
&& enumeration == nil && whiteSpace == nil
|
|
&& minInclusive == nil && maxInclusive == nil
|
|
&& minExclusive == nil && maxExclusive == nil
|
|
&& totalDigits == nil && fractionDigits == nil
|
|
}
|
|
}
|
|
|
|
/// A simple type's variety (XSD Part 2): atomic, a whitespace-separated list
|
|
/// of an item type, or a union of member types.
|
|
indirect enum Variety: Sendable {
|
|
case atomic
|
|
case list(SimpleType)
|
|
case union([SimpleType])
|
|
}
|
|
|
|
/// A simple type: a built-in base restricted by facets, of one of the three
|
|
/// varieties. Validates a lexical value against the XSD Part 2 rules:
|
|
/// whitespace processing, the base value space, the base's intrinsic bounds,
|
|
/// then the facets. A list checks each item against the item type and counts
|
|
/// items for the length facets; a union admits a value valid for any member.
|
|
struct SimpleType: Sendable {
|
|
public var base: BuiltinType
|
|
public var facets: Facets
|
|
public var variety: Variety
|
|
public var isAnySimpleType: Bool
|
|
public var isBuiltinList: Bool
|
|
|
|
public init(
|
|
base: BuiltinType,
|
|
facets: Facets = Facets(),
|
|
variety: Variety = .atomic,
|
|
isAnySimpleType: Bool = false,
|
|
isBuiltinList: Bool = false,
|
|
) {
|
|
self.base = base
|
|
self.facets = facets
|
|
self.variety = variety
|
|
self.isAnySimpleType = isAnySimpleType
|
|
self.isBuiltinList = isBuiltinList
|
|
}
|
|
|
|
/// A list type whose items are `item`, carrying its own (list) facets.
|
|
public static func list(item: SimpleType, facets: Facets = Facets(), isBuiltinList: Bool = false) -> SimpleType {
|
|
SimpleType(base: .string, facets: facets, variety: .list(item), isBuiltinList: isBuiltinList)
|
|
}
|
|
|
|
/// A union of `members`: a value is valid if any member admits it.
|
|
public static func union(_ members: [SimpleType]) -> SimpleType {
|
|
SimpleType(base: .string, variety: .union(members))
|
|
}
|
|
|
|
/// Whether `lexical` is a valid value of this type.
|
|
public func isValid(_ lexical: String) -> Bool {
|
|
validate(lexical) == nil
|
|
}
|
|
|
|
/// A description of the first constraint `lexical` violates, or nil when it
|
|
/// is valid.
|
|
public func validate(_ lexical: String) -> String? {
|
|
switch variety {
|
|
case .atomic: validateAtomic(lexical)
|
|
case let .list(item): validateList(lexical, item: item)
|
|
case let .union(members): validateUnion(lexical, members: members)
|
|
}
|
|
}
|
|
|
|
private func validateList(_ lexical: String, item: SimpleType) -> String? {
|
|
let normalized = Self.process(lexical, whiteSpace: facets.whiteSpace ?? .collapse)
|
|
let tokens = normalized.isEmpty ? [] : normalized.split(separator: " ").map(String.init)
|
|
for token in tokens {
|
|
if let error = item.validate(token) { return error }
|
|
}
|
|
if let exact = facets.length, tokens.count != exact { return "list length \(tokens.count) is not \(exact)" }
|
|
if let minimum = facets.minLength, tokens.count < minimum { return "list length \(tokens.count) is below \(minimum)" }
|
|
if let maximum = facets.maxLength, tokens.count > maximum { return "list length \(tokens.count) is above \(maximum)" }
|
|
if facets.minLength == nil, facets.length == nil {
|
|
if isBuiltinList, tokens.isEmpty {
|
|
return "list must not be empty"
|
|
}
|
|
}
|
|
if let error = patternError(normalized) { return error }
|
|
if let enumeration = facets.enumeration, !enumeration.contains(normalized) {
|
|
return "'\(normalized)' is not in the enumeration"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Whether `instance` and `literal` denote the same value in this type's
|
|
/// value space, the comparison a RELAX NG `<value>` performs. Ordered
|
|
/// types (numeric, date/time) compare in value space, so `1` equals `01`
|
|
/// and `1.0` equals `1.00`; booleans treat `1`/`true` and `0`/`false` as
|
|
/// equal. Every other type, including `string`/`token`, compares by its
|
|
/// whitespace-normalized lexical form. (Durations and binary types fall
|
|
/// under the lexical rule, so only their normalized forms compare equal.)
|
|
public func valueMatches(_ instance: String, literal: String) -> Bool {
|
|
switch variety {
|
|
case .atomic:
|
|
return atomicValueMatches(instance, literal)
|
|
case let .list(item):
|
|
let whitespaceProcessing = facets.whiteSpace ?? .collapse
|
|
let leftNorm = Self.process(instance, whiteSpace: whitespaceProcessing)
|
|
let rightNorm = Self.process(literal, whiteSpace: whitespaceProcessing)
|
|
let leftTokens = leftNorm.isEmpty ? [] : leftNorm.split(separator: " ").map(String.init)
|
|
let rightTokens = rightNorm.isEmpty ? [] : rightNorm.split(separator: " ").map(String.init)
|
|
guard leftTokens.count == rightTokens.count else { return false }
|
|
return zip(leftTokens, rightTokens).allSatisfy { item.valueMatches($0, literal: $1) }
|
|
case let .union(members):
|
|
guard let leftMember = members.first(where: { $0.isValid(instance) }),
|
|
let rightMember = members.first(where: { $0.isValid(literal) }),
|
|
leftMember.base == rightMember.base
|
|
else { return false }
|
|
return leftMember.valueMatches(instance, literal: literal)
|
|
}
|
|
}
|
|
|
|
private func atomicValueMatches(_ instance: String, _ literal: String) -> Bool {
|
|
let whiteSpace = facets.whiteSpace ?? base.whiteSpace
|
|
let left = Self.process(instance, whiteSpace: whiteSpace)
|
|
let right = Self.process(literal, whiteSpace: whiteSpace)
|
|
switch base.primitive {
|
|
case .decimal, .integer, .double, .float, .dateKind:
|
|
guard let leftValue = base.primitive.ordered(left), let rightValue = base.primitive.ordered(right) else { return false }
|
|
return leftValue == rightValue
|
|
case .boolean:
|
|
guard let leftValue = Self.booleanValue(left) else { return false }
|
|
return leftValue == Self.booleanValue(right)
|
|
default:
|
|
return left == right
|
|
}
|
|
}
|
|
|
|
private static func booleanValue(_ value: String) -> Bool? {
|
|
switch value {
|
|
case "true", "1": true
|
|
case "false", "0": false
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private func validateUnion(_ lexical: String, members: [SimpleType]) -> String? {
|
|
// The value must be valid against at least one member type.
|
|
guard members.contains(where: { $0.validate(lexical) == nil }) else {
|
|
return "'\(lexical)' does not match any member type of the union"
|
|
}
|
|
// `pattern` and `enumeration` are the only constraining facets XSD
|
|
// allows on a union (Datatypes 4.1.5); both apply on top of membership.
|
|
// Pattern matches the lexical value; enumeration compares in the
|
|
// value space of whichever member admits each value, so `01` equals an
|
|
// enumerated `1` for an integer member, matching the atomic path.
|
|
if let error = patternError(lexical) { return error }
|
|
if let enumeration = facets.enumeration {
|
|
let inEnumeration = enumeration.contains { candidate in
|
|
members.contains { $0.valueMatches(lexical, literal: candidate) }
|
|
}
|
|
if !inEnumeration { return "'\(lexical)' is not in the enumeration" }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func validateAtomic(_ lexical: String) -> String? {
|
|
let value = Self.process(lexical, whiteSpace: facets.whiteSpace ?? base.whiteSpace)
|
|
let primitive = base.primitive
|
|
guard primitive.isValid(value) else {
|
|
return "'\(lexical)' is not a valid \(base.rawValue)"
|
|
}
|
|
return boundsError(value, primitive)
|
|
?? lengthError(value, primitive)
|
|
?? digitsError(value)
|
|
?? patternError(value)
|
|
?? enumerationError(value, primitive)
|
|
?? rangeError(value, primitive)
|
|
}
|
|
|
|
// MARK: Whitespace
|
|
|
|
static func process(_ value: String, whiteSpace: WhiteSpace) -> String {
|
|
switch whiteSpace {
|
|
case .preserve:
|
|
return value
|
|
case .replace:
|
|
return String(value.map { ($0 == "\t" || $0 == "\n" || $0 == "\r") ? " " : $0 })
|
|
case .collapse:
|
|
let replaced = value.map { ($0 == "\t" || $0 == "\n" || $0 == "\r") ? " " : $0 }
|
|
return String(replaced).split(separator: " ").joined(separator: " ")
|
|
}
|
|
}
|
|
|
|
// MARK: Facet checks
|
|
|
|
private func boundsError(_ value: String, _: Primitive) -> String? {
|
|
let (lower, upper) = base.bounds
|
|
guard lower != nil || upper != nil, let decimal = DecimalValue(value, allowFraction: false) else {
|
|
return nil
|
|
}
|
|
if let lower, decimal < lower { return "\(value) is below the minimum for \(base.rawValue)" }
|
|
if let upper, decimal > upper { return "\(value) is above the maximum for \(base.rawValue)" }
|
|
return nil
|
|
}
|
|
|
|
private func lengthError(_ value: String, _ primitive: Primitive) -> String? {
|
|
// length/minLength/maxLength measure characters, octets, or list
|
|
// items depending on the type; for QName the spec leaves the unit
|
|
// unspecified (XSD 1.0 Datatypes 4.3.1), so like Xerces and the XSTS
|
|
// NIST oracle we do not constrain QName values by length.
|
|
if case .qName = primitive { return nil }
|
|
if case .notation = primitive { return nil }
|
|
let length = primitive.measuredLength(value)
|
|
if let exact = facets.length, length != exact { return "length \(length) is not \(exact)" }
|
|
if let minimum = facets.minLength, length < minimum { return "length \(length) is below \(minimum)" }
|
|
if let maximum = facets.maxLength, length > maximum { return "length \(length) is above \(maximum)" }
|
|
return nil
|
|
}
|
|
|
|
private func digitsError(_ value: String) -> String? {
|
|
guard facets.totalDigits != nil || facets.fractionDigits != nil,
|
|
let decimal = DecimalValue(value, allowFraction: true) else { return nil }
|
|
if let total = facets.totalDigits, decimal.totalDigits > total { return "more than \(total) total digits" }
|
|
if let fraction = facets.fractionDigits, decimal.fractionDigits.count > fraction {
|
|
return "more than \(fraction) fraction digits"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func patternError(_ value: String) -> String? {
|
|
for group in facets.patternGroups {
|
|
let matched = group.contains { pattern in
|
|
guard let regex = try? PureXML.Regex.Pattern(pattern) else { return false }
|
|
return regex.matches(value)
|
|
}
|
|
guard matched else {
|
|
let label = group.joined(separator: "|")
|
|
return "'\(value)' does not match pattern '\(label)'"
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func enumerationError(_ value: String, _ primitive: Primitive) -> String? {
|
|
guard let enumeration = facets.enumeration else { return nil }
|
|
let whiteSpace = facets.whiteSpace ?? base.whiteSpace
|
|
let matched = enumeration.contains { candidate in
|
|
let normalized = Self.process(candidate, whiteSpace: whiteSpace)
|
|
if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) { return lhs == rhs }
|
|
return normalized == value
|
|
}
|
|
return matched ? nil : "'\(value)' is not in the enumeration"
|
|
}
|
|
|
|
private func rangeError(_ value: String, _ primitive: Primitive) -> String? {
|
|
if case .duration = primitive { return durationRangeError(value) }
|
|
guard let actual = primitive.ordered(value) else { return nil }
|
|
if let bound = facets.minInclusive, let limit = primitive.ordered(bound), actual < limit {
|
|
return "\(value) is below the inclusive minimum"
|
|
}
|
|
if let bound = facets.maxInclusive, let limit = primitive.ordered(bound), actual > limit {
|
|
return "\(value) is above the inclusive maximum"
|
|
}
|
|
if let bound = facets.minExclusive, let limit = primitive.ordered(bound), !(limit < actual) {
|
|
return "\(value) is not above the exclusive minimum"
|
|
}
|
|
if let bound = facets.maxExclusive, let limit = primitive.ordered(bound), !(actual < limit) {
|
|
return "\(value) is not below the exclusive maximum"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Ordering facets on `xs:duration`, which is a partial order: a value
|
|
/// that is incomparable to the bound does not satisfy it (it is not in
|
|
/// the ordered range), so an incomparable result is a violation.
|
|
private func durationRangeError(_ value: String) -> String? {
|
|
guard let actual = DurationValue(value) else { return nil }
|
|
func order(_ bound: String?) -> DurationOrder? {
|
|
bound.flatMap(DurationValue.init).map(actual.compare(to:))
|
|
}
|
|
if let order = order(facets.minInclusive), order == .lessThan || order == .incomparable {
|
|
return "\(value) is below the inclusive minimum"
|
|
}
|
|
if let order = order(facets.maxInclusive), order == .greaterThan || order == .incomparable {
|
|
return "\(value) is above the inclusive maximum"
|
|
}
|
|
if let order = order(facets.minExclusive), order != .greaterThan {
|
|
return "\(value) is not above the exclusive minimum"
|
|
}
|
|
if let order = order(facets.maxExclusive), order != .lessThan {
|
|
return "\(value) is not below the exclusive maximum"
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Validates `value` against a built-in type with no extra facets.
|
|
static func isValid(_ value: String, type: BuiltinType) -> Bool {
|
|
SimpleType(base: type).isValid(value)
|
|
}
|
|
}
|