Skip to content

Test streaming CRC-32 for iOS on a cloud Mac

Test streaming CRC-32 for iOS on a cloud Mac

Test streaming CRC-32 for iOS on a cloud Mac

If a file passes checksum verification when processed in one go but fails when read in chunks, the problem is usually not the cloud Mac’s network connection. More often, the CRC state is being reset for every chunk. When teams working across regions add file-processing logic to continuous testing, they need to pin down more than the input file: the algorithm variant, chunking strategy, and result representation must also be fixed. Below, we build a Swift acceptance test that runs directly on a cloud Mac, then bring the same checks into an iOS project.

Define the CRC-32 contract first

This article uses CRC-32/ISO-HDLC: the polynomial in normal representation is 0x04C11DB7, while the reflected implementation uses 0xEDB88320. The initial value and final XOR value are both 0xFFFFFFFF, and both input and output follow the reflected convention. Do not label an interface simply “CRC32”—the two ends could choose different variants.

The standard test string 123456789 must produce CBF43926, and empty input must produce 00000000. Here, strings are converted to UTF-8 bytes with no trailing newline. When preparing samples in a terminal, remember that some output commands append a newline automatically.

CRC-32 detects accidental corruption; it does not protect against tampering. To verify a file’s provenance, validate a trusted digital signature. A CRC stored alongside the file is not a security credential.

The transfer contract must also specify whether the result is eight hexadecimal digits or a four-byte integer. For an integer, agree on byte order as well. Algorithmic “reflection” does not imply little-endian byte order on the wire.

Implement CRC-32 with persistent internal state

Save the following code as crc-check.swift. This example processes one bit at a time so its parameters are easy to review. It is a correctness baseline, not a high-throughput implementation. If you optimize it into a table-driven version for production, reuse the tests that follow.

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 is only a read-only projection of the internal register. Reading an intermediate result does not affect subsequent computation. Do not modify the register in this property, and do not use the result after the final XOR as the initial state for the next chunk. Creating a new instance for each file is clearer than exposing a reset method that is easy to misuse.

Extend the reference vector with chunking tests

A nine-byte vector covers only a small part of the behavior. Next, add deterministic input containing every possible byte value, so the tests exercise bytes with the high bit set, zero bytes, and partial final chunks.

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")

This checks that the same implementation produces consistent results across different chunk boundaries. It cannot, by itself, prove that the algorithm is correct. Keep the standard reference vector as well. When integrating with a real protocol, also add binary samples generated by an independent implementation and checked for correctness.

Do not assert that any two different inputs must have different CRCs. The checksum space is finite, so collisions do exist. An absence of collisions among random files does not prove that an implementation is reliable.

Verify error paths with file reads

Append the following file-reading function to the same file. It reads chunks sequentially through FileHandle and passes only the current chunk to the same CRC instance, avoiding loading the entire file into memory.

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))
}

Running xcrun swift crc-check.swift executes the assertions above. Add a readable file path as an argument to also print that file’s checksum. Read errors propagate to the caller rather than being mistaken for an empty file. At the application layer, log open failures, read failures, and checksum mismatches separately.

The file being read should be a stable copy that is no longer open for writing. If a download is still modifying the same path when you open it separately for checksum calculation, you may read a mixture of versions. Finishing the calculation also does not prove that no writes occurred afterward. For acceptance testing, freeze the input first, then compare its length and CRC.

Check state ownership when integrating

Place the implementation in a module accessible to unit tests, and retain four categories of checks: reference vectors, chunk invariance, instance isolation, and file-error propagation. Use a dedicated temporary directory for file tests, create both empty and binary files, and clean up afterward.

The iOS UI should only display progress and results; file reads must not run on the main thread. Give each transfer task its own CRC instance. Process a task’s chunks in file order, and do not let concurrent callbacks modify the register directly.

If a task is canceled midway, discard its instance. When resuming a transfer, the final checksum alone is not enough to continue the calculation directly. Either reread the saved prefix or design a versioned checkpoint format for the internal state. The latter must also verify the offset and file identity; storing a single integer is not enough.

Finally, include the toolchain version, sample lengths, chunking strategy, and names of failed assertions in the test artifacts. The next time you switch nodes or optimize the algorithm, these records will help you distinguish parameter changes, missing chunks, and state shared across tasks—instead of repeatedly retransferring files and hoping for success.

Frequently asked questions

Can CRC-32 prove that a downloaded file was not maliciously replaced?

No. CRC-32 detects accidental corruption, not malicious changes. An attacker can replace both the file and its checksum. Use a digital signature from a trusted source or a protocol-appropriate keyed authentication mechanism.

Does XORing the CRC of each chunk produce the whole-file CRC?

No. For sequential reads, keep updating one internal register and obtain the final value after all bytes are processed. Combining independent chunk checksums requires a dedicated algorithm that accounts for chunk lengths.

Rent a Mac mini on your project’s schedule

Choose a dedicated physical node and pay by the day, week, month, or quarter. Add time during busy periods; service stops when your rental ends unless renewed. Check the console for real-time availability.

Make sure a machine is ready for your next build

Compare the specs and rental term, then choose the right dedicated physical node for your project.