雲端 Mac 上的 iOS 十六進位編解碼契約測試
在 Windows 上完成業務程式碼後,將 iOS 專案移到雲端 Mac 建置封裝,介面中的十六進位欄位也需要隨程式碼一併驗收。裝置訊息、摘要和二進位識別碼經常採用這種表示方式:字串看起來正常,不代表解碼後的位元組正確。尤其要避免忽略無效字元、遺失前導零,以及自動補齊不完整的輸入,讓它變成另一筆資料。
本文先用獨立的 Swift 腳本建立位元組契約,再說明如何整合至專案測試。不依賴網路服務,也不需要簽署憑證等資料,適合在封存前執行。
先確定輸入與輸出契約
解碼器只接受 ASCII 的 0–9、a–f、A–F,每兩個字元代表一個位元組;編碼器統一輸出小寫,每個位元組固定使用兩位。大小寫可視為等價,但空格、換行、0x 前綴與全形字元都不屬於這項協定。
空字串在底層代表空資料。如果業務欄位不允許空值,應由業務驗證拒絕,不要讓通用解碼器暗中承擔欄位規則。輸入上限以解碼後的位元組數計算,並明確規定包含邊界值。
嚴格解碼與人工輸入整理是兩個不同層次的職責。如果需要支援貼上含有空格的資料,應先在介面調適層明確轉換,再交給嚴格解碼器;機器介面不應預設自動修正輸入。
同樣地,十六進位只是一種位元組表示方式,不提供加密、完整性保護或來源驗證。通過解碼驗收,不代表訊息就值得信任。
實作遇錯即拒的解碼與固定寬度編碼
將以下內容儲存為 HexContract.swift。這份實作會先檢查長度與位元組額度,再配置輸出空間;接著逐位元組判斷字元,避免使用會略過轉換失敗項目的 compactMap。範例定義了獨立的錯誤型別,方便測試區分失敗原因。
import Foundation
enum HexError: Error, Equatable {
case invalidLimit
case oddLength
case tooLarge
case invalidCharacter
}
func nibble(_ c: UInt8) throws -> UInt8 {
switch c {
case 48...57: return c - 48
case 65...70: return c - 65 + 10
case 97...102: return c - 97 + 10
default: throw HexError.invalidCharacter
}
}
func decodeHex(_ text: String, maxBytes: Int) throws -> Data {
guard maxBytes >= 0 else { throw HexError.invalidLimit }
let count = text.utf8.count
guard count % 2 == 0 else { throw HexError.oddLength }
guard count / 2 <= maxBytes else { throw HexError.tooLarge }
var iterator = text.utf8.makeIterator()
var output = Data()
output.reserveCapacity(count / 2)
while let high = iterator.next() {
guard let low = iterator.next() else {
throw HexError.oddLength
}
let a = try nibble(high)
let b = try nibble(low)
output.append((a << 4) | b)
}
return output
}
func encodeHex(_ data: Data) -> String {
let alphabet = Array("0123456789abcdef".utf8)
var output = [UInt8]()
for byte in data {
output.append(alphabet[Int(byte >> 4)])
output.append(alphabet[Int(byte & 15)])
}
return String(decoding: output, as: UTF8.self)
}
這裡使用 UTF-8 位元組數,而不是使用者看到的字元數;非 ASCII 輸入最終都必須失敗。錯誤判定順序也是契約的一部分:優先檢查負額度,其次是奇數長度,再來是容量,最後才檢查允許的字元集。
上限檢查不會計算 maxBytes * 2,因此不會因額度乘法而溢位。它控制的是輸出空間配置,而不是請求接收過程;介面仍須在讀取請求主體時限制大小,不能先接收無上限的字串,再指望解碼器節省記憶體。
加入固定測試向量與拒絕案例
將以下測試接在同一個檔案後面。固定測試向量用來檢查前導零與大小寫;涵蓋所有單一位元組值的測試則以標準格式化結果作為獨立參照,避免編碼器與解碼器犯了相互抵銷的錯誤,卻仍能成功往返。
func reject(
_ text: String,
limit: Int = 16,
expected: HexError
) {
do {
_ = try decodeHex(text, maxBytes: limit)
preconditionFailure("Expected rejection")
} catch let error as HexError {
precondition(error == expected, "Unexpected error")
} catch {
preconditionFailure("Unexpected error type")
}
}
let fixed = try decodeHex("000aFf", maxBytes: 3)
precondition(fixed == Data([0, 10, 255]))
precondition(encodeHex(fixed) == "000aff")
let empty = try decodeHex("", maxBytes: 0)
precondition(empty.isEmpty)
for value in 0...255 {
let reference = String(format: "%02x", value)
let decoded = try decodeHex(reference, maxBytes: 1)
precondition(decoded == Data([UInt8(value)]))
precondition(encodeHex(Data([UInt8(value)])) == reference)
}
reject("f", expected: .oddLength)
reject("0g", expected: .invalidCharacter)
reject("0x00", expected: .invalidCharacter)
reject("00\n0", expected: .invalidCharacter)
reject("00", expected: .invalidCharacter)
reject("0000", limit: 1, expected: .tooLarge)
reject("", limit: -1, expected: .invalidLimit)
let sequence = Data((0...255).map { UInt8($0) })
let restored = try decodeHex(encodeHex(sequence), maxBytes: 256)
precondition(restored == sequence)
奇數長度的案例應明確失敗,不能將 f 猜成 0f 或 f0。也要涵蓋「無效字元出現在結尾」的情況,確認實作不會傳回已解碼的前半段。對於安全相關欄位,失敗就應整筆拒絕,而不是傳回仍可繼續使用的部分結果。
在雲端 Mac 執行並整合至專案
先確認目前的開發工具可用,再執行腳本:
xcrun --find swift
xcrun swift HexContract.swift
成功時腳本不會輸出任何內容,結束代碼為零;斷言失敗或未處理的錯誤則會產生非零結束代碼。不要以會停用檢查的最佳化模式執行這份腳本,也不要在管線中加入忽略結束代碼的處理。
獨立腳本驗證的是目前 macOS 上的 Swift 實作。正式整合時,將編解碼器放入應用程式的共用模組,把上述測試向量移入 XCTest,以 XCTAssertEqual 與 XCTAssertThrowsError 取代檢查,並在既有的 iOS 測試目標中執行。這樣才能同時找出目標成員資格、條件式編譯與實際呼叫路徑的問題。
如果已有實作,不要另外維護一份測試專用副本;應讓測試直接匯入正式環境使用的模組。否則即使腳本通過,應用程式仍可能使用舊版解碼器。
用邊界清單完成回歸檢查
合併前逐項確認:值為零的位元組是否編碼為 00、空輸入是否符合欄位規則、達到容量上限時是否能通過、超過上限一個位元組時是否拒絕,以及無效輸入是否完全不傳回資料。再補上具確定性的長序列,不要只靠隨機資料碰運氣。
可以接受大小寫混用,但不要直接將原始字串當成唯一鍵:aa 與 AA 代表相同的位元組。需要以標準形式儲存時,應先嚴格解碼,再統一編碼;不要在驗證前依賴字串大小寫轉換,藉此取代位元組檢查。
最後保留工具鏈版本、失敗案例名稱與測試結束結果即可。涉及真實裝置訊息或權杖時,不要將完整輸入寫入日誌,應以固定的合成測試向量重現。驗收的目標不是讓所有字串都能轉換,而是讓合法位元組精確往返,讓超出邊界的輸入明確停止。
常見問題
解碼器應該自動移除空白或補齊奇數長度嗎?
機器介面預設應拒絕這兩種輸入。若人工輸入畫面允許整理格式,應在獨立轉接層處理,再交給嚴格解碼器。
只測試編碼後再解碼是否足夠?
不夠,兩個函式可能具有相同缺陷。還需固定向量、全部單一位元組測試、非法輸入拒絕及前導零檢查。