雲端 Mac 上的 iOS 十六進位編解碼契約測試

雲端 Mac 上的 iOS 十六進位編解碼契約測試

雲端 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 代表相同的位元組。需要以標準形式儲存時,應先嚴格解碼,再統一編碼;不要在驗證前依賴字串大小寫轉換,藉此取代位元組檢查。

最後保留工具鏈版本、失敗案例名稱與測試結束結果即可。涉及真實裝置訊息或權杖時,不要將完整輸入寫入日誌,應以固定的合成測試向量重現。驗收的目標不是讓所有字串都能轉換,而是讓合法位元組精確往返,讓超出邊界的輸入明確停止。

常見問題

解碼器應該自動移除空白或補齊奇數長度嗎?

機器介面預設應拒絕這兩種輸入。若人工輸入畫面允許整理格式,應在獨立轉接層處理,再交給嚴格解碼器。

只測試編碼後再解碼是否足夠?

不夠,兩個函式可能具有相同缺陷。還需固定向量、全部單一位元組測試、非法輸入拒絕及前導零檢查。

依照打包需求,租用專屬 Mac。

先選擇鄰近的節點,再確認專案所需的記憶體、儲存空間與計費週期。整台實體機由您獨享,並非虛擬機;實際可用狀態以控制台即時回報為準。

準備就緒,再開始打包。

下單前請確認配置與計費週期。連線、環境搬移及首次建置的操作步驟,都可以在支援頁面查看。