PureXML/Sources/Parsing/GB18030Decoder.swift
Mihaela Mihaljevic 4130d905c8 refactor(parsing): drop slice copies; document the expansion bounds
The whole-buffer ISO-2022-JP, GB18030, and EUC-TW decoders index the input
ArraySlice directly instead of materializing an Array first (one avoidable
O(n) allocation per decode each); declaredEncoding's subsequence scan uses
elementsEqual instead of allocating an Array per candidate offset. Document
the two analysis-flagged bounds: entity expansion is doubly bounded (the
character budget caps output; the visiting set caps recursion depth at the
number of declared entities), and the RELAX NG derivative engine carries no
memoization by design (the simplifying constructors keep real grammars
small; adversarial nested-choice grammars can grow the residual, so treat
untrusted schemas accordingly). Behavior unchanged, all suites green.

Closes #118
2026-06-10 04:01:16 +02:00

94 lines
3.8 KiB
Swift

extension PureXML.Parsing.ByteDecoder {
/// The GB18030 decoder (the WHATWG `gb18030` algorithm). It extends GBK with
/// the four-byte sequences: a lead in `0x81`-`0xFE` followed by a digit
/// (`0x30`-`0x39`) opens a four-byte form whose pointer is mapped through the
/// linear ranges, reaching the astral planes. Two-byte pairs resolve through
/// the shared GBK index.
enum GB18030 {
static func decode(_ bytes: ArraySlice<UInt8>) -> String {
var scalars = String.UnicodeScalarView()
var index = bytes.startIndex
while index < bytes.endIndex {
let byte = bytes[index]
if byte <= 0x7F {
scalars.append(Unicode.Scalar(byte))
index += 1
} else if byte == 0x80 {
scalars.append("\u{20AC}") // euro
index += 1
} else if (0x81 ... 0xFE).contains(byte) {
index += step(bytes, index, byte, into: &scalars)
} else {
scalars.append("\u{FFFD}")
index += 1
}
}
return String(scalars)
}
/// Consumes one sequence starting at a lead byte and returns how many bytes
/// it advanced. A digit second byte opens the four-byte form; anything else
/// is a two-byte pair resolved through the GBK index.
private static func step(
_ bytes: ArraySlice<UInt8>,
_ index: Int,
_ lead: UInt8,
into scalars: inout String.UnicodeScalarView,
) -> Int {
guard index + 1 < bytes.endIndex else {
scalars.append("\u{FFFD}")
return 1
}
let second = bytes[index + 1]
if (0x30 ... 0x39).contains(second) {
guard index + 3 < bytes.endIndex,
(0x81 ... 0xFE).contains(bytes[index + 2]),
(0x30 ... 0x39).contains(bytes[index + 3])
else {
scalars.append("\u{FFFD}")
return 1
}
let pointer = fourBytePointer(lead, second, bytes[index + 2], bytes[index + 3])
scalars.append(rangeScalar(pointer) ?? "\u{FFFD}")
return 4
}
if let scalar = GBK.twoByteScalar(lead: lead, trail: second) {
scalars.append(scalar)
return 2
}
scalars.append("\u{FFFD}")
return second <= 0x7F ? 1 : 2
}
/// The four-byte pointer from a `(b1, b2, b3, b4)` sequence.
static func fourBytePointer(_ byte1: UInt8, _ byte2: UInt8, _ byte3: UInt8, _ byte4: UInt8) -> Int {
(Int(byte1) - 0x81) * 12600
+ (Int(byte2) - 0x30) * 1260
+ (Int(byte3) - 0x81) * 10
+ (Int(byte4) - 0x30)
}
/// The scalar for a four-byte pointer through the linear ranges, or nil for
/// the unmapped gap and the out-of-range tail.
static func rangeScalar(_ pointer: Int) -> Unicode.Scalar? {
if pointer > 39419, pointer < 189_000 { return nil }
if pointer > 1_237_575 { return nil }
let target = UInt32(pointer)
var low = 0
var high = gb18030RangePointers.count - 1
var found = 0
while low <= high {
let mid = (low + high) / 2
if gb18030RangePointers[mid] <= target {
found = mid
low = mid + 1
} else {
high = mid - 1
}
}
let code = gb18030RangeCodePoints[found] + (target - gb18030RangePointers[found])
return Unicode.Scalar(code)
}
}
}