PureXML/Tests/EncodingTests.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

306 lines
15 KiB
Swift
Raw 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
}
}