|
| 1 | +// |
| 2 | +// FileTextLoaderTests.swift |
| 3 | +// TableProTests |
| 4 | +// |
| 5 | + |
| 6 | +import Foundation |
| 7 | +@testable import TablePro |
| 8 | +import Testing |
| 9 | + |
| 10 | +@Suite("File text loader") |
| 11 | +struct FileTextLoaderTests { |
| 12 | + private static let headerLength = 4_096 |
| 13 | + private static let reportedName = "B\u{E1}o c\u{E1}o doanh thu" |
| 14 | + |
| 15 | + private func withFile<T>(_ bytes: Data, _ body: (URL) -> T) throws -> T { |
| 16 | + let url = FileManager.default.temporaryDirectory |
| 17 | + .appendingPathComponent("FileTextLoaderTests-\(UUID().uuidString).sql") |
| 18 | + try bytes.write(to: url) |
| 19 | + defer { try? FileManager.default.removeItem(at: url) } |
| 20 | + return body(url) |
| 21 | + } |
| 22 | + |
| 23 | + private func loadHeader(of bytes: Data) throws -> FileTextLoader.LoadedText? { |
| 24 | + try withFile(bytes) { FileTextLoader.loadHeader($0) } |
| 25 | + } |
| 26 | + |
| 27 | + private func load(_ bytes: Data) throws -> FileTextLoader.LoadedText? { |
| 28 | + try withFile(bytes) { FileTextLoader.load($0) } |
| 29 | + } |
| 30 | + |
| 31 | + private func utf8File(named name: String, straddling character: String, bytesInsideHeader: Int) -> Data { |
| 32 | + var bytes = Data("-- @name: \(name)\nSELECT '".utf8) |
| 33 | + let paddingLength = Self.headerLength - bytesInsideHeader - bytes.count |
| 34 | + bytes.append(Data(repeating: UInt8(ascii: "x"), count: paddingLength)) |
| 35 | + bytes.append(Data(character.utf8)) |
| 36 | + bytes.append(Data("';\n".utf8)) |
| 37 | + return bytes |
| 38 | + } |
| 39 | + |
| 40 | + @Test("A UTF-8 file with a character across the header limit keeps its name and encoding") |
| 41 | + func keepsUTF8WhenACharacterStraddlesTheLimit() throws { |
| 42 | + let bytes = utf8File(named: Self.reportedName, straddling: "\u{1EC7}", bytesInsideHeader: 2) |
| 43 | + #expect(bytes.count > Self.headerLength) |
| 44 | + #expect(String(data: bytes.prefix(Self.headerLength), encoding: .utf8) == nil) |
| 45 | + |
| 46 | + let header = try #require(try loadHeader(of: bytes)) |
| 47 | + |
| 48 | + #expect(header.encoding == .utf8) |
| 49 | + #expect(SQLFrontmatter.parse(header.content).name == Self.reportedName) |
| 50 | + } |
| 51 | + |
| 52 | + @Test("A genuinely Latin-1 file is still read as Latin-1") |
| 53 | + func readsALatin1FileAsLatin1() throws { |
| 54 | + let bytes = try #require("-- @name: Caf\u{E9} cr\u{E8}me\nSELECT 1;\n".data(using: .isoLatin1)) |
| 55 | + |
| 56 | + let header = try #require(try loadHeader(of: bytes)) |
| 57 | + |
| 58 | + #expect(header.encoding == .isoLatin1) |
| 59 | + #expect(SQLFrontmatter.parse(header.content).name == "Caf\u{E9} cr\u{E8}me") |
| 60 | + } |
| 61 | + |
| 62 | + @Test("A UTF-16 file with a byte order mark is read as UTF-16") |
| 63 | + func readsAUTF16FileAsUTF16() throws { |
| 64 | + let text = try #require("-- @name: \(Self.reportedName)\nSELECT 1;\n".data(using: .utf16LittleEndian)) |
| 65 | + |
| 66 | + let header = try #require(try loadHeader(of: Data([0xFF, 0xFE]) + text)) |
| 67 | + |
| 68 | + #expect(header.encoding == .utf16) |
| 69 | + #expect(SQLFrontmatter.parse(header.content).name == Self.reportedName) |
| 70 | + } |
| 71 | + |
| 72 | + @Test("A file shorter than the header limit is read whole") |
| 73 | + func readsAShortFileWhole() throws { |
| 74 | + let text = "-- @name: \(Self.reportedName)\nSELECT 1;\n" |
| 75 | + |
| 76 | + let header = try #require(try loadHeader(of: Data(text.utf8))) |
| 77 | + |
| 78 | + #expect(header.encoding == .utf8) |
| 79 | + #expect(header.content == text) |
| 80 | + } |
| 81 | + |
| 82 | + @Test("A UTF-32 big-endian file loads as UTF-32") |
| 83 | + func loadsBigEndianUTF32() throws { |
| 84 | + let text = "-- @name: \(Self.reportedName)\nSELECT 1;\n" |
| 85 | + let bytes = try Data([0x00, 0x00, 0xFE, 0xFF]) + #require(text.data(using: .utf32BigEndian)) |
| 86 | + |
| 87 | + let loaded = try #require(try load(bytes)) |
| 88 | + |
| 89 | + #expect(loaded.encoding == .utf32) |
| 90 | + #expect(loaded.content == text) |
| 91 | + } |
| 92 | + |
| 93 | + @Test("A byte-order-marked file cut partway through a code unit keeps every byte") |
| 94 | + func keepsEveryByteOfACutMarkedFile() throws { |
| 95 | + let utf16 = try Data([0xFF, 0xFE]) + #require("ab".data(using: .utf16LittleEndian)) + Data([0x41]) |
| 96 | + let utf32 = try Data([0xFF, 0xFE, 0x00, 0x00]) + #require("ab".data(using: .utf32LittleEndian)).dropLast(2) |
| 97 | + |
| 98 | + for bytes in [utf16, utf32] { |
| 99 | + let loaded = try #require(try load(bytes)) |
| 100 | + #expect(loaded.encoding == .isoLatin1) |
| 101 | + #expect(loaded.content == String(data: bytes, encoding: .isoLatin1)) |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + @Test("A byte order mark outranks a text encoding attribute that names another encoding") |
| 106 | + func byteOrderMarkOutranksTheEncodingAttribute() throws { |
| 107 | + let text = "-- @name: \(Self.reportedName)\n" |
| 108 | + let bytes = try Data([0xFF, 0xFE]) + #require(text.data(using: .utf16LittleEndian)) |
| 109 | + let attribute = Array("MACINTOSH;0".utf8) |
| 110 | + |
| 111 | + let result = try withFile(bytes) { url in |
| 112 | + ( |
| 113 | + status: setxattr(url.path, "com.apple.TextEncoding", attribute, attribute.count, 0, 0), |
| 114 | + loaded: FileTextLoader.load(url) |
| 115 | + ) |
| 116 | + } |
| 117 | + |
| 118 | + #expect(result.status == 0) |
| 119 | + let loaded = try #require(result.loaded) |
| 120 | + #expect(loaded.encoding == .utf16) |
| 121 | + #expect(loaded.content == text) |
| 122 | + } |
| 123 | + |
| 124 | + @Test("The header is the start of what loading the whole file reads, in the same encoding") |
| 125 | + func agreesWithFullLoading() throws { |
| 126 | + let text = "-- @name: \(Self.reportedName)\n" |
| 127 | + let latin1 = try #require("-- @name: Caf\u{E9}\nSELECT 1;\n".data(using: .isoLatin1)) |
| 128 | + let utf16LittleEndian = try Data([0xFF, 0xFE]) + #require(text.data(using: .utf16LittleEndian)) |
| 129 | + let utf16BigEndian = try Data([0xFE, 0xFF]) + #require(text.data(using: .utf16BigEndian)) |
| 130 | + let utf32LittleEndian = try Data([0xFF, 0xFE, 0x00, 0x00]) + #require(text.data(using: .utf32LittleEndian)) |
| 131 | + let utf32BigEndian = try Data([0x00, 0x00, 0xFE, 0xFF]) + #require(text.data(using: .utf32BigEndian)) |
| 132 | + let files = [ |
| 133 | + utf8File(named: Self.reportedName, straddling: "\u{E1}", bytesInsideHeader: 1), |
| 134 | + utf8File(named: Self.reportedName, straddling: "\u{1EC7}", bytesInsideHeader: 1), |
| 135 | + utf8File(named: Self.reportedName, straddling: "\u{1F600}", bytesInsideHeader: 3), |
| 136 | + latin1, |
| 137 | + utf16LittleEndian, |
| 138 | + utf16BigEndian, |
| 139 | + utf32LittleEndian, |
| 140 | + utf32BigEndian, |
| 141 | + utf16LittleEndian + Data([0x41]), |
| 142 | + utf32LittleEndian + Data([0x41, 0x00]) |
| 143 | + ] |
| 144 | + |
| 145 | + for (index, bytes) in files.enumerated() { |
| 146 | + let loaded = try withFile(bytes) { url in |
| 147 | + (header: FileTextLoader.loadHeader(url), whole: FileTextLoader.load(url)) |
| 148 | + } |
| 149 | + let header = try #require(loaded.header, "file \(index)") |
| 150 | + let whole = try #require(loaded.whole, "file \(index)") |
| 151 | + #expect(header.encoding == whole.encoding, "file \(index)") |
| 152 | + #expect(whole.content.unicodeScalars.starts(with: header.content.unicodeScalars), "file \(index)") |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + @Test("An empty file has no header") |
| 157 | + func returnsNothingForAnEmptyFile() throws { |
| 158 | + let header = try loadHeader(of: Data()) |
| 159 | + |
| 160 | + #expect(header == nil) |
| 161 | + } |
| 162 | +} |
0 commit comments