Zum Inhalt springen

CRC-32 für iOS: Datenströme auf dem Cloud-Mac prüfen

CRC-32 für iOS: Datenströme auf dem Cloud-Mac prüfen

CRC-32 für iOS: Datenströme auf dem Cloud-Mac prüfen

Wenn die Prüfsumme einer Datei bei der Verarbeitung am Stück stimmt, beim blockweisen Lesen aber nicht, liegt das meist nicht am Netzwerk des Cloud-Macs. Häufig wird der CRC-Zustand nach jedem Block zurückgesetzt. Wenn verteilte Teams die Dateiverarbeitung in ihre kontinuierlichen Tests aufnehmen, müssen sie neben den Eingabedateien auch die Algorithmusvariante, die Blockaufteilung und die Ergebnisdarstellung festlegen. Im Folgenden entsteht mit Swift ein Abnahmetest, der direkt auf einem Cloud-Mac läuft. Anschließend werden dieselben Prüfungen in ein iOS-Projekt integriert.

Zuerst die Berechnungsvorgaben für CRC-32 festlegen

Dieser Artikel verwendet CRC-32/ISO-HDLC: Das Polynom in normaler Darstellung lautet 0x04C11DB7, die reflektierte Implementierung verwendet 0xEDB88320. Initialwert und abschließender XOR-Wert sind jeweils 0xFFFFFFFF; Ein- und Ausgabe folgen der reflektierten Konvention. Eine Schnittstelle sollte nicht lediglich „CRC32“ vorgeben, da die beiden Seiten sonst unterschiedliche Varianten wählen könnten.

Für die Standardzeichenfolge 123456789 muss das Ergebnis CBF43926 lauten, für eine leere Eingabe 00000000. Die Zeichenfolge wird hier ohne Zeilenumbruch in UTF-8-Bytes umgewandelt. Beim Erstellen einer Beispieldatei im Terminal ist zu beachten, dass manche Ausgabebefehle automatisch einen Zeilenumbruch anhängen.

CRC-32 erkennt unbeabsichtigte Beschädigungen, bietet aber keinen Schutz vor Manipulationen. Um die Herkunft einer Datei zu bestätigen, muss eine vertrauenswürdige digitale Signatur geprüft werden. Ein neben der Datei angegebener CRC-Wert ist kein Sicherheitsnachweis.

Die Übertragungsspezifikation muss außerdem festlegen, ob das Ergebnis als achtstelliger Hexadezimaltext oder als Vier-Byte-Ganzzahl übertragen wird. Im zweiten Fall ist zusätzlich die Byte-Reihenfolge zu vereinbaren. Die „Reflexion“ des Algorithmus bedeutet nicht, dass die Netzwerkübertragung Little Endian verwendet.

Eine Implementierung mit erhaltenem internem Zustand schreiben

Speichern Sie den folgenden Code als crc-check.swift. Das Beispiel arbeitet bitweise, damit sich die Parameter leicht überprüfen lassen. Es dient als Referenz für die Korrektheit, nicht als Implementierung für hohen Durchsatz. Auch nach einer Optimierung mit einer Nachschlagetabelle sollten die nachfolgenden Tests weiterverwendet werden.

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 ist lediglich eine schreibgeschützte Sicht auf das interne Register. Das Auslesen eines Zwischenergebnisses verändert die weitere Berechnung nicht. Diese Eigenschaft darf weder das Register verändern noch darf das Ergebnis nach dem abschließenden XOR als Anfangszustand des nächsten Blocks dienen. Für jede Datei eine neue Instanz anzulegen, ist eindeutiger als eine leicht falsch zu verwendende Reset-Schnittstelle.

Feste Testvektoren um Blocktests ergänzen

Der neun Byte lange Vektor deckt nur einen kleinen Teil des Verhaltens ab. Ergänzen Sie deshalb eine deterministische Eingabe, die alle möglichen Bytewerte enthält. So werden auch Bytes mit gesetztem höchstwertigem Bit, Nullbytes und unvollständige letzte Blöcke getestet.

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

Dieser Vergleich zeigt nur, dass dieselbe Implementierung bei unterschiedlichen Blockaufteilungen konsistente Ergebnisse liefert. Allein beweist er nicht, dass der Algorithmus korrekt ist. Der Standardvektor muss deshalb erhalten bleiben. Bei der tatsächlichen Protokollanbindung sollten zusätzlich geprüfte binäre Beispieldaten hinzukommen, die mit einer unabhängigen Implementierung erzeugt wurden.

Prüfen Sie nicht per Assertion, dass zwei beliebige unterschiedliche Eingaben immer unterschiedliche CRC-Werte liefern. Der Wertebereich der Prüfsumme ist begrenzt, Kollisionen sind daher unvermeidlich. Dass bei zufällig erzeugten Dateien keine Kollision auftritt, belegt nicht die Zuverlässigkeit der Implementierung.

