云端 Mac 上的 iOS UUID 标识符契约测试:严格解析与字节序

云端 Mac 上的 iOS UUID 标识符契约测试:严格解析与字节序

同一条订单记录,接口返回大写 UUID,本地缓存保存小写,去重逻辑却直接比较字符串;另一条链路把 UUID 写成二进制,回读后前几组字节顺序又变了。把这类问题放到云端 Mac 的构建前检查,不需要启动模拟器。先用固定 Swift 向量锁住输入、身份比较与序列化契约,再让 iOS 工程复用同一套规则。

先拆开格式与业务策略

UUID 是 128 位标识符,常见文本形式为五组十六进制字符,长度依次是 8、4、4、4、12。连字符固定出现四次,但这个外形并不说明它一定是随机生成的版本 4。

先写清三条规则:是否接受大写,是否允许首尾空白,是否限制版本。本文接受大小写,拒绝空白、花括号、无连字符形式与非 ASCII 字符;只有明确要求随机标识符的字段才检查版本 4 和对应变体。

“能够解析”是库的能力,“允许进入系统”是协议的决定。不要把解析器的宽松行为直接当成接口契约。

全零 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 值,而不是比较接口原文。需要写入文本数据库键时,在严格解析成功后调用 uuidString.lowercased(),得到唯一的小写连字符表示。

不要先删连字符、修剪空白再尝试解析。这样的“修复”会掩盖上游协议错误,也可能让两个原本应被拒绝的输入落到同一个键。展示层允许用户粘贴时,可以显式提供清理步骤,但不能让接口层静默沿用。

旧数据迁移要先统计大小写归一化后的碰撞。例如两条记录仅 UUID 字母大小写不同,不能直接覆盖其中一条。先核对业务关联,再合并或隔离异常记录,并保留迁移结果。

二进制交接检查完整字节序

上面的向量按 UUID 文本中十六进制字符的顺序列出 16 个字节。这个顺序必须成为线协议的一部分,而不是由当前机器的整数存储方式决定。

不要将前八个字节读成原生整数后直接写出。也不要凭某个接口把标识符称为 GUID,就假设它与本协议的内存布局相同。跨语言交接时,要求双方对同一个固定文本输出完全一致的十六进制字节串。

回读也需要独立样本

仅做“自己编码,再自己解码”不够:两边犯同一个错误,往返仍会成功。接收端应直接使用固定的 16 字节样本,断言恢复的文本等于原始向量;再测试 15 字节和 17 字节输入必须被拒绝,不能补零或截断。

把检查接到真实解析路径

脚本验证的是规则示例。落地时,把解析函数移入共享模块,由接口解码、缓存读写和导入工具统一调用;对应向量放进测试目标,避免检查脚本通过而应用仍使用另一条宽松路径。

补齐四组用例:合法大小写应归为同一身份;非法字符与空白应拒绝;版本限制只作用于指定字段;二进制样本应逐字节一致。每组都检查返回结果,不能只断言“没有崩溃”。

失败日志只记录字段名、失败类别与样本编号,不输出真实业务标识符。工具链更新、服务端序列化库替换或历史数据导入时,先重跑这组契约。把失败定位到文本、策略或字节序中的一层,比在缓存和接口之间反复猜测更直接。

常见问题

所有 UUID 都应该强制检查为版本 4 吗?

不应该。只有接口协议明确要求随机版本 4 标识符时才启用该限制;通用 UUID 字段应把文本解析与允许的版本策略分开。

UUID 大小写不同,是否应该视为两个标识符?

不应该。对合法 UUID 先解析,再生成统一的小写连字符文本作为键;保留原始输入只能用于诊断,不能让大小写参与身份判定。

让下一次构建独享一台 Mac。

选择非虚拟机的云端 Mac,保留现有 Xcode 与 CI 工作流程。按工程负载配置机器,实际可用状态以控制台实时返回为准。

立即租用云端 Mac

把下一次构建交给独享物理机。

保留现有命令与 CI 流程,按工程负载选择云端 Mac。