diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf04f5..02e7cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- RELAX NG namespaces and semantics, second burn-down (#131): the compiler now implements `ns` inheritance and the QName form of names (4.9-4.10: `ns` walks ancestors and crosses include/externalRef document boundaries; `name="eg:foo"` and `eg:foo` resolve their prefix against the schema's in-scope xmlns declarations; the `name=` shorthand on an attribute stays namespace-less rather than inheriting), an `include` brings in the included grammar's `start` with an include-level override honored (4.7), multiple `` elements combine like defines and a single define or start may omit `combine` while still merging by the named method (4.17), `` supports `` via a new `dataExcept` pattern in the derivative engine (4.12), an empty element matches a data/value pattern whose lexical space admits the empty string (Clark's empty-text rule), and a whitespace-only attribute value matches a pattern that matches empty. Spec-suite baselines tighten from valid 242/289 to 274/289 and invalid 278/291 to 279/291; the name-class and tree-helper sections split to their own files for the length caps. - RELAX NG simplification, first burn-down (#131): foreign-namespace elements are stripped before pattern interpretation (4.1: only RELAX NG-namespace elements are schema content; a namespace-less element counts only inside a fully unqualified schema), name/type/combine attribute values are whitespace-trimmed (4.2), and include/externalRef hrefs resolve against the xml:base attributes in scope and the loaded document's own URI (4.5, reusing the RFC 3986 resolver), with the document base threaded through nested loads and visited-tracking keyed on resolved hrefs. Spec-suite baselines tighten: valid 242/289 accepted (from 233), invalid 278/291 rejected. - RELAX NG spec test suite runner (#122, #131): James Clark's spectest.xml (385 cases, distributed with jing-trang) runs via an opt-in RNG_TS_ROOT runner, never vendored; the manifest is parsed with PureXML itself, nested testSuite scopes thread their resource/dir definitions into the compiler's loader, and each case asserts compile/reject for the schema plus accept/reject per instance. First measurement, carried as exact baselines: every correct schema compiles and most instances behave (valid 233/289 accepted, invalid 277/291 rejected), while schema-correctness validation is absent (all 213 incorrect schemas compile, count asserted exactly). The burn-down classes are recorded in the runner: schema validation against the RELAX NG grammar and the 4.16-4.18 restrictions, foreign-namespace stripping (4.1), xml:base-aware include/externalRef resolution (4.5), and the section 6 compatibility datatypes. - Canonical XML 1.0 spec-example vectors (#122, #132): the worked examples of the C14N 1.0 Recommendation's section 3 run as input/expected-output tests (cited per example; used under the W3C Software and Document License), all six passing: top-level PIs/comments with and without comments mode, whitespace preservation, start/end tags with attribute sorting and DTD defaults, character modifications and references, entity expansion, and UTF-8 output from legacy-encoded input. The vectors exposed three real conformance fixes: top-level siblings in the canonical form are now separated by single line feeds; an empty default-namespace declaration renders only when it undeclares a rendered non-empty default (superfluous `xmlns=""` is dropped, inclusive mode); and the text/attribute escape loops iterate Unicode scalars, so a carriage return that grapheme-clusters with a following line feed still escapes to ` ` (the #135 class, surfacing in output rather than lexing). diff --git a/Sources/Schema/RelaxNGDerivative.swift b/Sources/Schema/RelaxNGDerivative.swift index 1d85559..f8f2d20 100644 --- a/Sources/Schema/RelaxNGDerivative.swift +++ b/Sources/Schema/RelaxNGDerivative.swift @@ -92,6 +92,8 @@ extension PureXML.Schema { case let .value(type, literal): type.valueMatches(string, literal: literal) ? .empty : .notAllowed case let .data(type): type.validate(string) == nil ? .empty : .notAllowed + case let .dataExcept(type, except): + type.validate(string) == nil && !nullable(valueDeriv(except, string)) ? .empty : .notAllowed case let .list(inner): listMatches(inner, string) ? .empty : .notAllowed default: .notAllowed } @@ -124,7 +126,11 @@ extension PureXML.Schema { } private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String) -> Bool { - value.isEmpty ? nullable(content) : nullable(textDeriv(content, value)) + if nullable(textDeriv(content, value)) { return true } + // A whitespace-only attribute value also matches a pattern that + // matches the empty sequence, the same leniency element content + // gets for ignorable whitespace. + return value.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isWhitespace) && nullable(content) } private func oneOrMoreP(_ inner: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { @@ -206,7 +212,9 @@ extension PureXML.Schema.RelaxNGEngine { private func childrenDeriv(_ pattern: PureXML.Schema.Pattern, _ children: [PureXML.Model.Node]) -> PureXML.Schema.Pattern { let coalesced = Self.coalesceText(children) - if coalesced.isEmpty { return pattern } + // Clark's algorithm treats an empty element as optional empty text, so + // a data/value pattern whose lexical space admits "" matches . + if coalesced.isEmpty { return choice(pattern, textDeriv(pattern, "")) } if coalesced.count == 1, case let .text(string) = coalesced[0] { let derived = textDeriv(pattern, string) return Self.isWhitespace(string) ? choice(pattern, derived) : derived diff --git a/Sources/Schema/RelaxNGNameClasses.swift b/Sources/Schema/RelaxNGNameClasses.swift new file mode 100644 index 0000000..bd5c65e --- /dev/null +++ b/Sources/Schema/RelaxNGNameClasses.swift @@ -0,0 +1,92 @@ +/// The name-class side of the RELAX NG compiler (4.9-4.10: ns inheritance and +/// QName resolution), split from the compiler body to keep it under the +/// length caps. +extension RNGCompiler { + typealias Tree = RNGTree + + func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) { + let children = RNGNode.elementChildren(node) + if let name = RNGNode.attribute(node, "name") { + if let qualified = RNGNode.resolveQName(name, at: node) { + return (.name(namespace: qualified.namespace, localName: qualified.localName), children) + } + // The `name=` shorthand on an attribute does not inherit ns: an + // unprefixed attribute name has no namespace unless the attribute + // element itself carries ns (4.9). Elements inherit normally. + let isAttribute = RNGNode.localName(node) == "attribute" + let namespace = isAttribute + ? (node.attributes.first { $0.name.prefix == nil && $0.name.localName == "ns" }?.value ?? "") + : effectiveNS(node) + return (.name(namespace: namespace, localName: name), children) + } + guard let first = children.first else { return (.anyName, []) } + return (nameClass(first), Array(children.dropFirst())) + } + + // MARK: Combinators and name classes + + func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern { + let patterns = nodes.map(pattern) + let identity: Pattern = compositor == .choice ? .notAllowed : .empty + guard let first = patterns.first else { return identity } + return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) } + } + + func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern { + switch compositor { + case .sequence: .group(lhs, rhs) + case .choice: .choice(lhs, rhs) + case .all: .interleave(lhs, rhs) + } + } + + func nameClass(_ node: Tree) -> NameClass { + switch RNGNode.localName(node) { + case "name": + nameElementClass(node) + case "anyName": + anyName(node) + case "nsName": + nsName(node) + case "choice": + nameClassChoice(RNGNode.elementChildren(node)) + default: + .anyName + } + } + + func anyName(_ node: Tree) -> NameClass { + guard let except = exceptClass(node) else { return .anyName } + return .anyNameExcept(except) + } + + /// A `` element: its text may be a QName resolved against in-scope + /// xmlns declarations (4.10); otherwise the inherited ns applies (4.9). + func nameElementClass(_ node: Tree) -> NameClass { + let raw = RNGNode.text(node) + if let qualified = RNGNode.resolveQName(raw, at: node) { + return .name(namespace: qualified.namespace, localName: qualified.localName) + } + return .name(namespace: effectiveNS(node), localName: raw) + } + + func nsName(_ node: Tree) -> NameClass { + let namespace = effectiveNS(node) + guard let except = exceptClass(node) else { return .nsName(namespace) } + return .nsNameExcept(namespace: namespace, except: except) + } + + /// The name class of a ``'s `` child, treating multiple + /// children as an implicit choice (`anyName`/`nsName` minus the union). + func exceptClass(_ node: Tree) -> NameClass? { + guard let except = RNGNode.children(node, named: "except").first else { return nil } + let children = RNGNode.elementChildren(except) + return children.isEmpty ? nil : nameClassChoice(children) + } + + func nameClassChoice(_ nodes: [Tree]) -> NameClass { + let classes = nodes.map(nameClass) + guard let first = classes.first else { return .anyName } + return classes.dropFirst().reduce(first) { .choice($0, $1) } + } +} diff --git a/Sources/Schema/RelaxNGNode.swift b/Sources/Schema/RelaxNGNode.swift new file mode 100644 index 0000000..6419663 --- /dev/null +++ b/Sources/Schema/RelaxNGNode.swift @@ -0,0 +1,109 @@ +typealias RNGTree = PureXML.Model.TreeNode + +/// RNGTree helpers for the RELAX NG parser. File-scope and private. +enum RNGNode { + static func localName(_ node: RNGTree) -> String? { + node.name?.localName + } + + static func attribute(_ node: RNGTree, _ name: String) -> String? { + let raw = node.attributes.first { $0.name.localName == name }?.value + // Simplification 4.2: the value of name, type, and combine attributes + // is whitespace-trimmed. + guard let raw, ["name", "type", "combine"].contains(name) else { return raw } + return raw.trimmingXMLWhitespace() + } + + /// The RELAX NG namespace; simplification 4.1 removes foreign-namespace + /// elements (annotations such as documentation or comments) before any + /// pattern interpretation. + static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0" + + static func elementChildren(_ node: RNGTree) -> [RNGTree] { + node.children.filter { child in + guard child.kind == .element else { return false } + // Simplification 4.1: only RELAX NG-namespace elements are schema + // content. An element with no namespace counts as schema content + // only when the whole document is unqualified (leniency for + // namespace-less schemas); under a qualified parent it is foreign. + let uri = child.name?.namespaceURI + if uri == relaxNGNamespace { return true } + return uri == nil && node.name?.namespaceURI == nil + } + } + + static func children(_ node: RNGTree, named name: String) -> [RNGTree] { + elementChildren(node).filter { localName($0) == name } + } + + static func text(_ node: RNGTree) -> String { + node.stringValue.trimmingXMLWhitespace() + } + + static func strip(_ qualified: String) -> String { + qualified.split(separator: ":").last.map(String.init) ?? qualified + } + + /// The nearest `ns` attribute on the node or an ancestor (simplification + /// 4.9 inheritance), or nil when none is in scope in this document. + static func inheritedNS(_ node: RNGTree) -> String? { + var current: RNGTree? = node + while let candidate = current { + if let namespace = candidate.attributes.first(where: { $0.name.prefix == nil && $0.name.localName == "ns" })?.value { + return namespace + } + current = candidate.parent + } + return nil + } + + /// Resolves a prefixed name against the xmlns declarations in scope at the + /// schema node (simplification 4.10), or nil for an unprefixed name. + static func resolveQName(_ raw: String, at node: RNGTree) -> (namespace: String, localName: String)? { + guard let colon = raw.firstIndex(of: ":") else { return nil } + let prefix = String(raw[.. String { + guard let base, !base.isEmpty else { return reference } + let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base) + let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":") + if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") { + return String(merged.dropFirst()) + } + return merged + } + + /// The `href` of an include/externalRef, resolved against the document's + /// base and the `xml:base` attributes in scope at the node (4.5). + static func resolvedHref(_ node: RNGTree, documentBase: String?) -> String? { + guard let href = attribute(node, "href") else { return nil } + var bases: [String] = [] + var cursor: RNGTree? = node + while let current = cursor { + if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value { + bases.append(base) + } + cursor = current.parent + } + if let documentBase { bases.append(documentBase) } + var resolved = href + for base in bases { + resolved = resolveRelative(resolved, against: base) + } + return resolved + } +} diff --git a/Sources/Schema/RelaxNGParser.swift b/Sources/Schema/RelaxNGParser.swift index fc9ccf6..a81bc52 100644 --- a/Sources/Schema/RelaxNGParser.swift +++ b/Sources/Schema/RelaxNGParser.swift @@ -1,97 +1,36 @@ -private typealias Tree = PureXML.Model.TreeNode - -/// Tree helpers for the RELAX NG parser. File-scope and private. -private enum RNGNode { - static func localName(_ node: Tree) -> String? { - node.name?.localName - } - - static func attribute(_ node: Tree, _ name: String) -> String? { - let raw = node.attributes.first { $0.name.localName == name }?.value - // Simplification 4.2: the value of name, type, and combine attributes - // is whitespace-trimmed. - guard let raw, ["name", "type", "combine"].contains(name) else { return raw } - return raw.trimmingXMLWhitespace() - } - - /// The RELAX NG namespace; simplification 4.1 removes foreign-namespace - /// elements (annotations such as documentation or comments) before any - /// pattern interpretation. - static let relaxNGNamespace = "http://relaxng.org/ns/structure/1.0" - - static func elementChildren(_ node: Tree) -> [Tree] { - node.children.filter { child in - guard child.kind == .element else { return false } - // Simplification 4.1: only RELAX NG-namespace elements are schema - // content. An element with no namespace counts as schema content - // only when the whole document is unqualified (leniency for - // namespace-less schemas); under a qualified parent it is foreign. - let uri = child.name?.namespaceURI - if uri == relaxNGNamespace { return true } - return uri == nil && node.name?.namespaceURI == nil - } - } - - static func children(_ node: Tree, named name: String) -> [Tree] { - elementChildren(node).filter { localName($0) == name } - } - - static func text(_ node: Tree) -> String { - node.stringValue.trimmingXMLWhitespace() - } - - static func strip(_ qualified: String) -> String { - qualified.split(separator: ":").last.map(String.init) ?? qualified - } - - /// Resolves `reference` against an optional base URI (RFC 3986), keeping a - /// relative base's merged form relative (the same adjustment external - /// identifiers use). - static func resolveRelative(_ reference: String, against base: String?) -> String { - guard let base, !base.isEmpty else { return reference } - let merged = PureXML.Canonical.Canonicalizer.resolveURI(reference, against: base) - let baseHasScheme = base.split(separator: "/", maxSplits: 1)[0].hasSuffix(":") - if !base.hasPrefix("/"), !baseHasScheme, !reference.hasPrefix("/"), merged.hasPrefix("/") { - return String(merged.dropFirst()) - } - return merged - } - - /// The `href` of an include/externalRef, resolved against the document's - /// base and the `xml:base` attributes in scope at the node (4.5). - static func resolvedHref(_ node: Tree, documentBase: String?) -> String? { - guard let href = attribute(node, "href") else { return nil } - var bases: [String] = [] - var cursor: Tree? = node - while let current = cursor { - if let base = current.attributes.first(where: { $0.name.prefix == "xml" && $0.name.localName == "base" })?.value { - bases.append(base) - } - cursor = current.parent - } - if let documentBase { bases.append(documentBase) } - var resolved = href - for base in bases { - resolved = resolveRelative(resolved, against: base) - } - return resolved - } -} +private typealias Tree = RNGTree /// Compiles a RELAX NG XML-syntax document into the pattern algebra, resolving /// `include` and `externalRef` through a loader. Holds the accumulating `define` /// table so external grammars merge into it. File-scope and private. -private final class RNGCompiler { +final class RNGCompiler { typealias Pattern = PureXML.Schema.Pattern typealias NameClass = PureXML.Schema.NameClass typealias Compositor = PureXML.Schema.Compositor private(set) var defines: [String: Pattern] = [:] + /// The combine method established for each define name (4.17): defines + /// sharing a name agree on one method, and the single define that omits + /// `combine` still merges by it. + private var defineCombine: [String: String] = [:] private let loader: (String) -> String? private var visited: Set = [] /// The URI of the document currently being compiled (nil for the top /// level): hrefs inside a loaded document resolve against it. private var documentBase: String? + /// The `ns` value carried across a document boundary: an externalRef's + /// in-scope ns applies to the loaded pattern when it has none of its own + /// (simplification 4.6); the tree-ancestor walk cannot cross documents. + private var fallbackNamespace = "" + /// The start pattern contributed by an `include`d grammar (4.7), used when + /// the including grammar declares no start of its own. + private var includedStart: Pattern? + + /// The ns in scope at a schema node: its own/ancestor `ns` attribute, or + /// the namespace inherited across the document boundary. + func effectiveNS(_ node: Tree) -> String { + RNGNode.inheritedNS(node) ?? fallbackNamespace + } init(loader: @escaping (String) -> String?) { self.loader = loader @@ -108,8 +47,26 @@ private final class RNGCompiler { for define in RNGNode.children(node, named: "define") { addDefine(define) } - let start = RNGNode.children(node, named: "start").first - return start.map { combined(RNGNode.elementChildren($0), .sequence) } ?? .notAllowed + let starts = RNGNode.children(node, named: "start") + guard let first = starts.first else { + return includedStart ?? .notAllowed + } + // Multiple starts combine like defines (4.17): the named method wins, + // and the single start that omits `combine` still merges by it. + var method: String? + for start in starts { + if let named = RNGNode.attribute(start, "combine") { method = named } + } + var result = combined(RNGNode.elementChildren(first), .sequence) + for start in starts.dropFirst() { + let next = combined(RNGNode.elementChildren(start), .sequence) + switch method { + case "choice": result = .choice(result, next) + case "interleave": result = .interleave(result, next) + default: result = next + } + } + return result } /// Adds a `define`, honoring `combine`: a second definition of the same name @@ -118,11 +75,14 @@ private final class RNGCompiler { private func addDefine(_ define: Tree) { guard let name = RNGNode.attribute(define, "name") else { return } let pattern = combined(RNGNode.elementChildren(define), .sequence) + if let method = RNGNode.attribute(define, "combine") { + defineCombine[name] = method + } guard let existing = defines[name] else { defines[name] = pattern return } - switch RNGNode.attribute(define, "combine") { + switch defineCombine[name] { case "choice": defines[name] = .choice(existing, pattern) case "interleave": defines[name] = .interleave(existing, pattern) default: defines[name] = pattern @@ -140,12 +100,22 @@ private final class RNGCompiler { } visited.insert(href) let outerBase = documentBase + let outerNamespace = fallbackNamespace documentBase = href - defer { documentBase = outerBase } + if RNGNode.inheritedNS(grammar) == nil { + fallbackNamespace = effectiveNS(node) + } for define in RNGNode.children(grammar, named: "define") { addDefine(define) } + if includedStart == nil, let start = RNGNode.children(grammar, named: "start").first { + includedStart = combined(RNGNode.elementChildren(start), .sequence) + } documentBase = outerBase + fallbackNamespace = outerNamespace + if let override = RNGNode.children(node, named: "start").first { + includedStart = combined(RNGNode.elementChildren(override), .sequence) + } for define in RNGNode.children(node, named: "define") { addDefine(define) } @@ -153,7 +123,7 @@ private final class RNGCompiler { // MARK: Patterns - private func pattern(_ node: Tree) -> Pattern { + func pattern(_ node: Tree) -> Pattern { leafPattern(node) ?? compositePattern(node) } @@ -189,7 +159,13 @@ private final class RNGCompiler { PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets) } } - return .data(PureXML.Schema.SimpleType(base: base, facets: facets)) + let type = PureXML.Schema.SimpleType(base: base, facets: facets) + // 4.12: an except child excludes its (implicitly choice-combined) + // patterns from the data type's lexical space. + if let except = RNGNode.children(node, named: "except").first { + return .dataExcept(type, combined(RNGNode.elementChildren(except), .choice)) + } + return .data(type) } /// Builds a `` pattern carrying its datatype, so the value compares in @@ -246,8 +222,15 @@ private final class RNGCompiler { } visited.insert(href) let outerBase = documentBase + let outerNamespace = fallbackNamespace documentBase = href - defer { documentBase = outerBase } + if RNGNode.inheritedNS(top) == nil { + fallbackNamespace = effectiveNS(node) + } + defer { + documentBase = outerBase + fallbackNamespace = outerNamespace + } return topLevel(top) } @@ -279,73 +262,6 @@ private final class RNGCompiler { let content = contentNodes.isEmpty ? Pattern.text : combined(contentNodes, .sequence) return .attribute(nameClass, content) } - - private func nameClassAndContent(_ node: Tree) -> (NameClass, [Tree]) { - let children = RNGNode.elementChildren(node) - if let name = RNGNode.attribute(node, "name") { - let namespace = RNGNode.attribute(node, "ns") ?? "" - return (.name(namespace: namespace, localName: name), children) - } - guard let first = children.first else { return (.anyName, []) } - return (nameClass(first), Array(children.dropFirst())) - } - - // MARK: Combinators and name classes - - private func combined(_ nodes: [Tree], _ compositor: Compositor) -> Pattern { - let patterns = nodes.map(pattern) - let identity: Pattern = compositor == .choice ? .notAllowed : .empty - guard let first = patterns.first else { return identity } - return patterns.dropFirst().reduce(first) { combine($0, $1, compositor) } - } - - private func combine(_ lhs: Pattern, _ rhs: Pattern, _ compositor: Compositor) -> Pattern { - switch compositor { - case .sequence: .group(lhs, rhs) - case .choice: .choice(lhs, rhs) - case .all: .interleave(lhs, rhs) - } - } - - private func nameClass(_ node: Tree) -> NameClass { - switch RNGNode.localName(node) { - case "name": - .name(namespace: RNGNode.attribute(node, "ns") ?? "", localName: RNGNode.text(node)) - case "anyName": - anyName(node) - case "nsName": - nsName(node) - case "choice": - nameClassChoice(RNGNode.elementChildren(node)) - default: - .anyName - } - } - - private func anyName(_ node: Tree) -> NameClass { - guard let except = exceptClass(node) else { return .anyName } - return .anyNameExcept(except) - } - - private func nsName(_ node: Tree) -> NameClass { - let namespace = RNGNode.attribute(node, "ns") ?? "" - guard let except = exceptClass(node) else { return .nsName(namespace) } - return .nsNameExcept(namespace: namespace, except: except) - } - - /// The name class of a ``'s `` child, treating multiple - /// children as an implicit choice (`anyName`/`nsName` minus the union). - private func exceptClass(_ node: Tree) -> NameClass? { - guard let except = RNGNode.children(node, named: "except").first else { return nil } - let children = RNGNode.elementChildren(except) - return children.isEmpty ? nil : nameClassChoice(children) - } - - private func nameClassChoice(_ nodes: [Tree]) -> NameClass { - let classes = nodes.map(nameClass) - guard let first = classes.first else { return .anyName } - return classes.dropFirst().reduce(first) { .choice($0, $1) } - } } extension PureXML.Schema { diff --git a/Sources/Schema/RelaxNGPattern.swift b/Sources/Schema/RelaxNGPattern.swift index 2a07ce7..df4ad35 100644 --- a/Sources/Schema/RelaxNGPattern.swift +++ b/Sources/Schema/RelaxNGPattern.swift @@ -36,6 +36,9 @@ public extension PureXML.Schema { case element(NameClass, Pattern) case attribute(NameClass, Pattern) case data(SimpleType) + /// `` with an `` child: the type matches but the except + /// pattern must not (4.12). + case dataExcept(SimpleType, Pattern) case value(SimpleType, String) case list(Pattern) case ref(String) diff --git a/Tests/RelaxNGSpecSuiteTests.swift b/Tests/RelaxNGSpecSuiteTests.swift index fc7fd7e..cafe128 100644 --- a/Tests/RelaxNGSpecSuiteTests.swift +++ b/Tests/RelaxNGSpecSuiteTests.swift @@ -33,12 +33,10 @@ struct RelaxNGSpecSuiteTests { private let knownIncorrectCompiledCount = 213 private let knownCorrectRejected: Set = [] private let knownValidRejected: Set = [ - 98, 104, 109, 110, 111, 115, 119, 120, 124, 125, 126, 127, 128, 130, - 131, 132, 133, 142, 190, 191, 194, 195, 208, 209, 210, 236, 256, 258, - 265, 266, 268, 269, 271, 328, 372, 374, 378, 379, 380, + 98, 111, 115, 133, 195, 208, 209, 210, 271, 328, 379, 380, ] private let knownInvalidAccepted: Set = [ - 104, 124, 125, 126, 127, 128, 142, 264, 271, + 120, 142, 264, 271, 378, 379, ] private struct SpecCase {