PureXML/Sources/Schema/RelaxNGParserIncludes.swift
Mihaela Mihaljevic 3415ff3d9a feat(relaxng): schema-correctness validation, spec suite complete
A RELAX NG schema document is validated before pattern interpretation
in three layers: the section-3 grammar check (vocabulary, attributes,
arity, text, QNames, datatypeLibrary URIs, hrefs, 4.16 excepts, xmlns
prohibitions), compiler checks (4.17 combine rules, unresolved refs,
parentRef scope, startless grammars, include/externalRef cycles and
overrides, grammar-checked loaded documents, unknown datatypes,
built-in-library params), and the 4.19/7.1-7.4 restrictions over the
compiled algebra after 4.20/4.21 normalization (prohibited paths,
content types, duplicate attributes via representative-name overlap,
infinite-class attributes outside oneOrMore, interleave constraints),
with recursion detection deliberately before normalization. Incorrect
schemas: 213 compiling down to 7, all the pre-Fifth-Edition name-class
class (count asserted). Two leniency tests updated to spec behavior.

Closes #131
2026-06-10 11:38:45 +02:00

118 lines
5.2 KiB
Swift

/// The include and externalRef resolution of the RELAX NG compiler, split
/// from the compiler body for the length caps: transitive merging with 4.7
/// overrides, per-document bases, cross-document ns, and cycle detection.
extension RNGCompiler {
/// Merges an `include`d grammar: its `define`s first, then the `include`'s own
/// nested `define`s, which override or (with `combine`) merge with them.
func mergeInclude(_ node: Tree) {
guard let href = RNGNode.resolvedHref(node, documentBase: documentBase) else { return }
guard !visited.contains(href) else {
violations.append("include '\(href)' includes itself")
return
}
guard let grammar = loadedGrammar(href) else { return }
visited.insert(href)
defer { visited.remove(href) }
mergeLoadedGrammar(grammar, from: node, href: href)
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)
}
}
/// Merges the loaded grammar's components under its own base and
/// namespace scope: transitive includes first, then its defines (minus
/// the include's overrides) and its start.
private func mergeLoadedGrammar(_ grammar: Tree, from node: Tree, href: String) {
let outerBase = documentBase
let outerNamespace = fallbackNamespace
documentBase = href
if RNGNode.inheritedNS(grammar) == nil {
fallbackNamespace = effectiveNS(node)
}
defer {
documentBase = outerBase
fallbackNamespace = outerNamespace
}
// The included grammar's own includes merge first (4.7 is transitive).
for nested in RNGNode.components(grammar, named: "include") {
mergeInclude(nested)
}
let overrides = includeOverrides(node, grammar: grammar)
for define in RNGNode.components(grammar, named: "define") {
guard let name = RNGNode.attribute(define, "name"), !overrides.contains(name) else { continue }
addDefine(define)
}
if includedStart == nil, let start = RNGNode.components(grammar, named: "start").first {
includedStart = combined(RNGNode.elementChildren(start), .sequence)
}
}
/// A define carried by the include element REPLACES the included
/// grammar's define of the same name (4.7 override); an override with
/// nothing to override is a schema error.
private func includeOverrides(_ node: Tree, grammar: Tree) -> Set<String> {
let overrides = Set(RNGNode.children(node, named: "define").compactMap { RNGNode.attribute($0, "name") })
let provided = Set(RNGNode.components(grammar, named: "define").compactMap { RNGNode.attribute($0, "name") })
for name in overrides.sorted() where !provided.contains(name) {
violations.append("include overrides define '\(name)', which the included grammar does not define")
}
let overridesStart = RNGNode.children(node, named: "start").first != nil
if overridesStart, RNGNode.components(grammar, named: "start").first == nil {
violations.append("include overrides the start, which the included grammar does not define")
}
return overrides
}
/// Loads and validates an include's target, which must be a grammatical
/// RELAX NG grammar element.
private func loadedGrammar(_ href: String) -> Tree? {
guard let text = loader(href), let root = try? PureXML.parseTree(text),
let grammar = RNGNode.elementChildren(root).first
else {
return nil
}
if RNGNode.localName(grammar) != "grammar" {
violations.append("include '\(href)' must reference a grammar")
return nil
}
if let problem = RelaxNGGrammarCheck.violation(grammar) {
violations.append("included schema '\(href)': \(problem)")
return nil
}
return grammar
}
func externalRef(_ node: Tree) -> Pattern {
guard let href = RNGNode.resolvedHref(node, documentBase: documentBase) else { return .notAllowed }
guard !visited.contains(href) else {
violations.append("externalRef '\(href)' references itself")
return .notAllowed
}
guard let text = loader(href), let root = try? PureXML.parseTree(text),
let top = RNGNode.elementChildren(root).first
else {
return .notAllowed
}
if let problem = RelaxNGGrammarCheck.violation(top) {
violations.append("referenced schema '\(href)': \(problem)")
return .notAllowed
}
visited.insert(href)
defer { visited.remove(href) }
let outerBase = documentBase
let outerNamespace = fallbackNamespace
documentBase = href
if RNGNode.inheritedNS(top) == nil {
fallbackNamespace = effectiveNS(node)
}
defer {
documentBase = outerBase
fallbackNamespace = outerNamespace
}
return topLevel(top)
}
}