PureXML/Tests/EncodingTests.swift
Mihaela Mihaljevic 83e6e27d12 style(format): adopt --importgrouping testable-bottom from canonical rules-swift
Group @testable imports at the bottom, per public/rules-swift's swiftformat. A
behavior-preserving import-ordering reformat across 252 files (mostly tests).
--trailingclosures was deliberately not adopted (cosmetic, and swiftformat flags
it as caution-worthy).
2026-06-18 20:33:07 +02:00

304 lines
15 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Testing
@testable import PureXML
@Suite("Encoding")
struct EncodingTests {
@Test("Detects encodings from the leading bytes")
func test_detect() {
let detect = PureXML.Parsing.InputEncoding.detect
#expect(detect([0xEF, 0xBB, 0xBF, 0x3C]) == .utf8)
#expect(detect([0xFE, 0xFF, 0x00, 0x3C]) == .utf16BigEndian)
#expect(detect([0xFF, 0xFE, 0x3C, 0x00]) == .utf16LittleEndian)
#expect(detect([0x00, 0x3C, 0x00, 0x3F]) == .utf16BigEndian)
#expect(detect([0x3C, 0x00, 0x3F, 0x00]) == .utf16LittleEndian)
#expect(detect(Array("<r/>".utf8)) == .utf8)
}
@Test("Parses UTF-8 bytes, with and without a BOM")
func test_parseUTF8Bytes() throws {
let plain = try PureXML.parse(bytes: Array("<r>hi</r>".utf8))
#expect(rootText(plain) == "hi")
let withBOM = try PureXML.parse(bytes: [0xEF, 0xBB, 0xBF] + Array("<r>hi</r>".utf8))
#expect(rootText(withBOM) == "hi")
}
@Test("Parses UTF-16 bytes in both byte orders")
func test_parseUTF16Bytes() throws {
let bigEndianNode = try PureXML.parse(bytes: utf16("<r>hi</r>", bigEndian: true))
#expect(rootText(bigEndianNode) == "hi")
let littleEndianNode = try PureXML.parse(bytes: utf16("<r>hi</r>", bigEndian: false))
#expect(rootText(littleEndianNode) == "hi")
}
@Test("Decodes a UTF-8 byte stream incrementally (multi-byte scalars)")
func test_streamingUTF8() throws {
let node = try PureXML.parse(pullingBytes: byteSource(Array("<r>café \u{1F600}</r>".utf8)))
#expect(rootText(node) == "café \u{1F600}")
}
@Test("Decodes a UTF-16 byte stream incrementally, both byte orders")
func test_streamingUTF16() throws {
let bigEndianNode = try PureXML.parse(pullingBytes: byteSource(utf16("<r>héllo</r>", bigEndian: true)))
#expect(rootText(bigEndianNode) == "héllo")
let littleEndianNode = try PureXML.parse(pullingBytes: byteSource(utf16("<r>héllo</r>", bigEndian: false)))
#expect(rootText(littleEndianNode) == "héllo")
}
@Test("Streaming honors the declared encoding and agrees with whole-buffer decode")
func test_streamingDeclaredEncoding() throws {
func doc(_ encoding: String, _ content: [UInt8]) -> [UInt8] {
Array("<?xml version=\"1.0\" encoding=\"\(encoding)\"?><r>".utf8) + content + Array("</r>".utf8)
}
// Single-byte, CJK, and the stateful ISO-2022-JP, none detectable from
// bytes alone, so each exercises declaration-driven streaming.
struct Case { let encoding: String
let content: [UInt8]
let expected: String
}
let cases = [
Case(encoding: "ISO-8859-1", content: [0xE9], expected: "\u{00E9}"), // é
Case(encoding: "Shift_JIS", content: [0xB1], expected: "\u{FF71}"), // ア half-width katakana
Case(encoding: "ISO-2022-JP", content: [0x1B, 0x28, 0x49, 0x31, 0x1B, 0x28, 0x42], expected: "\u{FF71}"), // ア via escape
]
for testCase in cases {
let bytes = doc(testCase.encoding, testCase.content)
let whole = try rootText(PureXML.parse(bytes: bytes))
let streamed = try rootText(PureXML.parse(pullingBytes: byteSource(bytes)))
#expect(whole == testCase.expected, "whole-buffer wrong for \(testCase.encoding)")
#expect(streamed == whole, "streaming disagreed with whole-buffer for \(testCase.encoding)")
}
}
@Test("Odd-length UTF-16 input is rejected as malformed")
func test_malformedUTF16() {
#expect(throws: PureXML.Parsing.ParseError.malformedEncoding) {
_ = try PureXML.parse(bytes: [0xFE, 0xFF, 0x00])
}
}
@Test("Detects UTF-32 byte-order marks")
func test_detectUTF32() {
#expect(PureXML.Parsing.InputEncoding.detect([0x00, 0x00, 0xFE, 0xFF, 0x00]) == .utf32BigEndian)
#expect(PureXML.Parsing.InputEncoding.detect([0xFF, 0xFE, 0x00, 0x00, 0x00]) == .utf32LittleEndian)
}
@Test("Parses UTF-32 bytes in both byte orders")
func test_parseUTF32() throws {
try #expect(rootText(PureXML.parse(bytes: utf32("<r>hi</r>", bigEndian: true))) == "hi")
try #expect(rootText(PureXML.parse(bytes: utf32("<r>hi</r>", bigEndian: false))) == "hi")
}
@Test("Honors a declared ISO-8859-1 encoding")
func test_latin1() throws {
var bytes = Array("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf".utf8)
bytes.append(0xE9) // small letter e with acute, in Latin-1
bytes += Array("</r>".utf8)
try #expect(rootText(PureXML.parse(bytes: bytes)) == "caf\u{E9}")
}
@Test("Honors a declared Windows-1252 encoding")
func test_windows1252() throws {
var bytes = Array("<?xml version=\"1.0\" encoding=\"windows-1252\"?><r>".utf8)
bytes.append(0x80) // euro sign in CP1252
bytes += Array("</r>".utf8)
try #expect(rootText(PureXML.parse(bytes: bytes)) == "\u{20AC}")
}
private func decoded(_ encoding: String, _ high: [UInt8]) throws -> String? {
var bytes = Array("<?xml version=\"1.0\" encoding=\"\(encoding)\"?><r>".utf8)
bytes += high
bytes += Array("</r>".utf8)
return try rootText(PureXML.parse(bytes: bytes))
}
@Test("Decodes ISO-8859-15: euro and the Latin-9 substitutions")
func test_iso8859_15() throws {
try #expect(decoded("ISO-8859-15", [0xA4]) == "\u{20AC}") // €
try #expect(decoded("ISO-8859-15", [0xBD]) == "\u{0153}") // œ
try #expect(decoded("ISO-8859-15", [0xE9]) == "\u{E9}") // é, unchanged from Latin-1
}
@Test("Decodes ISO-8859-9: the Turkish letters")
func test_iso8859_9() throws {
try #expect(decoded("ISO-8859-9", [0xDD]) == "\u{0130}") // İ
try #expect(decoded("ISO-8859-9", [0xFE]) == "\u{015F}") // ş
try #expect(decoded("ISO-8859-9", [0xE9]) == "\u{E9}") // é, unchanged from Latin-1
}
@Test("Decodes ISO-8859-2 (Latin-2) from the vendored table")
func test_iso8859_2() throws {
try #expect(decoded("ISO-8859-2", [0xA1]) == "\u{0104}") // Ą
try #expect(decoded("ISO-8859-2", [0xE8]) == "\u{010D}") // č
try #expect(decoded("ISO-8859-2", [0xE9]) == "\u{E9}") // é
try #expect(decoded("ISO-8859-2", [0xFF]) == "\u{02D9}") // ˙
}
@Test("Decodes the vendored ISO-8859 Latin and Greek tables")
func test_vendoredTables() throws {
try #expect(decoded("ISO-8859-3", [0xA1]) == "\u{0126}") // Ħ
try #expect(decoded("ISO-8859-3", [0xC6]) == "\u{0108}") // Ĉ
try #expect(decoded("ISO-8859-4", [0xA1]) == "\u{0104}") // Ą
try #expect(decoded("ISO-8859-4", [0xC0]) == "\u{0100}") // Ā
try #expect(decoded("ISO-8859-7", [0xC1]) == "\u{0391}") // Α
try #expect(decoded("ISO-8859-7", [0xE1]) == "\u{03B1}") // α
try #expect(decoded("ISO-8859-7", [0xA4]) == "\u{20AC}") // €
try #expect(decoded("ISO-8859-13", [0xA8]) == "\u{00D8}") // Ø
try #expect(decoded("ISO-8859-13", [0xC0]) == "\u{0104}") // Ą
try #expect(decoded("ISO-8859-6", [0xC7]) == "\u{0627}") // ا (Arabic alef)
try #expect(decoded("ISO-8859-8", [0xE0]) == "\u{05D0}") // א (Hebrew alef)
try #expect(decoded("ISO-8859-10", [0xFF]) == "\u{0138}") // ĸ
try #expect(decoded("ISO-8859-14", [0xA1]) == "\u{1E02}") // Ḃ
try #expect(decoded("ISO-8859-16", [0xAA]) == "\u{0218}") // Ș
}
@Test("Decodes the Windows code pages, KOI8-R, and Thai from vendored tables")
func test_windowsAndKoi8() throws {
try #expect(decoded("ISO-8859-11", [0xA1]) == "\u{0E01}") // ก
try #expect(decoded("windows-1250", [0x8A]) == "\u{0160}") // Š
try #expect(decoded("windows-1250", [0xC8]) == "\u{010C}") // Č
try #expect(decoded("windows-1251", [0xC0]) == "\u{0410}") // А
try #expect(decoded("windows-1251", [0x88]) == "\u{20AC}") // €
try #expect(decoded("windows-1253", [0xC1]) == "\u{0391}") // Α
try #expect(decoded("windows-1257", [0xA8]) == "\u{00D8}") // Ø
try #expect(decoded("koi8-r", [0xE1]) == "\u{0410}") // А
try #expect(decoded("koi8-r", [0xC1]) == "\u{0430}") // а
try #expect(decoded("koi8-r", [0xA3]) == "\u{0451}") // ё
try #expect(decoded("windows-1255", [0xE0]) == "\u{05D0}") // א
try #expect(decoded("windows-1255", [0xA4]) == "\u{20AA}") // ₪
try #expect(decoded("windows-1256", [0xC7]) == "\u{0627}") // ا
try #expect(decoded("windows-1258", [0xFE]) == "\u{20AB}") // ₫
try #expect(decoded("koi8-u", [0xA4]) == "\u{0454}") // є (KOI8-U specific)
}
@Test("Decodes Shift-JIS: kanji, ASCII, and half-width katakana")
func test_shiftJIS() throws {
// 日 (0x93FA), 本 (0x967B), A (0x41), half-width katakana ア (0xB1).
try #expect(decoded("Shift_JIS", [0x93, 0xFA, 0x96, 0x7B, 0x41, 0xB1]) == "\u{65E5}\u{672C}A\u{FF71}")
}
@Test("Decodes EUC-JP: JIS X 0208 and the 0x8E half-width katakana lead")
func test_eucJP() throws {
// 日 (0xC6FC), 本 (0xCBDC), A (0x41), 0x8E + 0xB1 = half-width katakana ア.
try #expect(decoded("EUC-JP", [0xC6, 0xFC, 0xCB, 0xDC, 0x41, 0x8E, 0xB1]) == "\u{65E5}\u{672C}A\u{FF71}")
}
@Test("Decodes EUC-KR (CP949): Hangul syllables")
func test_eucKR() throws {
// 한 (0xC7D1), 가 (0xB0A1), A (0x41).
try #expect(decoded("EUC-KR", [0xC7, 0xD1, 0xB0, 0xA1, 0x41]) == "\u{D55C}\u{AC00}A")
}
@Test("Decodes GBK/GB2312: Chinese characters, ASCII, and the 0x80 euro")
func test_gbk() throws {
// 中 (0xD6D0), 文 (0xCEC4), A (0x41), 0x80 = euro.
try #expect(decoded("GBK", [0xD6, 0xD0, 0xCE, 0xC4, 0x41, 0x80]) == "\u{4E2D}\u{6587}A\u{20AC}")
try #expect(decoded("gb2312", [0xD6, 0xD0]) == "\u{4E2D}") // GB2312 alias
}
@Test("Decodes GB18030: two-byte pairs plus the four-byte and astral ranges")
func test_gb18030() throws {
// 中 (0xD6D0) shares the GBK two-byte range; 0x80 = euro.
try #expect(decoded("GB18030", [0xD6, 0xD0, 0x80]) == "\u{4E2D}\u{20AC}")
// Four-byte sequences: ptr 36 = ¥ (U+00A5), ptr 0 = U+0080.
try #expect(decoded("gb18030", [0x81, 0x30, 0x84, 0x36]) == "\u{00A5}")
try #expect(decoded("gb-18030", [0x81, 0x30, 0x81, 0x30]) == "\u{0080}")
// The astral ranges: ptr 189000 -> U+10000, ptr 254536 -> U+20000.
try #expect(decoded("GB18030", [0x90, 0x30, 0x81, 0x30]) == "\u{10000}")
try #expect(decoded("GB18030", [0x95, 0x32, 0x82, 0x36]) == "\u{20000}")
}
@Test("Decodes Big5: BMP ideographs, an astral HKSCS pair, and a combining sequence")
func test_big5() throws {
// 中 (0xA4A4), 文 (0xA4E5), A (0x41).
try #expect(decoded("Big5", [0xA4, 0xA4, 0xA4, 0xE5, 0x41]) == "\u{4E2D}\u{6587}A")
// An astral HKSCS ideograph (pointer 947 -> U+27267).
try #expect(decoded("big5-hkscs", [0x87, 0x45]) == "\u{27267}")
// A two-codepoint pointer (1133 -> U+00CA U+0304, the base letter plus a macron).
try #expect(decoded("csbig5", [0x88, 0x62]) == "\u{00CA}\u{0304}")
}
@Test("Decodes ISO-2022-JP: ASCII, JIS X 0201 Roman and katakana, and JIS X 0208")
func test_iso2022jp() throws {
let esc: UInt8 = 0x1B
// JIS X 0201 Roman: 0x5C is the yen sign, 0x7E the overline.
try #expect(decoded("ISO-2022-JP", [esc, 0x28, 0x4A, 0x5C, 0x7E, esc, 0x28, 0x42]) == "\u{00A5}\u{203E}")
// JIS X 0201 katakana: 0x31 -> U+FF71 (ア).
try #expect(decoded("iso-2022-jp", [esc, 0x28, 0x49, 0x31, esc, 0x28, 0x42]) == "\u{FF71}")
// JIS X 0208: 0x46 0x7C shares pointer 3569 with EUC-JP 0xC6 0xFC, the kanji 日.
let kanji = try decoded("ISO-2022-JP", [esc, 0x24, 0x42, 0x46, 0x7C, esc, 0x28, 0x42])
let viaEUCJP = try decoded("EUC-JP", [0xC6, 0xFC])
#expect(kanji == viaEUCJP)
#expect(kanji == "\u{65E5}")
// ASCII passes through, and mode resets across an escape.
try #expect(decoded("ISO-2022-JP", [0x48, 0x69]) == "Hi")
}
@Test("Decodes EUC-TW (CNS 11643): plane 1 two-byte and four-byte, and plane 2")
func test_eucTW() throws {
// § (U+00A7) in plane 1, as a two-byte pair and as the four-byte plane-1 form.
try #expect(decoded("EUC-TW", [0xA1, 0xF0]) == "\u{00A7}")
try #expect(decoded("euc-tw", [0x8E, 0xA1, 0xA1, 0xF0]) == "\u{00A7}")
// 万 (U+4E07) in plane 2, four-byte.
try #expect(decoded("EUC-TW", [0x8E, 0xA2, 0xA1, 0xA6]) == "\u{4E07}")
// Streaming agrees with whole-buffer for the four-byte plane-2 form.
let bytes = Array("<?xml version=\"1.0\" encoding=\"EUC-TW\"?><r>".utf8) + [0x8E, 0xA2, 0xA1, 0xA6] + Array("</r>".utf8)
try #expect(rootText(PureXML.parse(pullingBytes: byteSource(bytes))) == "\u{4E07}")
}
@Test("Decodes ISO-8859-5: the Cyrillic block")
func test_iso8859_5() throws {
try #expect(decoded("ISO-8859-5", [0xB0]) == "\u{0410}") // А
try #expect(decoded("ISO-8859-5", [0xE0]) == "\u{0440}") // р
try #expect(decoded("ISO-8859-5", [0xF0]) == "\u{2116}") // №
}
@Test("Decodes Windows-1254: CP1252 punctuation plus the Turkish letters")
func test_windows1254() throws {
try #expect(decoded("windows-1254", [0x80]) == "\u{20AC}") // € (from CP1252 high range)
try #expect(decoded("windows-1254", [0xDE]) == "\u{015E}") // Ş
try #expect(decoded("windows-1254", [0xE9]) == "\u{E9}") // é, unchanged
}
private func byteSource(_ bytes: [UInt8]) -> () -> UInt8? {
var index = 0
return {
guard index < bytes.count else { return nil }
defer { index += 1 }
return bytes[index]
}
}
private func utf16(_ string: String, bigEndian: Bool) -> [UInt8] {
var bytes: [UInt8] = bigEndian ? [0xFE, 0xFF] : [0xFF, 0xFE]
for unit in string.utf16 {
let high = UInt8(unit >> 8)
let low = UInt8(unit & 0xFF)
bytes += bigEndian ? [high, low] : [low, high]
}
return bytes
}
private func utf32(_ string: String, bigEndian: Bool) -> [UInt8] {
var bytes: [UInt8] = bigEndian ? [0x00, 0x00, 0xFE, 0xFF] : [0xFF, 0xFE, 0x00, 0x00]
for scalar in string.unicodeScalars {
let value = scalar.value
let bigEndianBytes = [
UInt8(value >> 24 & 0xFF),
UInt8(value >> 16 & 0xFF),
UInt8(value >> 8 & 0xFF),
UInt8(value & 0xFF),
]
bytes += bigEndian ? bigEndianBytes : bigEndianBytes.reversed()
}
return bytes
}
private func rootText(_ node: PureXML.Model.Node) -> String? {
guard case let .document(children) = node else { return nil }
for child in children {
if case let .element(element) = child { return element.text }
}
return nil
}
}