Fehlerpfade beim Lesen von Dateien prüfen

Hängen Sie die Lesefunktion an dieselbe Datei an. Sie liest mit FileHandle nacheinander einzelne Blöcke und übergibt jeweils nur die aktuellen Daten an dieselbe Instanz. So muss nicht die gesamte Datei in den Arbeitsspeicher geladen werden.

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

Der Aufruf xcrun swift crc-check.swift führt die bisherigen Assertions aus. Wird zusätzlich der Pfad einer lesbaren Datei angegeben, gibt das Skript auch deren Prüfsumme aus. Lesefehler werden weitergereicht, statt fälschlich als leere Datei behandelt zu werden. Fehler beim Öffnen, Fehler beim Lesen und abweichende Prüfsummen sollten in der Anwendungslogik getrennt protokolliert werden.

Die gelesene Datei muss eine stabile Kopie sein, auf die keine Schreibzugriffe mehr erfolgen. Wird während eines Downloads derselbe Pfad weiter verändert und separat zur Prüfsummenberechnung geöffnet, können Daten verschiedener Versionen in die Berechnung einfließen. Auch eine abgeschlossene Berechnung beweist nicht, dass danach keine Schreibzugriffe stattfinden. Für die Abnahme muss die Eingabe zuerst unveränderlich gesichert werden; erst dann werden Länge und CRC verglichen.

Bei der Integration die Zustandszuordnung prüfen

Legen Sie die Implementierung in einem Modul ab, auf das die Unit-Tests zugreifen können. Behalten Sie vier Arten von Prüfungen bei: Standardvektoren, Unabhängigkeit von der Blockaufteilung, Isolation der Instanzen und Weitergabe von Dateifehlern. Verwenden Sie für Dateitests ein eigenes temporäres Verzeichnis, erstellen Sie darin eine leere Datei sowie eine Binärdatei und räumen Sie anschließend auf.

Die iOS-Oberfläche zeigt lediglich Fortschritt und Ergebnis an; das Lesen der Datei darf den Hauptthread nicht belegen. Jede Übertragungsaufgabe erhält eine eigene CRC-Instanz. Die Blöcke einer Aufgabe müssen in der Reihenfolge der Datei verarbeitet werden. Nebenläufige Callbacks dürfen das Register nicht direkt verändern.

Wird eine Aufgabe abgebrochen, wird ihre Instanz verworfen. Beim Fortsetzen einer unterbrochenen Übertragung reicht der abschließende Prüfwert allein nicht aus, um direkt weiterzurechnen. Entweder wird der bereits gespeicherte Anfang der Datei erneut gelesen, oder es werden versionierte Checkpoints des internen Zustands vorgesehen. Im zweiten Fall müssen zusätzlich Offset und Dateiidentität geprüft werden; eine einzelne gespeicherte Ganzzahl genügt nicht.

Halten Sie abschließend die Toolchain-Version, die Länge der Beispieldaten, die Blockaufteilung und die Namen fehlgeschlagener Assertions in den Testartefakten fest. Beim nächsten Wechsel des Knotens oder bei einer Algorithmusoptimierung lassen sich so geänderte Parameter, ausgelassene Blöcke und versehentlich gemeinsam genutzte Zustände unterscheiden, statt Dateien auf gut Glück immer wieder zu übertragen.

Häufig gestellte Fragen

Schützt CRC-32 vor absichtlich veränderten Dateien?

Nein. CRC-32 erkennt zufällige Beschädigungen, bietet aber keinen Manipulationsschutz. Dafür sind vertrauenswürdig bereitgestellte digitale Signaturen oder ein zum Protokoll passendes Verfahren mit geheimem Schlüssel erforderlich.

Kann ich die CRC-Werte einzelner Blöcke einfach XOR-verknüpfen?

Nein. Aktualisieren Sie beim sequenziellen Lesen denselben internen Zustand und lesen Sie danach den Endwert aus. Das Zusammenführen unabhängiger Blockwerte erfordert einen speziellen Algorithmus, der auch die Blocklängen berücksichtigt.

Mieten Sie einen Mac mini passend zum Projektrhythmus

Wählen Sie einen exklusiven physischen Server und zahlen Sie tage-, wochen-, monats- oder quartalsweise. In der Hochsaison können Sie zusätzliche Laufzeit buchen. Läuft die Mietdauer ab, endet der Dienst automatisch. Maßgeblich ist der Verfügbarkeitsstatus, den die Konsole in Echtzeit anzeigt.

Sorgen Sie dafür, dass beim nächsten Build ein Server bereitsteht

Vergleichen Sie zuerst Ausstattung und Laufzeit und wählen Sie dann den passenden exklusiven physischen Server für Ihr Projekt.