PureXML/Sources/Parsing/CJKByteEncoder.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
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.
2026-07-12 14:12:17 +02:00

129 lines
5.0 KiB
Swift

extension PureXML.Parsing.ByteEncoder {
private typealias Decoder = PureXML.Parsing.ByteDecoder
/// The bytes of `string` in a multi-byte CJK encoding, or nil for a
/// non-CJK encoding. Each inverse is built by running the existing,
/// tested whole-buffer decoder over every valid byte sequence and
/// recording the shortest sequence per scalar, so the encoder is by
/// construction the exact inverse of the decoder.
static func cjkEncode(_ string: String, as encoding: PureXML.Parsing.InputEncoding) -> [UInt8]? {
let inverse: [UInt32: [UInt8]]
switch encoding {
case .shiftJIS: inverse = shiftJISInverse
case .eucJP: inverse = eucJPInverse
case .eucKR: inverse = eucKRInverse
case .gbk: inverse = gbkInverse
case .big5: inverse = big5Inverse
case .gb18030: return gb18030Encode(string)
default: return nil
}
return mapped(string, through: inverse)
}
static func supportsCJK(_ encoding: PureXML.Parsing.InputEncoding) -> Bool {
switch encoding {
case .shiftJIS, .eucJP, .eucKR, .gbk, .big5, .gb18030: true
default: false
}
}
private static func mapped(_ string: String, through inverse: [UInt32: [UInt8]]) -> [UInt8] {
var bytes: [UInt8] = []
for scalar in string.unicodeScalars {
if scalar.value < 0x80 {
bytes.append(UInt8(scalar.value))
} else if let sequence = inverse[scalar.value] {
bytes.append(contentsOf: sequence)
} else {
bytes.append(contentsOf: Array("&#\(scalar.value);".utf8))
}
}
return bytes
}
// MARK: GB18030 (two-byte plus the four-byte ranges, a full Unicode encoding)
private static func gb18030Encode(_ string: String) -> [UInt8] {
var bytes: [UInt8] = []
for scalar in string.unicodeScalars {
if scalar.value < 0x80 {
bytes.append(UInt8(scalar.value))
} else if let sequence = gbkInverse[scalar.value] {
bytes.append(contentsOf: sequence)
} else {
bytes.append(contentsOf: gb18030FourBytes(scalar.value))
}
}
return bytes
}
/// The four-byte GB18030 sequence for a scalar, the inverse of the decoder's
/// pointer-to-scalar range mapping.
private static func gb18030FourBytes(_ codepoint: UInt32) -> [UInt8] {
let pointers = Decoder.gb18030RangePointers
let codepoints = Decoder.gb18030RangeCodePoints
var low = 0
var high = codepoints.count - 1
var found = 0
while low <= high {
let mid = (low + high) / 2
if codepoints[mid] <= codepoint {
found = mid
low = mid + 1
} else {
high = mid - 1
}
}
let pointer = Int(pointers[found]) + Int(codepoint - codepoints[found])
let byte1 = pointer / 12600
var rest = pointer % 12600
let byte2 = rest / 1260
rest %= 1260
let byte3 = rest / 10
let byte4 = rest % 10
return [UInt8(byte1 + 0x81), UInt8(byte2 + 0x30), UInt8(byte3 + 0x81), UInt8(byte4 + 0x30)]
}
// MARK: Inverse tables, built once from the decoders
private static let shiftJISInverse = buildInverse(Decoder.ShiftJIS.decode, leads: 0x81 ... 0xFE)
private static let eucKRInverse = buildInverse(Decoder.EUCKR.decode, leads: 0x81 ... 0xFE)
private static let gbkInverse = buildInverse(Decoder.GBK.decode, leads: 0x81 ... 0xFE)
private static let big5Inverse = buildInverse(Decoder.Big5.decode, leads: 0x81 ... 0xFE)
private static let eucJPInverse = buildInverse(Decoder.EUCJP.decode, leads: 0x8E ... 0xFE, threeByteLead: 0x8F)
/// Enumerates every one-, two-, and (for EUC-JP) three-byte sequence the
/// decoder accepts and records the scalar it yields. The first (shortest,
/// lowest) sequence wins a scalar so the encoder stays deterministic.
private static func buildInverse(
_ decode: (ArraySlice<UInt8>) -> String,
leads: ClosedRange<Int>,
threeByteLead: UInt8? = nil,
) -> [UInt32: [UInt8]] {
var map: [UInt32: [UInt8]] = [:]
func record(_ bytes: [UInt8]) {
let scalars = Array(decode(bytes[...]).unicodeScalars)
guard scalars.count == 1, scalars[0].value != 0xFFFD, scalars[0].value >= 0x80 else { return }
if map[scalars[0].value] == nil {
map[scalars[0].value] = bytes
}
}
for byte in 0x80 ... 0xFF {
record([UInt8(byte)])
}
for lead in leads {
for trail in 0x40 ... 0xFE {
record([UInt8(lead), UInt8(trail)])
}
}
if let threeByteLead {
for middle in 0xA1 ... 0xFE {
for trail in 0xA1 ... 0xFE {
record([threeByteLead, UInt8(middle), UInt8(trail)])
}
}
}
return map
}
}