雲端 Mac 上的 iOS UUID 識別碼契約測試:嚴格解析與位元組順序

雲端 Mac 上的 iOS UUID 識別碼契約測試:嚴格解析與位元組順序

雲端 Mac 上的 iOS UUID 識別碼契約測試:嚴格解析與位元組順序

同一筆訂單紀錄,API 回傳大寫 UUID,本機快取卻儲存小寫,去重邏輯又直接比較字串;另一條處理流程將 UUID 寫成二進位資料,讀回後,前幾組位元組的順序卻變了。這類問題可以放到雲端 Mac 的建置前檢查,不必啟動模擬器。先用固定的 Swift 測試向量明訂輸入、識別碼比較與序列化契約,再讓 iOS 專案共用同一套規則。

先區分格式與業務政策

UUID 是 128 位元的識別碼,常見的文字表示法由五組十六進位字元組成,各組長度依序為 8、4、4、4、12。連字號固定出現四次,但光憑這個格式,並不能判定它一定是隨機產生的版本 4 UUID。

先明訂三項規則:是否接受大寫、是否允許前後空白,以及是否限制版本。本文接受大小寫,但拒絕空白、大括號、不含連字號的格式與非 ASCII 字元;只有明確要求隨機識別碼的欄位,才檢查版本 4 及對應的變體。

「能夠解析」是函式庫的能力,「允許進入系統」則是通訊協定的決定。不要直接把解析器的寬鬆行為當成 API 契約。

全零 UUID 也需要另行決定處理方式。它可能符合通用的文字格式,卻不應自動成為「未設定」的替代值。選填欄位應優先採用明確的空值語意。

在雲端 Mac 執行固定測試向量

準備好已選定開發工具鏈的雲端 Mac,將以下內容儲存為 uuid-contract.swift。這份指令稿只使用 Foundation,不依賴專案、網路或亂數。此處的 precondition 用來在測試失敗時中止指令稿,不是供應用程式處理外部輸入之用。

import Foundation

func parseUUID(_ text: String, requireV4: Bool = false) -> UUID? {
    let bytes = Array(text.utf8)
    guard bytes.count == 36 else { return nil }

    let separators: Set<Int> = [8, 13, 18, 23]
    for (index, byte) in bytes.enumerated() {
        if separators.contains(index) {
            guard byte == 45 else { return nil }
        } else {
            let isHex = (48...57).contains(byte)
                || (65...70).contains(byte)
                || (97...102).contains(byte)
            guard isHex else { return nil }
        }
    }

    guard let value = UUID(uuidString: text) else { return nil }
    if requireV4 {
        let raw = value.uuid
        guard raw.6 >> 4 == 4, raw.8 & 0xC0 == 0x80 else {
            return nil
        }
    }
    return value
}

let valid = "00112233-4455-4677-8899-aabbccddeeff"
let parsed = parseUUID(valid, requireV4: true)!
precondition(parsed.uuidString.lowercased() == valid)
precondition(parseUUID(valid.uppercased()) == parsed)

let invalid = [
    " " + valid,
    valid + "\n",
    "{" + valid + "}",
    valid.replacingOccurrences(of: "-", with: ""),
    valid.replacingOccurrences(of: "a", with: "a")
]
for input in invalid {
    precondition(parseUUID(input) == nil)
}

let otherVersion = "00112233-4455-5677-8899-aabbccddeeff"
precondition(parseUUID(otherVersion) != nil)
precondition(parseUUID(otherVersion, requireV4: true) == nil)

let wrongVariant = "00112233-4455-4677-0899-aabbccddeeff"
precondition(parseUUID(wrongVariant) != nil)
precondition(parseUUID(wrongVariant, requireV4: true) == nil)

let raw = parsed.uuid
let wire: [UInt8] = [
    raw.0, raw.1, raw.2, raw.3,
    raw.4, raw.5, raw.6, raw.7,
    raw.8, raw.9, raw.10, raw.11,
    raw.12, raw.13, raw.14, raw.15
]
precondition(wire == [
    0x00, 0x11, 0x22, 0x33,
    0x44, 0x55, 0x46, 0x77,
    0x88, 0x99, 0xaa, 0xbb,
    0xcc, 0xdd, 0xee, 0xff
])

