PureXML/Sources/Parsing/NamespaceContext.swift
Mihaela Mihaljevic 4b03c45434 feat(parsing): Namespaces 1.0 constraint enforcement
The namespace layer rejects malformed qualified names, declarations of
the xmlns prefix, misbindings of the xml prefix and either reserved
namespace name (including as the default namespace), and prefix
undeclaration; attributes must be distinct by expanded name; PI targets
and entity/notation names are NCNames; and DTD-typed xmlns values bind
their normalized form. Clears all 18 namespace cases from the eduni
baseline; xmltest valid/sa/012 joins the documented deviations per the
suite's own NAMESPACE='no' flag.

Closes #136
2026-06-10 08:06:45 +02:00

145 lines
6.8 KiB
Swift

extension PureXML.Parsing {
/// Tracks in-scope namespace bindings as elements open and close, resolving
/// qualified names to their namespace URI. The reserved `xml` and `xmlns`
/// prefixes are built in. Internal to the parser.
///
/// Follows the XML Namespaces rules: a default namespace (`xmlns="..."`)
/// applies to unprefixed element names but never to attribute names, an empty
/// declaration undeclares a binding, and an unbound prefix is an error.
struct NamespaceContext {
/// The reserved binding for the `xml` prefix.
static let xmlNamespaceURI = "http://www.w3.org/XML/1998/namespace"
/// The namespace name reserved for `xmlns` itself; it may never be
/// bound by a declaration.
static let xmlnsNamespaceURI = "http://www.w3.org/2000/xmlns/"
/// A stack of scopes; each maps a prefix ("" = default namespace) to a URI.
private var scopes: [[String: String]] = []
/// Opens an element: pushes a scope built from its `xmlns` declarations,
/// then resolves the element name and its attribute names against the
/// now-in-scope bindings.
mutating func enterElement(
name: PureXML.Model.QualifiedName,
attributes: [PureXML.Model.Attribute],
at mark: Mark,
) throws -> (name: PureXML.Model.QualifiedName, attributes: [PureXML.Model.Attribute]) {
try checkNameShape(name, at: mark)
var scope: [String: String] = [:]
for attribute in attributes {
try checkNameShape(attribute.name, at: mark)
if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
try checkDefaultBinding(attribute.value, at: mark)
scope[""] = attribute.value
} else if attribute.name.prefix == "xmlns" {
try checkPrefixBinding(attribute.name.localName, uri: attribute.value, at: mark)
scope[attribute.name.localName] = attribute.value
}
}
scopes.append(scope)
let resolvedName = try resolveElement(name, at: mark)
let resolvedAttributes = try attributes.map { try resolveAttribute($0, at: mark) }
try checkExpandedNames(resolvedAttributes, at: mark)
return (resolvedName, resolvedAttributes)
}
/// Closes an element, popping its scope.
mutating func leaveElement() {
if !scopes.isEmpty {
scopes.removeLast()
}
}
private func resolveElement(
_ name: PureXML.Model.QualifiedName,
at mark: Mark,
) throws -> PureXML.Model.QualifiedName {
if name.prefix == nil {
return name.resolved(namespaceURI: lookup(""))
}
return try name.resolved(namespaceURI: requireURI(forPrefix: name.prefix, at: mark))
}
private func resolveAttribute(
_ attribute: PureXML.Model.Attribute,
at mark: Mark,
) throws -> PureXML.Model.Attribute {
let name = attribute.name
if name.prefix == nil || name.prefix == "xmlns" {
return attribute
}
let uri = try requireURI(forPrefix: name.prefix, at: mark)
return PureXML.Model.Attribute(name: name.resolved(namespaceURI: uri), value: attribute.value)
}
private func requireURI(forPrefix prefix: String?, at mark: Mark) throws -> String {
if prefix == "xml" {
return Self.xmlNamespaceURI
}
if let uri = lookup(prefix ?? ""), !uri.isEmpty {
return uri
}
throw ParseError.undefinedNamespacePrefix(prefix: prefix ?? "", mark)
}
/// A qualified name has exactly one colon separating two non-empty
/// NCNames; `QualifiedName` keeps a malformed raw form (`foo:`,
/// `:foo`, `a:b:c`, `xmlns:`) in its local name, where the colon
/// betrays it.
private func checkNameShape(_ name: PureXML.Model.QualifiedName, at mark: Mark) throws {
guard name.localName.contains(":") else { return }
throw ParseError.namespaceConstraint(reason: "'\(name.description)' is not a legal qualified name", mark)
}
/// The reserved-binding rules for `xmlns:prefix` declarations: `xmlns`
/// itself may never be declared, `xml` only with its own namespace
/// name, no other prefix may take either reserved namespace name, and
/// a prefix may not be undeclared in Namespaces 1.0.
private func checkPrefixBinding(_ prefix: String, uri: String, at mark: Mark) throws {
if prefix == "xmlns" {
throw ParseError.namespaceConstraint(reason: "the 'xmlns' prefix must not be declared", mark)
}
if prefix == "xml" {
guard uri == Self.xmlNamespaceURI else {
throw ParseError.namespaceConstraint(reason: "the 'xml' prefix is bound to '\(Self.xmlNamespaceURI)' and cannot be rebound", mark)
}
return
}
if uri == Self.xmlNamespaceURI || uri == Self.xmlnsNamespaceURI {
throw ParseError.namespaceConstraint(reason: "the reserved namespace name '\(uri)' must not be bound to prefix '\(prefix)'", mark)
}
if uri.isEmpty {
throw ParseError.namespaceConstraint(reason: "prefix '\(prefix)' may not be undeclared in Namespaces 1.0", mark)
}
}
/// Neither reserved namespace name may become the default namespace.
private func checkDefaultBinding(_ uri: String, at mark: Mark) throws {
guard uri == Self.xmlNamespaceURI || uri == Self.xmlnsNamespaceURI else { return }
throw ParseError.namespaceConstraint(reason: "the reserved namespace name '\(uri)' must not be the default namespace", mark)
}
/// Attributes must be distinct by expanded name: two prefixes bound to
/// the same URI cannot carry the same local name on one element.
private func checkExpandedNames(_ attributes: [PureXML.Model.Attribute], at mark: Mark) throws {
var seen: Set<String> = []
for attribute in attributes where attribute.name.namespaceURI != nil {
let key = (attribute.name.namespaceURI ?? "") + "\u{0}" + attribute.name.localName
guard seen.insert(key).inserted else {
throw ParseError.duplicateAttribute(name: attribute.name.description, mark)
}
}
}
private func lookup(_ key: String) -> String? {
for scope in scopes.reversed() {
if let uri = scope[key] {
return uri.isEmpty ? nil : uri
}
}
return nil
}
}
}