PureXML/Sources/Parsing/PseudoAttributeScanner.swift
Mihaela Mihaljevic dd0ff5d086 feat(parsing): fourth W3C-suite strictness pass, declaration grammar
Make the DTD declaration grammar strict: <!ENTITY> requires its whitespace
(after the keyword, around %, after the name), a real name (not #DEFAULT),
nothing but '>' after the value, NDATA preceded by whitespace and forbidden
on parameter entities; entity-value literals are validated (XML Chars only,
every & a complete reference); SYSTEM/PUBLIC external identifiers parse
strictly (PUBLIC needs both literals for entities and the DOCTYPE, public-only
stays legal for notations) with PubidChar validation; <!ATTLIST> bodies are
validated against XML 1.0 productions 52-60 via a new DTDAttListGrammar
(spacing, attribute types, enumerations, quoted defaults); the reserved xml
PI target is rejected inside the subset. Also: strict UTF-8 decoding (no
silent replacement of malformed sequences), XML Char checks in attribute
values and CDATA, the declaration's version/encoding value grammar, and
required whitespace between pseudo-attributes. The strict machinery lives in
DoctypeScannerStrict/DTDAttListGrammar to stay under the length caps.

W3C xmltest: not-wf/sa 162/186 rejected (from 129; 80 at first contact),
valid/sa 118/119, zero false positives across all 846 tests at every step.
The remaining 24-case baseline is exactly: the entity replacement-text class
(19) and the deliberate internal-subset extensions this package supports as
features (conditional sections, PEs in the internal subset: 5).

Part of #120, #105
2026-06-10 05:34:11 +02:00

78 lines
2.8 KiB
Swift

extension PureXML.Parsing.XMLDeclaration {
/// A name/value pair from an XML declaration.
struct PseudoAttribute: Equatable {
let name: String
let value: String
}
/// Scans the `name="value"` pseudo-attributes of an XML declaration's text.
/// Each value must be quoted; anything malformed (a missing `=`, an unquoted
/// value, trailing junk) makes the whole scan fail so the declaration is
/// rejected rather than partly read.
struct PseudoAttributeScanner {
private let chars: [Character]
private var index = 0
init(_ text: String) {
chars = Array(text)
}
mutating func scan() -> [PseudoAttribute]? {
var pairs: [PseudoAttribute] = []
skipSpace()
while index < chars.count {
guard let name = scanName(), !name.isEmpty else { return nil }
skipSpace()
guard consume("=") else { return nil }
skipSpace()
guard let value = scanQuoted() else { return nil }
pairs.append(PseudoAttribute(name: name, value: value))
// Pseudo-attributes must be separated by whitespace.
let before = index
skipSpace()
if index == before, index < chars.count { return nil }
}
return pairs
}
private mutating func scanName() -> String? {
var name = ""
while index < chars.count, isNameCharacter(chars[index]) {
name.append(chars[index])
index += 1
}
return name.isEmpty ? nil : name
}
private mutating func scanQuoted() -> String? {
guard index < chars.count, chars[index] == "\"" || chars[index] == "'" else { return nil }
let quote = chars[index]
index += 1
var value = ""
while index < chars.count, chars[index] != quote {
value.append(chars[index])
index += 1
}
guard index < chars.count else { return nil }
index += 1
return value
}
private mutating func consume(_ character: Character) -> Bool {
guard index < chars.count, chars[index] == character else { return false }
index += 1
return true
}
private mutating func skipSpace() {
while index < chars.count, chars[index] == " " || chars[index] == "\t" || chars[index] == "\n" || chars[index] == "\r" {
index += 1
}
}
private func isNameCharacter(_ character: Character) -> Bool {
character.isLetter || character.isNumber || character == "-" || character == "_" || character == "."
}
}
}