The attribute-value scanner ran the entity decoder on every value, and the decoder re-scanned the whole value for '&' before doing nothing when none was present. The byte-run scanner already visits every value byte, so it now notes any '&' and an ampersand-free value (the common case) returns its normalized run directly. Measured against a same-machine-state baseline, every run came in below every baseline run, ~4% faster parse on the 20k-item generated corpus. Move the bulk byte-run scanners (contentRunBytes, attributeRunBytes) into ReaderByteRuns.swift and centralize their cursor mutation through a single advanceByteRun method, so Reader's position fields stay write-encapsulated in Reader.swift while both files stay under the length cap. The byte and Character paths are held to identical decoded values by the differential oracle, extended with ampersand-free, entity, and character-reference attribute cases. No spec behavior changed.
104 lines
5.1 KiB
Swift
104 lines
5.1 KiB
Swift
/// The bulk byte-run scanners, split into their own file so `Reader`'s primary
|
|
/// type body and `Reader.swift` both stay under the length caps. They consume
|
|
/// plain ASCII runs of content or attribute-value text directly from the owned
|
|
/// storage (the common case in any real document) and hand subtle bytes back to
|
|
/// the Character path. Both engage only when the lookahead buffer is empty, read
|
|
/// the cursor through `Reader`'s `private(set)` accessors, and commit their
|
|
/// position deltas through `advanceByteRun` so all cursor mutation stays in
|
|
/// `Reader.swift`.
|
|
extension PureXML.Parsing.Reader {
|
|
/// Bulk-scans character data in byte mode: consumes and returns the longest
|
|
/// upcoming run of plain ASCII content bytes (no '<', no carriage return, no
|
|
/// "]]" pair, every byte a valid XML character), or nil when the fast path
|
|
/// does not apply. Anything subtle, an entity boundary aside ('&' is plain
|
|
/// content here, noted via `sawAmpersand`), is left for the Character path so
|
|
/// error marks stay exact. Returns nil rather than an empty run so callers
|
|
/// can alternate with the slow loop.
|
|
mutating func contentRunBytes(sawAmpersand: inout Bool) -> String? {
|
|
guard buffer.isEmpty, pendingRaw == nil, let storage else { return nil }
|
|
let pointer = storage.pointer
|
|
let count = storage.count
|
|
// A run never contains ']', so "]]>" can only straddle the boundary where
|
|
// the slow path consumed "]]" and the run would begin with '>': leave a
|
|
// leading '>' to the slow path and its cdataCloseInContent check (W3C
|
|
// ibm14n01).
|
|
if byteIndex < count, pointer[byteIndex] == 0x3E { return nil }
|
|
var index = byteIndex
|
|
var newlines = 0
|
|
var lastLineStart = -1
|
|
while index < count {
|
|
let byte = pointer[index]
|
|
if byte == 0x3C { break } // '<'
|
|
// Valid plain ASCII content only; CR, ']' and non-ASCII go to the
|
|
// character path.
|
|
guard byte < 0x80, byte != 0x0D, byte != 0x5D,
|
|
byte >= 0x20 || byte == 0x09 || byte == 0x0A
|
|
else {
|
|
if index == byteIndex { return nil }
|
|
break
|
|
}
|
|
if byte == 0x0A {
|
|
newlines += 1
|
|
lastLineStart = index
|
|
} else if byte == 0x26 {
|
|
sawAmpersand = true
|
|
}
|
|
index += 1
|
|
}
|
|
guard index > byteIndex else { return nil }
|
|
let run = String(decoding: UnsafeBufferPointer(start: pointer + byteIndex, count: index - byteIndex), as: UTF8.self)
|
|
advanceByteRun(to: index, length: index - byteIndex, newlines: newlines, lastLineStart: lastLineStart)
|
|
return run
|
|
}
|
|
|
|
/// Bulk-scans an attribute value in byte mode: consumes and returns the
|
|
/// longest upcoming run of plain ASCII value bytes, stopping at the closing
|
|
/// `quote`, a raw '<', a carriage return (2.11 folding then 3.3.3
|
|
/// normalization must run on the Character path), a control character, or a
|
|
/// non-ASCII byte. Tab and line feed inside the run normalize to a single
|
|
/// space per 3.3.3; '&' stays raw for later reference decoding and is noted
|
|
/// via `sawAmpersand`. Returns nil when the fast path does not apply or the
|
|
/// run would be empty, so callers alternate with the slow loop.
|
|
mutating func attributeRunBytes(quote: UInt8, sawAmpersand: inout Bool) -> String? {
|
|
guard buffer.isEmpty, pendingRaw == nil, let storage else { return nil }
|
|
let pointer = storage.pointer
|
|
let count = storage.count
|
|
var index = byteIndex
|
|
var newlines = 0
|
|
var lastLineStart = -1
|
|
var needsTransform = false
|
|
scan: while index < count {
|
|
switch pointer[index] {
|
|
case quote, 0x3C, 0x80...: break scan
|
|
case 0x0A: needsTransform = true
|
|
newlines += 1
|
|
lastLineStart = index
|
|
case 0x09: needsTransform = true
|
|
case 0x26: sawAmpersand = true
|
|
case ..<0x20: break scan
|
|
default: break
|
|
}
|
|
index += 1
|
|
}
|
|
guard index > byteIndex else { return nil }
|
|
let length = index - byteIndex
|
|
let run = attributeRunString(pointer, from: byteIndex, length: length, transform: needsTransform)
|
|
advanceByteRun(to: index, length: length, newlines: newlines, lastLineStart: lastLineStart)
|
|
return run
|
|
}
|
|
|
|
/// Builds the value string for an attribute byte run, applying the 3.3.3
|
|
/// tab/line-feed-to-space normalization only when the run contained one.
|
|
private func attributeRunString(_ pointer: UnsafePointer<UInt8>, from start: Int, length: Int, transform: Bool) -> String {
|
|
guard transform else {
|
|
return String(decoding: UnsafeBufferPointer(start: pointer + start, count: length), as: UTF8.self)
|
|
}
|
|
var bytes = [UInt8](repeating: 0, count: length)
|
|
for position in 0 ..< length {
|
|
let byte = pointer[start + position]
|
|
bytes[position] = (byte == 0x09 || byte == 0x0A) ? 0x20 : byte
|
|
}
|
|
return String(decoding: bytes, as: UTF8.self)
|
|
}
|
|
}
|