The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass.
110 lines
5.2 KiB
Swift
110 lines
5.2 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)
|
|
}
|
|
}
|