Grenztests für iOS-Binärprotokolle auf dem Cloud-Mac
Der Remote-Build ist erfolgreich durchgelaufen, doch eine empfangene Binärnachricht bringt die App auf dem Gerät zum Absturz. Bevor Sie das Netzwerk verantwortlich machen, prüfen Sie den Decoder: Das Längenfeld könnte größer sein als der tatsächliche Inhalt, die Leseposition könnte falsch ausgerichtet sein oder eine Big-Endian-Ganzzahl könnte in der nativen Byte-Reihenfolge interpretiert worden sein. Auf einem Cloud-Mac von VpsKeep lassen sich diese Eingaben zunächst mit Swift auf der Kommandozeile als feste Testfälle prüfen. Anschließend können dieselben Testdaten in iOS-Unit-Tests übernommen werden. Das Abnahmekriterium lautet: ungültige Nachrichten zurückweisen — nicht für jede Eingabe Daten liefern.
Zuerst Frame-Format und Grenzen festlegen
Dieser Artikel verwendet ein minimales Protokoll: Das erste Byte enthält die Version, die nächsten vier Bytes die vorzeichenlose Nutzdatenlänge in Big-Endian-Reihenfolge, danach folgen die rohen Nutzdaten. Als Version ist ausschließlich 1 zulässig. Die Länge darf null sein und ist auf 1 MiB begrenzt. Ein einzelner Decodiervorgang muss die gesamte Eingabe verbrauchen; auch zusätzliche Bytes am Ende gelten als Fehler.
Dieser Vertrag für einzelne Frames ist kein Vertrag für einen Datenstrom. Ein Lesevorgang aus dem Netzwerk kann nur einen halben Frame liefern oder zwei Frames auf einmal enthalten. Die Empfangsschicht muss die Daten sammeln und in Frames aufteilen; der Parser erhält nur einen bereits vollständig abgegrenzten Frame. Ein unvollständiger Frame darf daher nicht unmittelbar als defekte Verbindung gewertet werden.
Das Längenfeld ist eine nicht vertrauenswürdige Eingabe. Prüfen Sie zuerst die Obergrenze und die verbleibenden Bytes, bevor Sie einen Ausschnitt bilden oder Speicher reservieren. Erzeugen Sie keinen Puffer allein auf Grundlage der vom Gegenüber angegebenen Länge.
Führen Sie zunächst xcrun swift --version aus und bewahren Sie die Ausgabe zur Toolchain auf. Das Beispiel benötigt nur Foundation, weder einen Simulator noch eine Signierungskonfiguration.
Expliziter Lesecursor statt Annahmen über Zeiger
Speichern Sie den folgenden Code als packet-check.swift. Der Reader verwendet intern ein Byte-Array; der Lesecursor wird nur nach einem erfolgreichen Lesevorgang weitergesetzt. Die Prüfung lautet „angeforderte Menge höchstens gleich verbleibende Menge“. So müssen nicht zwei nicht vertrauenswürdige Zahlen addiert und anschließend verglichen werden.
import Foundation
enum PacketError: Error {
case truncated, oversized, version, trailing
}
struct Reader {
let bytes: [UInt8]
private(set) var offset = 0
mutating func take(_ count: Int) throws -> [UInt8] {
guard count >= 0, count <= bytes.count - offset else {
throw PacketError.truncated
}
let start = offset
offset += count
return Array(bytes[start..<offset])
}
mutating func u32BE() throws -> UInt32 {
let b = try take(4)
return b.reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
}
}
func decode(_ data: Data) throws -> [UInt8] {
let limit = 1_048_576
guard data.count <= limit + 5 else {
throw PacketError.oversized
}
var reader = Reader(bytes: Array(data))
guard try reader.take(1)[0] == 1 else {
throw PacketError.version
}
let declared = try reader.u32BE()
guard let length = Int(exactly: declared), length <= limit else {
throw PacketError.oversized
}
let payload = try reader.take(length)
guard reader.offset == reader.bytes.count else {
throw PacketError.trailing
}
return payload
}
Die Ganzzahl Byte für Byte zusammenzusetzen macht die Big-Endian-Reihenfolge explizit und funktioniert unabhängig von der Speicherausrichtung. Ersetzen Sie Längenprüfungen nicht durch erzwungenes Entpacken von Optional-Werten. Fangen Sie Fehler auch nicht einfach ab, um dann leere Nutzdaten zurückzugeben: Sonst lässt sich ein gültiger leerer Frame nicht mehr von einer beschädigten Nachricht unterscheiden.
Die Umwandlung in ein Array erzeugt eine Kopie. Hier wird zunächst die Größe des gesamten Frames begrenzt, damit auch der Kopieraufwand begrenzt bleibt. Protokolle für große Dateien sollten stattdessen begrenzte Datenblöcke lesen, anstatt die Obergrenze zu erhöhen und weiterhin ganze Frames zu kopieren. Auch die Empfangsschicht muss ihren Sammelpuffer begrenzen; andernfalls kann der Speicher bereits erschöpft sein, bevor die Daten den Parser erreichen.
Gültige Nachrichten Byte für Byte abschneiden
Hängen Sie den folgenden Code an dieselbe Datei an. Die fest vorgegebenen Nutzdaten enthalten sowohl ein Nullbyte als auch Bytes mit gesetztem höchstem Bit, damit die Tests nicht nur Text abdecken. Geprüft werden alle unvollständigen Präfixe des gültigen Frames: Schon ein einziges fehlendes Byte muss einen Fehler auslösen — weder einen Absturz noch ein Teilergebnis.
func expectRejected(_ data: Data) {
do {
_ = try decode(data)
} catch {
return
}
fatalError("Invalid frame accepted")
}
let frame = Data([1, 0, 0, 0, 3, 0, 128, 255])
let payload = try decode(frame)
precondition(payload == [0, 128, 255])
for end in 0..<frame.count {
expectRejected(Data(frame.prefix(end)))
}
let empty = try decode(Data([1, 0, 0, 0, 0]))
precondition(empty.isEmpty)
expectRejected(Data([2, 0, 0, 0, 0]))
expectRejected(Data([1, 255, 255, 255, 255]))
expectRejected(Data([1, 0, 0, 0, 0, 7]))
Führen Sie xcrun swift packet-check.swift aus. Im Erfolgsfall gibt das Programm nichts aus und endet mit dem Exit-Code null. Wird eine ungültige Eingabe unerwartet akzeptiert, bricht das Programm ab. Die Pipeline kann direkt den Exit-Status prüfen; lokalisierte Logmeldungen müssen nicht ausgewertet werden.
Verwenden Sie hier keine Option, die bei optimierten Builds die Prüfungen deaktiviert. Nach der Übernahme in das Test-Target ersetzen Sie die Erfolgsprüfungen durch XCTAssertEqual und die Prüfungen auf Zurückweisung durch XCTAssertThrowsError. Prüfen Sie zusätzlich die konkreten Fehlertypen.
Gegenbeispiele für Länge und Byte-Reihenfolge ergänzen
Tests mit abgeschnittenen Frames allein beweisen noch nicht, dass die Byte-Reihenfolge stimmt. Ergänzen Sie einen festen Frame, dessen deklarierte Länge 256 beträgt und der tatsächlich 256 Bytes Nutzdaten enthält. Sein Längenfeld muss 00 00 01 00 lauten. Ein korrekter Parser muss die vollständigen Nutzdaten zurückgeben; eine Implementierung, die fälschlich Little-Endian verwendet, interpretiert die Länge als einen anderen Wert.
Testen Sie außerdem die Obergrenze selbst und die Obergrenze plus eins: Der erste Fall muss mit vollständigen Nutzdaten erfolgreich sein, der zweite bereits dann sofort zurückgewiesen werden, wenn nur der Header vorliegt. Ergänzen Sie auch eine Eingabe, bei der die deklarierte Länge kleiner als der tatsächliche Inhalt ist. So prüfen Sie, dass der strikte Einzelframe-Modus zusätzliche Bytes am Ende nicht stillschweigend ignoriert.
Erzeugen Sie nicht sämtliche Header in den Tests mit dem produktiven Encoder. Encoder und Decoder können denselben Fehler enthalten, sodass ein Roundtrip-Test trotzdem erfolgreich ist. Bewahren Sie mindestens einige manuell überprüfte Byte-Beispiele auf und dokumentieren Sie die Bedeutung jedes Feldes.
Fehlerverhalten in die Abnahmekriterien aufnehmen
Prüfen Sie vor dem Merge vier Punkte: Alle vollständigen gültigen Frames liefern exakt die ursprünglichen Bytes; alle abgeschnittenen Präfixe schlagen eindeutig fehl; unbekannte Versionen und Längen oberhalb der Grenze werden zurückgewiesen; nachgestellte Daten können die Einzelframe-Grenze nicht umgehen. Halten Sie in den Testprotokollen die Commit-Kennung, die Toolchain-Version und die Namen der Testfälle fest, aber keine echten Nutzdaten von Anwendern.
Bewahren Sie bei einem fehlgeschlagenen Test zuerst die ursprünglichen Bytes auf und reduzieren Sie sie anschließend auf das kürzeste Gegenbeispiel. Ändern Sie nicht zuerst die Obergrenze, nur damit der Test besteht. Kommen fachlich bedingt Kompression oder verschachtelte Felder hinzu, müssen auch die Größe nach dem Dekomprimieren und die Verschachtelungstiefe jeweils begrenzt werden. Die Längenbegrenzung des äußeren Frames ersetzt diese Regeln nicht.
Diese Abnahme prüft ausschließlich die Frame-Struktur, nicht die fachliche Gültigkeit der Nutzdaten. Trennen Sie Strukturparsing, Textdecodierung und fachliche Validierung. Wenn später neue Felder hinzukommen, lässt sich so feststellen, welche Schicht das Verhalten bei der Zurückweisung verändert hat.
Häufig gestellte Fragen
Warum UInt32 nicht direkt über einen Data-Zeiger laden?
Beliebige Byte-Positionen erfüllen nicht zwingend die Ausrichtungsanforderungen. Außerdem muss die Byte-Reihenfolge berücksichtigt werden. Einzelne Bytes explizit zusammenzusetzen vermeidet diese Annahmen.
Sind leere Nutzdaten ein Fehler?
Das legt das Protokoll fest. Das Beispiel erlaubt Länge null. Verlangt die Anwendung Inhalt, braucht sie eine ausdrückliche Prüfung und einen eigenen Testfall.
Ersetzen Tests aller Schnittstellen im Paket Fuzzing?
Nein. Sie prüfen nur die Präfixe eines bestimmten gültigen Frames. Feldkombinationen und zustandsabhängige Mehrfachframes brauchen zusätzliche Tests mit Ressourcenlimits.
Schritte auf einem exklusiven Cloud-Mac ausführen
Mieten Sie einen kompletten physischen Mac mini statt einer virtuellen Maschine. Wählen Sie einen Zeitraum von Tag, Woche, Monat oder Quartal. Prüfen Sie zuerst Modell und Standort und bestätigen Sie dann Ihre Bestellung. Maßgeblich ist der Verfügbarkeitsstatus, den die Konsole in Echtzeit anzeigt.