extension PureXML.Schema { /// Validates a document against a RELAX NG grammar by Brzozowski-style /// derivatives (James Clark's "An algorithm for RELAX NG validation"). The /// pattern is repeatedly derived with respect to each event of the instance; /// the document is valid when the residual pattern is nullable. /// /// Bound: there is no derivative memoization (patterns are not hashable by /// design). The simplifying constructors (`choice`/`group`/`interleave` /// short-circuit on `notAllowed` and `empty`) keep the residual small for /// real-world grammars, but an adversarial grammar of deeply nested /// choices can still grow it; treat untrusted schemas accordingly. struct RelaxNGEngine { let defines: [String: PureXML.Schema.Pattern] /// Whether `root` matches `start`. func matches(start: PureXML.Schema.Pattern, root: PureXML.Model.Node) -> Bool { nullable(childDeriv(start, root, env: Self.rootEnvironment)) } /// The namespace bindings every instance starts with. static let rootEnvironment: [String: String] = ["xml": "http://www.w3.org/XML/1998/namespace"] /// `env` extended with the element's own xmlns declarations. static func extendedEnvironment(_ env: [String: String], with element: PureXML.Model.Element) -> [String: String] { var extended = env for attribute in element.attributes where isNamespaceDeclaration(attribute) { let prefix = attribute.name.prefix == "xmlns" ? attribute.name.localName : "" extended[prefix] = attribute.value } return extended } /// Resolves an instance-side QName text against the in-scope bindings: /// a prefix must be bound; an unprefixed name takes the default /// namespace (the xs:QName rules). static func resolveQNameValue(_ raw: String, env: [String: String]) -> (namespace: String, localName: String)? { let trimmed = raw.trimmingXMLWhitespace() guard let colon = trimmed.firstIndex(of: ":") else { return (env[""] ?? "", trimmed) } let prefix = String(trimmed[.. PureXML.Schema.Pattern { if case .notAllowed = lhs { return rhs } if case .notAllowed = rhs { return lhs } return .choice(lhs, rhs) } private func group(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { if case .notAllowed = lhs { return .notAllowed } if case .notAllowed = rhs { return .notAllowed } if case .empty = lhs { return rhs } if case .empty = rhs { return lhs } return .group(lhs, rhs) } private func interleave(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { if case .notAllowed = lhs { return .notAllowed } if case .notAllowed = rhs { return .notAllowed } if case .empty = lhs { return rhs } if case .empty = rhs { return lhs } return .interleave(lhs, rhs) } private func after(_ lhs: PureXML.Schema.Pattern, _ rhs: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { if case .notAllowed = lhs { return .notAllowed } if case .notAllowed = rhs { return .notAllowed } return .after(lhs, rhs) } func resolve(_ name: String) -> PureXML.Schema.Pattern { defines[name] ?? .notAllowed } // MARK: Nullability func nullable(_ pattern: PureXML.Schema.Pattern, visiting: Set = []) -> Bool { switch pattern { case .empty, .text: return true case let .choice(lhs, rhs): return nullable(lhs, visiting: visiting) || nullable(rhs, visiting: visiting) case let .group(lhs, rhs), let .interleave(lhs, rhs): return nullable(lhs, visiting: visiting) && nullable(rhs, visiting: visiting) case let .oneOrMore(inner), let .list(inner): return nullable(inner, visiting: visiting) case let .ref(name): guard !visiting.contains(name) else { return false } return nullable(resolve(name), visiting: visiting.union([name])) default: return false } } // MARK: Text and attribute derivatives func textDeriv(_ pattern: PureXML.Schema.Pattern, _ string: String, env: [String: String] = rootEnvironment) -> PureXML.Schema.Pattern { switch pattern { case let .choice(lhs, rhs): return choice(textDeriv(lhs, string, env: env), textDeriv(rhs, string, env: env)) case let .interleave(lhs, rhs): return choice(interleave(textDeriv(lhs, string, env: env), rhs), interleave(lhs, textDeriv(rhs, string, env: env))) case let .group(lhs, rhs): let derived = group(textDeriv(lhs, string, env: env), rhs) return nullable(lhs) ? choice(derived, textDeriv(rhs, string, env: env)) : derived case let .after(lhs, rhs): return after(textDeriv(lhs, string, env: env), rhs) case let .oneOrMore(inner): return group(textDeriv(inner, string, env: env), choice(.oneOrMore(inner), .empty)) case .text: return .text case let .ref(name): return textDeriv(resolve(name), string, env: env) default: return valueDeriv(pattern, string, env: env) } } private func valueDeriv(_ pattern: PureXML.Schema.Pattern, _ string: String, env: [String: String]) -> PureXML.Schema.Pattern { switch pattern { 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(textDeriv(except, string, env: env)) ? .empty : .notAllowed case let .valueQName(namespace, localName): Self.resolveQNameValue(string, env: env).map { $0 == (namespace, localName) } == true ? .empty : .notAllowed case let .list(inner): listMatches(inner, string, env: env) ? .empty : .notAllowed default: .notAllowed } } private func listMatches(_ pattern: PureXML.Schema.Pattern, _ string: String, env _: [String: String]) -> Bool { var residual = pattern for token in string.split(whereSeparator: \.isWhitespace) { residual = textDeriv(residual, String(token)) } return nullable(residual) } func attributeDeriv(_ pattern: PureXML.Schema.Pattern, _ attribute: PureXML.Model.Attribute, env: [String: String] = rootEnvironment) -> PureXML.Schema.Pattern { switch pattern { case let .choice(lhs, rhs): return choice(attributeDeriv(lhs, attribute, env: env), attributeDeriv(rhs, attribute, env: env)) case let .group(lhs, rhs): return choice(group(attributeDeriv(lhs, attribute, env: env), rhs), group(lhs, attributeDeriv(rhs, attribute, env: env))) case let .interleave(lhs, rhs): return choice(interleave(attributeDeriv(lhs, attribute, env: env), rhs), interleave(lhs, attributeDeriv(rhs, attribute, env: env))) case let .oneOrMore(inner): return group(attributeDeriv(inner, attribute, env: env), choice(.oneOrMore(inner), .empty)) case let .after(lhs, rhs): return after(attributeDeriv(lhs, attribute, env: env), rhs) case let .attribute(nameClass, content): guard nameClass.contains(attribute.name), valueMatches(content, attribute.value, env: env) else { return .notAllowed } return .empty case let .ref(name): return attributeDeriv(resolve(name), attribute, env: env) default: return .notAllowed } } private func valueMatches(_ content: PureXML.Schema.Pattern, _ value: String, env: [String: String]) -> Bool { if nullable(textDeriv(content, value, env: env)) { 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 { if case .notAllowed = inner { return .notAllowed } return .oneOrMore(inner) } } } extension PureXML.Schema.RelaxNGEngine { // MARK: Element derivatives func childDeriv(_ pattern: PureXML.Schema.Pattern, _ node: PureXML.Model.Node, env: [String: String] = rootEnvironment) -> PureXML.Schema.Pattern { switch node { case let .text(string), let .cdata(string): textDeriv(pattern, string, env: env) case let .element(element): processElement(pattern, element, env: env) default: pattern } } private func processElement(_ pattern: PureXML.Schema.Pattern, _ element: PureXML.Model.Element, env: [String: String]) -> PureXML.Schema.Pattern { let extended = Self.extendedEnvironment(env, with: element) let opened = startTagOpenDeriv(pattern, element.name) let attributed = element.attributes .filter { !Self.isNamespaceDeclaration($0) } .reduce(opened) { attributeDeriv($0, $1, env: extended) } let closed = startTagCloseDeriv(attributed) let withChildren = childrenDeriv(closed, element.children, env: extended) return endTagDeriv(withChildren) } func startTagOpenDeriv(_ pattern: PureXML.Schema.Pattern, _ name: PureXML.Model.QualifiedName) -> PureXML.Schema.Pattern { switch pattern { case let .choice(lhs, rhs): return choice(startTagOpenDeriv(lhs, name), startTagOpenDeriv(rhs, name)) case let .element(nameClass, content): return nameClass.contains(name) ? after(content, .empty) : .notAllowed case let .interleave(lhs, rhs): return choice( applyAfter({ interleave($0, rhs) }, startTagOpenDeriv(lhs, name)), applyAfter({ interleave(lhs, $0) }, startTagOpenDeriv(rhs, name)), ) case let .group(lhs, rhs): let derived = applyAfter({ group($0, rhs) }, startTagOpenDeriv(lhs, name)) return nullable(lhs) ? choice(derived, startTagOpenDeriv(rhs, name)) : derived case let .oneOrMore(inner): return applyAfter({ group($0, choice(oneOrMoreP(inner), .empty)) }, startTagOpenDeriv(inner, name)) case let .after(lhs, rhs): return applyAfter({ after($0, rhs) }, startTagOpenDeriv(lhs, name)) case let .ref(reference): return startTagOpenDeriv(resolve(reference), name) default: return .notAllowed } } private func applyAfter(_ transform: (PureXML.Schema.Pattern) -> PureXML.Schema.Pattern, _ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { switch pattern { case let .after(lhs, rhs): after(lhs, transform(rhs)) case let .choice(lhs, rhs): choice(applyAfter(transform, lhs), applyAfter(transform, rhs)) default: .notAllowed } } func startTagCloseDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { switch pattern { case let .choice(lhs, rhs): choice(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs)) case let .group(lhs, rhs): group(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs)) case let .interleave(lhs, rhs): interleave(startTagCloseDeriv(lhs), startTagCloseDeriv(rhs)) case let .oneOrMore(inner): oneOrMoreP(startTagCloseDeriv(inner)) case let .after(lhs, rhs): after(startTagCloseDeriv(lhs), rhs) case .attribute: .notAllowed case let .ref(reference): startTagCloseDeriv(resolve(reference)) default: pattern } } private func childrenDeriv(_ pattern: PureXML.Schema.Pattern, _ children: [PureXML.Model.Node], env: [String: String]) -> PureXML.Schema.Pattern { let coalesced = Self.coalesceText(children) // 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, "", env: env)) } if coalesced.count == 1, case let .text(string) = coalesced[0] { let derived = textDeriv(pattern, string, env: env) return Self.isWhitespace(string) ? choice(pattern, derived) : derived } return coalesced.reduce(pattern) { accumulated, node in Self.isWhitespaceText(node) ? accumulated : childDeriv(accumulated, node, env: env) } } private func endTagDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern { switch pattern { case let .choice(lhs, rhs): choice(endTagDeriv(lhs), endTagDeriv(rhs)) case let .after(lhs, rhs): nullable(lhs) ? rhs : .notAllowed case let .ref(reference): endTagDeriv(resolve(reference)) default: .notAllowed } } // MARK: Helpers /// Merges adjacent text and CDATA nodes into single text nodes. static func coalesceText(_ children: [PureXML.Model.Node]) -> [PureXML.Model.Node] { var result: [PureXML.Model.Node] = [] var run = "" for child in children { switch child { case let .text(value), let .cdata(value): run += value case .comment, .processingInstruction: continue default: if !run.isEmpty { result.append(.text(run)) run = "" } result.append(child) } } if !run.isEmpty { result.append(.text(run)) } return result } private static func isWhitespace(_ string: String) -> Bool { string.allSatisfy(\.isWhitespace) } static func isWhitespaceText(_ node: PureXML.Model.Node) -> Bool { if case let .text(value) = node { return isWhitespace(value) } return false } static func isNamespaceDeclaration(_ attribute: PureXML.Model.Attribute) -> Bool { attribute.name.prefix == "xmlns" || (attribute.name.prefix == nil && attribute.name.localName == "xmlns") } static func elementNameCounts(_ children: [PureXML.Model.Node]) -> [String: Int] { var counts: [String: Int] = [:] for case let .element(element) in children { counts[element.name.description, default: 0] += 1 } return counts } } /// One open element while streaming a RELAX NG derivative: whether it has had an /// element child yet, and the text buffered since the last structural event. The /// `after`-threaded pattern is the validation stack; this frame only carries the /// bookkeeping `childrenDeriv` needs to treat ignorable whitespace correctly. private struct RelaxNGStreamFrame { var hadElementChild = false var pendingText = "" /// The namespace bindings in scope at this element, for QName values. var env: [String: String] = [:] } public extension PureXML.Schema { /// The residual derivative plus per-element whitespace bookkeeping carried /// across the events of a streamed RELAX NG validation. struct RelaxNGStreamingState { var current: PureXML.Schema.Pattern fileprivate var frames: [RelaxNGStreamFrame] } } extension PureXML.Schema.RelaxNGEngine { /// The initial streaming state for a start pattern, with a document-level frame. func streamingStart(_ start: PureXML.Schema.Pattern) -> PureXML.Schema.RelaxNGStreamingState { PureXML.Schema.RelaxNGStreamingState(current: start, frames: [RelaxNGStreamFrame(env: Self.rootEnvironment)]) } /// Derives the state by one parse event, the streaming form of `childDeriv`. /// Text is buffered and applied at structural boundaries so ignorable /// whitespace is treated exactly as the tree walk's `childrenDeriv` does. func streamingConsume(_ event: PureXML.Parsing.Event, into state: inout PureXML.Schema.RelaxNGStreamingState) { switch event { case let .startElement(name, attributes): flushText(&state, closing: false) if !state.frames.isEmpty { state.frames[state.frames.count - 1].hadElementChild = true } var env = state.frames.last?.env ?? Self.rootEnvironment for attribute in attributes where Self.isNamespaceDeclaration(attribute) { env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value } let opened = startTagOpenDeriv(state.current, name) let attributed = attributes .filter { !Self.isNamespaceDeclaration($0) } .reduce(opened) { attributeDeriv($0, $1, env: env) } state.current = startTagCloseDeriv(attributed) state.frames.append(RelaxNGStreamFrame(env: env)) case let .characters(text), let .cdata(text): if !state.frames.isEmpty { state.frames[state.frames.count - 1].pendingText += text } case .endElement: flushText(&state, closing: true) state.current = endTagDeriv(state.current) if !state.frames.isEmpty { state.frames.removeLast() } case .comment, .processingInstruction: break } } /// Whether the streamed document satisfied the schema (the residual is nullable). func streamingValid(_ state: PureXML.Schema.RelaxNGStreamingState) -> Bool { nullable(state.current) } private func flushText(_ state: inout PureXML.Schema.RelaxNGStreamingState, closing: Bool) { guard let index = state.frames.indices.last else { return } let text = state.frames[index].pendingText let env = state.frames[index].env guard !text.isEmpty else { // An empty element is optional empty text (the tree walk's rule), // so a data/value pattern admitting "" matches . if closing, !state.frames[index].hadElementChild { state.current = choice(state.current, textDeriv(state.current, "", env: env)) } return } state.frames[index].pendingText = "" if !Self.isWhitespace(text) { state.current = textDeriv(state.current, text, env: env) } else if closing, !state.frames[index].hadElementChild { // A lone whitespace text child is optional, the `childrenDeriv` rule. state.current = choice(state.current, textDeriv(state.current, text, env: env)) } // Whitespace among element siblings is ignorable and skipped. } }