執行 xcrun swift uuid-contract.swift,成功時不會輸出任何內容,失敗時則回傳非零結束狀態。流水線必須保留這個狀態;同時記錄 xcrun swift --version,方便在工具鏈更新後重新執行並比較結果。

識別碼比較只保留單一入口

應用程式內部應盡量比較 UUID 值,而不是 API 傳回的原始字串。需要寫入文字型資料庫鍵值時,先通過嚴格解析,再呼叫 uuidString.lowercased(),取得唯一的小寫連字號表示法。

不要先刪除連字號、去除前後空白,再嘗試解析。這類「修復」會掩蓋上游的通訊協定錯誤,也可能讓兩個原本都應拒絕的輸入對應到同一個鍵值。若顯示層允許使用者貼上內容,可以明確提供清理步驟,但不能讓 API 層默默沿用這種行為。

遷移舊資料前,必須先統計大小寫正規化後的衝突。例如,兩筆紀錄的 UUID 只有英文字母大小寫不同,就不能直接覆寫其中一筆。應先核對業務關聯,再合併或隔離異常紀錄,並保留遷移結果。

二進位資料交接要檢查完整位元組順序

上面的測試向量依照 UUID 文字中十六進位字元的順序,列出 16 個位元組。這個順序必須明訂為傳輸協定的一部分,不能由目前機器的原生整數儲存方式決定。

不要把前八個位元組讀成原生整數後直接寫出,也不要因為某個 API 將識別碼稱為 GUID,就假設它的記憶體配置與本協定相同。跨語言交接時,必須要求雙方針對同一個固定文字,輸出完全一致的十六進位位元組序列。

讀回測試也需要獨立樣本

只做「自行編碼,再自行解碼」並不足夠:兩邊若犯了同樣的錯,往返測試仍然可能通過。接收端應直接使用固定的 16 位元組樣本,斷言還原後的文字等於原始測試向量;接著再測試 15 位元組與 17 位元組的輸入,確認兩者都會被拒絕,不能補零或截斷。

將檢查接到實際解析路徑

這份指令稿驗證的是規則範例。實際導入時,應將解析函式移入共用模組,讓 API 解碼、快取讀寫與匯入工具統一呼叫;對應的測試向量則放進測試目標,避免檢查指令稿通過,應用程式卻仍走另一條寬鬆的解析路徑。

補齊四組測試案例:合法的大小寫輸入應視為同一個識別碼;非法字元與空白應拒絕;版本限制只套用至指定欄位;二進位樣本應逐位元組一致。每一組都要檢查回傳結果,不能只斷言「沒有當機」。

失敗日誌只記錄欄位名稱、失敗類別與樣本編號,不輸出真實的業務識別碼。工具鏈更新、伺服器端序列化函式庫更換,或匯入歷史資料時,都應先重新執行這組契約測試。將失敗原因定位到文字格式、政策或位元組順序其中一層,比起在快取與 API 之間反覆猜測更直接。

常見問題

每個 UUID 都必須限制為第 4 版嗎?

不必。只有介面協定明訂使用隨機第 4 版識別碼時,才啟用此限制;通用 UUID 欄位應分開處理文字解析與允許的版本。

UUID 的大小寫不同,是否代表不同識別碼?

不是。合法值解析後,應統一輸出為小寫且帶連字號的文字作為索引鍵;原始寫法僅供診斷,不應影響身分判定。

讓下一次建置擁有專屬 Mac。

選擇非虛擬機的雲端 Mac,沿用現有的 Xcode 與 CI 工作流程。依照專案負載選擇主機規格,實際可用狀態請以控制台即時回報為準。

立即租用雲端 Mac

把下一次建置交給專屬實體主機。

沿用現有指令與 CI 流程,依照專案負載選擇雲端 Mac。