跳至主要內容

雲端 Mac 上的 iOS CRC-32 串流驗證與標準向量測試

雲端 Mac 上的 iOS CRC-32 串流驗證與標準向量測試

雲端 Mac 上的 iOS CRC-32 串流驗證與標準向量測試

同一份檔案,一次處理全部內容時檢查碼驗證通過,改成分塊讀取卻失敗,通常不是雲端 Mac 的網路出了問題,而是每讀取一塊就重設了 CRC 狀態。跨區團隊將檔案處理邏輯納入持續測試時,需要固定的不只是輸入檔案,還有演算法變體、分塊方式與結果表示格式。以下用 Swift 寫一份可直接在雲端 Mac 上執行的驗收範例,再將相同檢查整合到 iOS 專案。

先確立 CRC-32 的計算規約

本文使用 CRC-32/ISO-HDLC:正常表示法的多項式為 0x04C11DB7,反射實作使用 0xEDB88320,初始值與最終 XOR 值皆為 0xFFFFFFFF,輸入與輸出均採反射慣例。不要只在介面中標示「CRC32」,否則兩端可能選用不同的變體。

標準測試字串 123456789 的結果應為 CBF43926,空輸入的結果為 00000000。這裡的字串以 UTF-8 轉成位元組,不包含換行字元。在終端機準備樣本時,請留意部分輸出指令會自動附加換行字元。

CRC-32 用來偵測意外損毀,不提供防竄改保證。若需確認檔案來源,應驗證可信的數位簽章,不能將附在檔案旁的 CRC 當成安全憑證。

傳輸規約也必須明確定義結果是八位十六進位文字,還是四位元組整數;若為後者,還要另外約定位元組順序。演算法的「反射」不代表網路傳輸採用小端序。

實作可保留內部狀態的 CRC-32

將以下程式碼儲存為 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 只是內部暫存器的唯讀投影,讀取中間結果不會改變後續計算。不要在這個屬性中修改暫存器,也不要將經過最終 XOR 的結果當成下一塊的初始狀態。每個檔案都建立新實例,比提供容易誤用的重設介面更直觀。

從標準向量擴充到分塊測試

九位元組的向量只能驗證一小部分行為。接著加入涵蓋所有位元組值的確定性輸入,讓最高位元為 1 的位元組、零位元組,以及不足一整塊的尾端資料都納入測試。

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 嗎?

不會。依序讀取時應持續更新同一個內部暫存器,全部位元組處理完再取得最終值。獨立分塊結果的合併需要考慮分塊長度的專用演算法,不能直接做互斥或。

依照專案進度租用 Mac mini

選擇獨享實體節點,提供日租、週租、月租和季租。旺季可加租;租期結束後若未續租,服務即會停止。實際可用狀態請以控制台即時回報為準。

讓下一次建置有台機器可用

先確認規格與租期,再為專案選擇合適的獨享實體節點。