雲端 Mac 上的 iOS UUID 識別碼驗收:嚴格解析與正規化

雲端 Mac 上的 iOS UUID 識別碼驗收:嚴格解析與正規化

雲端 Mac 上的 iOS UUID 識別碼驗收:嚴格解析與正規化

API 把同一筆紀錄的 UUID 從大寫改成小寫,用戶端卻新增了第二筆紀錄。問題通常不在亂數產生器,而是直接將字串當成主鍵,導致比較、快取與持久化各自採用不同規則。在雲端 Mac 上,可以先用獨立的 Swift 驗收腳本驗證這個邊界,再納入 iOS 專案測試,不必啟動模擬器。

先訂定輸入契約

本文接受標準的 36 位元組文字格式:十六進位字元分成五組,長度依序為 8、4、4、4、12,組間使用 ASCII 連字號。輸入允許大小寫混用,輸出一律為小寫;前後空白、大括號包覆及不含連字號的形式,一律拒絕。

這並不是說其他表示法都無效,而是為 API 選定唯一的輸入形式。若舊版 API 允許其他形式,應交由明確的相容層處理,不要讓核心解析器默默修正。

還要區分兩件事:語法合法,不代表業務規則允許。全零 UUID 在結構上合法,但訂單或文件識別碼可以禁止使用;是否限制 UUID 版本,也應由 API 契約決定,不能只因第三組的第一個字元不是某個數字就直接報錯。

UUID 識別碼不是存取憑證。即使格式正確,伺服器仍須檢查目前使用者是否有權讀取對應紀錄。

撰寫可執行的嚴格解析器

在節點上執行 xcrun --find swift,確認目前的工具鏈提供 Swift。將下列內容儲存為 uuid-contract.swift。程式碼會先檢查位元組結構,再交給 Foundation 解析,避免依賴特定系統版本對寬鬆輸入的容忍程度。

import Foundation

enum IdentifierError: Error {
    case invalidSyntax
    case zeroNotAllowed
}

func parseIdentifier(
    _ text: String,
    allowZero: Bool = false
) throws -> UUID {
    let bytes = Array(text.utf8)
    let separators: Set<Int> = [8, 13, 18, 23]

    guard bytes.count == 36 else {
        throw IdentifierError.invalidSyntax
    }

    for (index, byte) in bytes.enumerated() {
        if separators.contains(index) {
            guard byte == 45 else {
                throw IdentifierError.invalidSyntax
            }
        } else {
            let isHex = (48...57).contains(byte)
                || (65...70).contains(byte)
                || (97...102).contains(byte)
            guard isHex else {
                throw IdentifierError.invalidSyntax
            }
        }
    }

    guard let value = UUID(uuidString: text) else {
        throw IdentifierError.invalidSyntax
    }

    let zero = UUID(
        uuid: (0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0)
    )
    guard allowZero || value != zero else {
        throw IdentifierError.zeroNotAllowed
    }
    return value
}

func canonical(_ value: UUID) -> String {
    value.uuidString.lowercased()
}

使用 UTF-8 位元組,而不是肉眼可見的字元數,是為了明確排除全形字元及外觀類似連字號的 Unicode 符號。這裡的 lowercased() 只處理已通過驗證的 UUID 輸出,不會先轉換任意使用者輸入再予以放行。

用固定測試資料檢查邊界

接著在同一個檔案末尾加入測試。有效案例用來驗證值相等、正規化與冪等性;無效案例則涵蓋空白、字元替換及長度變化。不要以隨機 UUID 取代固定的反例,否則失敗情境不容易重現。

let upper = "550E8400-E29B-41D4-A716-446655440000"
let lower = upper.lowercased()
let value = try parseIdentifier(upper)

precondition(value == (try parseIdentifier(lower)))
precondition(canonical(value) == lower)
let reparsed = try parseIdentifier(canonical(value))
precondition(canonical(reparsed) == lower)

let invalid = [
    "",
    " " + lower,
    lower + "\n",
    "{" + lower + "}",
    lower.replacingOccurrences(of: "-", with: ""),
    lower.replacingOccurrences(of: "-", with: "-"),
    lower.replacingOccurrences(of: "5", with: "g"),
    lower + "0"
]

for text in invalid {
    do {
        _ = try parseIdentifier(text)
        fatalError("Invalid UUID accepted")
    } catch IdentifierError.invalidSyntax {
    }
}

let zero = "00000000-0000-0000-0000-000000000000"
do {
    _ = try parseIdentifier(zero)
    fatalError("Zero UUID accepted")
} catch IdentifierError.zeroNotAllowed {
}
_ = try parseIdentifier(zero, allowZero: true)

執行 xcrun swift uuid-contract.swift,成功時不會輸出任何內容,失敗時程序會以非零結束碼結束。程式碼中的空白 catch 區塊表示捕捉到預期錯誤;其他錯誤並未被忽略,仍會讓腳本失敗。移入 XCTest 後,應針對具體錯誤使用斷言,保留同樣的區分。

在資料邊界進行正規化

解析應在網路回應、深層連結參數或匯入檔案進入領域模型的位置進行。領域層持有 UUID,只有在需要發送請求或寫入文字欄位時,才呼叫 canonical。不要讓介面層與快取層各自執行字串替換。

儲存資料的移轉尤其需要謹慎。先在唯讀副本中將舊鍵值對應至正規形式,統計「同一個正規形式對應多個舊鍵值」的情況。發現衝突後,應依業務紀錄的合併規則處理,再更新外鍵與唯一性約束;直接批次轉成小寫,可能會將兩筆歷史紀錄壓成一筆。

不要把 UUID 文字排序當成時間排序

隨機 UUID 的字典序無法代表建立先後。清單需要穩定順序時,應使用明確的建立時間欄位,並在時間相同時,以正規化識別碼作為次要排序鍵。若採用具有時間語意的 UUID 方案,也必須另外約定版本與排序規則。

納入雲端 Mac 的驗收流程

在 ArmVNC 節點執行腳本前,先記錄提交識別碼、sw_vers 與 xcrun swift --version。這些資訊用來釐清工具鏈差異,不含業務資料;失敗案例應使用固定測試值,不要直接封存使用者紀錄。

納入持續整合後,至少檢查四項:

  • 標準文字格式可以解析,大小寫不同仍得到同一個值。
  • 空白、錯誤分隔符號與非十六進位字元都必須解析失敗。
  • 全零值須依明確政策處理,不能被一般語法錯誤掩蓋。
  • 舊鍵值移轉前先檢查衝突,且關聯紀錄數量在移轉前後保持一致。

最後補上一項整合測試:將同一個實體分別以大寫與小寫 UUID 輸入兩次,儲存中仍應只有一筆紀錄,更新也應套用至原紀錄。解析器確保輸入端一致,整合測試才能證明快取、資料庫與業務更新確實遵守這份契約。

常見問題

UUID 文字大小寫不同,應視為不同識別碼嗎?

不應該。先解析為 UUID 值再比較,寫入文字儲存時統一輸出小寫標準形式。若舊資料庫使用區分大小寫的字串鍵,移轉前必須先檢查正規化後是否衝突。

全零 UUID 一定要拒絕嗎?

不一定。它在結構上是有效 UUID,但業務規則可以禁止將它作為實體識別碼。將此規則設為明確參數,並分別測試允許與拒絕的情況。

將建置工作交給專屬雲端 Mac

選擇 Apple Silicon 物理節點,獨享整台機器,使用原生 macOS 圖形介面與命令列。機型、租用週期與節點,請以訂單確認內容為準。