PureXML/Sources/Parsing/ISO2022JPDecoder.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

106 lines
4.3 KiB
Swift

extension PureXML.Parsing.ByteDecoder {
/// The ISO-2022-JP decoder (the WHATWG `iso-2022-jp` algorithm). Unlike the
/// other CJK encodings this one is stateful: escape sequences switch between
/// ASCII, JIS X 0201 Roman, JIS X 0201 katakana, and the JIS X 0208 plane,
/// which is resolved through the shared `jis0208` table (no new data).
enum ISO2022JP {
static func decode(_ bytes: ArraySlice<UInt8>) -> String {
var scalars = String.UnicodeScalarView()
var mode: ISO2022JPMode = .ascii
var lead: UInt8?
var index = bytes.startIndex
while index < bytes.endIndex {
let byte = bytes[index]
index += 1
if byte == 0x1B {
mode = applyEscape(bytes, &index, into: &scalars) ?? mode
lead = nil
continue
}
decodeByte(byte, mode: mode, lead: &lead, into: &scalars)
}
if lead != nil { scalars.append("\u{FFFD}") }
return String(scalars)
}
/// Reads the bytes after an escape and returns the new mode, or nil (with a
/// replacement emitted) for an unrecognized sequence.
private static func applyEscape(_ bytes: ArraySlice<UInt8>, _ index: inout Int, into scalars: inout String.UnicodeScalarView) -> ISO2022JPMode? {
guard index < bytes.endIndex else { scalars.append("\u{FFFD}")
return nil
}
let first = bytes[index]
index += 1
guard index < bytes.endIndex else { scalars.append("\u{FFFD}")
return nil
}
let second = bytes[index]
index += 1
switch (first, second) {
case (0x28, 0x42): return .ascii // ESC ( B
case (0x28, 0x4A): return .roman // ESC ( J
case (0x28, 0x49): return .katakana // ESC ( I
case (0x24, 0x40), (0x24, 0x42): return .jis0208 // ESC $ @ / ESC $ B
default:
scalars.append("\u{FFFD}")
return nil
}
}
private static func decodeByte(_ byte: UInt8, mode: ISO2022JPMode, lead: inout UInt8?, into scalars: inout String.UnicodeScalarView) {
switch mode {
case .ascii:
scalars.append(byte <= 0x7F ? Unicode.Scalar(byte) : "\u{FFFD}")
case .roman:
switch byte {
case 0x5C: scalars.append("\u{00A5}") // yen
case 0x7E: scalars.append("\u{203E}") // overline
case 0 ... 0x7F: scalars.append(Unicode.Scalar(byte))
default: scalars.append("\u{FFFD}")
}
case .katakana:
if (0x21 ... 0x5F).contains(byte) {
scalars.append(Unicode.Scalar(0xFF61 + UInt32(byte) - 0x21) ?? "\u{FFFD}")
} else {
scalars.append("\u{FFFD}")
}
case .jis0208:
decodeJIS0208(byte, lead: &lead, into: &scalars)
}
}
private static func decodeJIS0208(_ byte: UInt8, lead: inout UInt8?, into scalars: inout String.UnicodeScalarView) {
guard let first = lead else {
if (0x21 ... 0x7E).contains(byte) {
lead = byte
} else if byte == 0x0A || byte == 0x0D {
scalars.append(Unicode.Scalar(byte))
} else {
scalars.append("\u{FFFD}")
}
return
}
lead = nil
guard (0x21 ... 0x7E).contains(first), (0x21 ... 0x7E).contains(byte) else {
scalars.append("\u{FFFD}")
return
}
let pointer = (Int(first) - 0x21) * 94 + Int(byte) - 0x21
if pointer >= 0, pointer < jis0208.count, jis0208[pointer] != 0xFFFF {
scalars.append(Unicode.Scalar(UInt32(jis0208[pointer])) ?? "\u{FFFD}")
} else {
scalars.append("\u{FFFD}")
}
}
}
}
/// The active character set while decoding ISO-2022-JP, switched by escape
/// sequences. Held by the streaming decoder across reads, so it is module-visible.
enum ISO2022JPMode {
case ascii
case roman
case katakana
case jis0208
}