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
78 lines
2.8 KiB
Swift
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 == "."
|
|
}
|
|
}
|
|
}
|