同一份文件,整块计算校验值能够通过,改成分块读取却失败,通常不是云端 Mac 的网络出了问题,而是每读一块都重置了 CRC 状态。跨区团队把文件处理逻辑放到持续测试中时,需要固定的不只是输入文件,还有算法变体、分块方式与结果表示。下面用 Swift 写一份可以直接在云端 Mac 运行的验收样例,再把同样的检查接入 iOS 工程。
先固定 CRC-32 的计算契约
本文使用 CRC-32/ISO-HDLC:正常表示的多项式为 0x04C11DB7,反射实现使用 0xEDB88320,初值和最终异或值均为 0xFFFFFFFF,输入与输出采用反射约定。不要只在接口里写“CRC32”,否则两端可能选择不同变体。
标准字符串 123456789 的结果应为 CBF43926,空输入结果为 00000000。这里字符串按 UTF-8 转成字节,不包含换行。用终端准备样本时,注意有些输出命令会自动补上换行。
CRC-32 用于发现意外损坏,不提供防篡改保证。需要确认文件来源时,应验证可信数字签名,不能把文件旁边的一段 CRC 当成安全凭证。
传输契约还要明确结果是八位十六进制文本,还是四字节整数;如果是后者,另外约定字节序。算法的“反射”不等于网络传输时采用小端序。
写一个保留内部状态的实现
把下面代码保存为 crc-check.swift。示例逐位处理,便于审查参数;它适合作为正确性基线,不代表高吞吐实现。生产优化成查表版本后,也应复用后面的测试。
import Foundation
struct CRC32 {
private var register: UInt32 = 0xFFFFFFFF
mutating func update<S: Sequence>(_ bytes: S)
where S.Element == UInt8 {
for byte in bytes {
register ^= UInt32(byte)
for _ in 0..<8 {
let lowBit = register & 1
register >>= 1
if lowBit != 0 {
register ^= 0xEDB88320
}
}
}
}
var value: UInt32 {
register ^ 0xFFFFFFFF
}
}
func checksum(_ bytes: [UInt8]) -> UInt32 {
var crc = CRC32()
crc.update(bytes)
return crc.value
}
func require(_ condition: Bool, _ name: String) {
if !condition {
fatalError(name)
}
}
let vector = Array("123456789".utf8)
require(checksum(vector) == 0xCBF43926, "reference-vector")
require(checksum([]) == 0, "empty-input")
for split in 0...vector.count {
var crc = CRC32()
crc.update(vector[..<split])
let interim = crc.value
require(interim == checksum(Array(vector[..<split])),
"intermediate-value")
crc.update([])
crc.update(vector[split...])
require(crc.value == 0xCBF43926, "split-invariance")
}
value 只是内部寄存器的只读投影,读取中间结果不会改变后续计算。不要在这个属性里修改寄存器,也不要把最终异或后的结果当成下一块的初始状态。每个文件新建一个实例,比提供容易误用的重置接口更直观。
从固定向量扩展到分块测试
九字节向量只能证明一小部分行为。接着添加包含全部字节取值的确定性输入,让高位字节、零字节和不齐整尾块都进入测试。
let payload = (0..<4097).map {
UInt8(truncatingIfNeeded: $0)
}
let expected = checksum(payload)
for size in [1, 7, 64, 1024, 4096, 8192] {
var crc = CRC32()
var offset = 0
while offset < payload.count {
let end = min(offset + size, payload.count)
crc.update(payload[offset..<end])
offset = end
}
require(crc.value == expected, "chunk-size-\(size)")
}
var first = CRC32()
first.update(vector)
var second = CRC32()
second.update(payload)
require(first.value == 0xCBF43926, "independent-state")
require(second.value == expected, "second-stream")
这里比较的是“同一实现对不同切块保持一致”,不能单独证明算法正确。因此必须同时保留标准向量;正式接协议时,再加入由独立实现生成并经过核对的二进制样本。
不要断言任意两个不同输入的 CRC 必然不同。校验值空间有限,碰撞客观存在,不能用随机文件没有碰撞来证明实现可靠。
用文件读取验证错误路径
继续把读取函数追加到同一个文件。通过 FileHandle 顺序取块,每次只把当前数据交给同一实例,避免将整个文件载入内存。
func checksumFile(_ url: URL) throws -> UInt32 {
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
var crc = CRC32()
while true {
let data = try handle.read(upToCount: 64 * 1024) ?? Data()
if data.isEmpty {
break
}
crc.update(data)
}
return crc.value
}
if CommandLine.arguments.count == 2 {
let url = URL(fileURLWithPath: CommandLine.arguments[1])
let result = try checksumFile(url)
Swift.print(String(format: "%08X", result))
}
执行 xcrun swift crc-check.swift 会运行前面的断言;追加一个可读文件路径,则还会输出文件校验值。读取错误会向上传递,而不是被误判为空文件。打开失败、读取失败与结果不匹配,应在业务层分别记录。
被读取的文件应是已经关闭写入的稳定副本。边下载边修改同一路径,再单独打开计算,可能读到混合版本;计算结束也不能证明之后没有写入。验收时先封存输入,再比较长度与 CRC。
接入工程时检查状态归属
把实现放入可被单元测试访问的模块,保留四类检查:标准向量、分块不变性、实例隔离、文件错误传播。文件测试用独立临时目录,分别创建空文件和二进制文件,结束后清理。
iOS 界面只展示进度和结果,文件读取不要占用主线程。每条传输任务独占一个 CRC 实例;同一任务的分块必须按文件顺序处理,不能让并发回调直接修改寄存器。
如果任务中途取消,就丢弃该实例。断点恢复时,仅有最终校验值不足以直接续算:应重新读取已保存的前缀,或设计有版本约束的内部状态检查点。后者还必须核对偏移与文件身份,不能只保存一个整数。
最后将工具链版本、样本长度、切块方案和失败断言名留在测试产物中。这样下一次换节点或优化算法时,你能区分参数变化、分块遗漏和状态串用,而不是反复重传文件碰运气。
常见问题
CRC-32 能证明下载文件没有被恶意替换吗?
不能。CRC-32 只能用于发现意外损坏,攻击者可以同时修改文件与校验值。需要防篡改时,应验证可信来源提供的数字签名,或按协议使用带密钥的认证机制。
每个分块分别计算 CRC 后异或,等于整份文件的 CRC 吗?
不等于。顺序读取时应持续更新同一个内部寄存器,全部字节处理完后再取最终值。独立分块结果的合并需要专门的长度相关算法,不能直接异或。