Strikte iOS-Hex-Codecs auf dem Cloud-Mac testen
Wenn der Anwendungscode unter Windows fertiggestellt ist und das iOS-Projekt zum Erstellen des Builds auf einen Cloud-Mac umzieht, müssen auch die Hexadezimalfelder der Schnittstellen zusammen mit dem Code geprüft werden. Gerätenachrichten, Hashwerte und binäre Kennungen verwenden häufig diese Darstellung. Dass eine Zeichenfolge plausibel aussieht, bedeutet jedoch nicht, dass beim Dekodieren die richtigen Bytes entstehen. Insbesondere darf der Decoder keine ungültigen Zeichen übergehen, führende Nullen verlieren oder unvollständige Eingaben automatisch zu anderen Daten ergänzen.
Dieser Artikel legt den Byte-Vertrag zunächst mit einem eigenständigen Swift-Skript fest und zeigt anschließend, wie sich die Prüfungen in die Projekttests übernehmen lassen. Das Verfahren benötigt weder Netzwerkdienste noch Signierungsmaterial und eignet sich für die Ausführung vor dem Archivieren.
Zuerst den Ein- und Ausgabevertrag festlegen
Der Decoder akzeptiert ausschließlich die ASCII-Zeichen 0–9, a–f und A–F. Je zwei Zeichen stehen für ein Byte. Der Encoder gibt ausschließlich Kleinbuchstaben aus und stellt jedes Byte mit genau zwei Zeichen dar. Groß- und Kleinschreibung dürfen gleichwertig sein; Leerzeichen, Zeilenumbrüche, das Präfix 0x und Zeichen in voller Breite gehören dagegen nicht zu diesem Protokoll.
Eine leere Zeichenfolge steht auf dieser grundlegenden Ebene für leere Daten. Darf ein fachliches Feld nicht leer sein, muss die fachliche Validierung dies ablehnen. Der allgemeine Decoder sollte nicht stillschweigend feldspezifische Regeln übernehmen. Die Eingabegrenze wird anhand der dekodierten Byteanzahl definiert und schließt den Grenzwert ausdrücklich ein.
Striktes Dekodieren und das Aufbereiten manueller Eingaben sind zwei getrennte Aufgaben. Soll das Einfügen von Daten mit Leerzeichen unterstützt werden, muss die Anpassungsschicht der Benutzeroberfläche diese zunächst ausdrücklich umwandeln und erst dann an den strikten Decoder übergeben. Maschinenschnittstellen sollten Eingaben nicht standardmäßig korrigieren.
Außerdem ist Hexadezimal lediglich eine Darstellung von Bytes. Sie bietet weder Verschlüsselung noch Integritätsschutz oder Herkunftsauthentifizierung. Eine erfolgreiche Dekodierungsprüfung sagt daher nichts darüber aus, ob eine Nachricht vertrauenswürdig ist.
Strikten Decoder und Encoder mit fester Ausgabebreite implementieren
Speichern Sie den folgenden Inhalt als HexContract.swift. Die Implementierung prüft zuerst Länge und Größenlimit und reserviert erst danach Speicher für die Ausgabe. Sie untersucht die Zeichen Byte für Byte und vermeidet compactMap, das fehlgeschlagene Umwandlungen überspringen könnte. Das Beispiel definiert einen eigenen Fehlertyp, damit die Tests verschiedene Fehlerursachen unterscheiden können.
import Foundation
enum HexError: Error, Equatable {
case invalidLimit
case oddLength
case tooLarge
case invalidCharacter
}
func nibble(_ c: UInt8) throws -> UInt8 {
switch c {
case 48...57: return c - 48
case 65...70: return c - 65 + 10
case 97...102: return c - 97 + 10
default: throw HexError.invalidCharacter
}
}
func decodeHex(_ text: String, maxBytes: Int) throws -> Data {
guard maxBytes >= 0 else { throw HexError.invalidLimit }
let count = text.utf8.count
guard count % 2 == 0 else { throw HexError.oddLength }
guard count / 2 <= maxBytes else { throw HexError.tooLarge }
var iterator = text.utf8.makeIterator()
var output = Data()
output.reserveCapacity(count / 2)
while let high = iterator.next() {
guard let low = iterator.next() else {
throw HexError.oddLength
}
let a = try nibble(high)
let b = try nibble(low)
output.append((a << 4) | b)
}
return output
}
func encodeHex(_ data: Data) -> String {
let alphabet = Array("0123456789abcdef".utf8)
var output = [UInt8]()
for byte in data {
output.append(alphabet[Int(byte >> 4)])
output.append(alphabet[Int(byte & 15)])
}
return String(decoding: output, as: UTF8.self)
}
Gezählt werden hier UTF-8-Bytes, nicht die für Benutzer sichtbaren Zeichen. Eingaben außerhalb von ASCII müssen in jedem Fall scheitern. Auch die Reihenfolge der Fehlerprüfungen gehört zum Vertrag: zuerst ein negatives Größenlimit, dann eine ungerade Länge, danach eine Überschreitung des Limits und erst zuletzt die Prüfung des erlaubten Zeichensatzes.
Die Größenprüfung berechnet nicht maxBytes * 2 und vermeidet damit einen Überlauf durch die Multiplikation des Limits. Sie begrenzt die Speicherreservierung für die Ausgabe, nicht jedoch den Empfang der Anfrage. Die Schnittstelle muss die Größe weiterhin beim Einlesen des Request-Bodys begrenzen. Sie darf nicht zunächst eine beliebig große Zeichenfolge entgegennehmen und anschließend vom Decoder erwarten, Speicher zu sparen.
Feste Testvektoren und Ablehnungsfälle ergänzen
Hängen Sie die folgenden Tests an dieselbe Datei an. Die festen Testvektoren prüfen führende Nullen sowie Groß- und Kleinschreibung. Für sämtliche einzelnen Bytewerte dient das Ergebnis der Standardformatierung als unabhängige Referenz. So wird verhindert, dass Encoder und Decoder denselben Fehler machen und der Roundtrip trotzdem gelingt.
func reject(
_ text: String,
limit: Int = 16,
expected: HexError
) {
do {
_ = try decodeHex(text, maxBytes: limit)
preconditionFailure("Expected rejection")
} catch let error as HexError {
precondition(error == expected, "Unexpected error")
} catch {
preconditionFailure("Unexpected error type")
}
}
let fixed = try decodeHex("000aFf", maxBytes: 3)
precondition(fixed == Data([0, 10, 255]))
precondition(encodeHex(fixed) == "000aff")
let empty = try decodeHex("", maxBytes: 0)
precondition(empty.isEmpty)
for value in 0...255 {
let reference = String(format: "%02x", value)
let decoded = try decodeHex(reference, maxBytes: 1)
precondition(decoded == Data([UInt8(value)]))
precondition(encodeHex(Data([UInt8(value)])) == reference)
}
reject("f", expected: .oddLength)
reject("0g", expected: .invalidCharacter)
reject("0x00", expected: .invalidCharacter)
reject("00\n0", expected: .invalidCharacter)
reject("00", expected: .invalidCharacter)
reject("0000", limit: 1, expected: .tooLarge)
reject("", limit: -1, expected: .invalidLimit)
let sequence = Data((0...255).map { UInt8($0) })
let restored = try decodeHex(encodeHex(sequence), maxBytes: 256)
precondition(restored == sequence)
Eingaben mit ungerader Länge müssen ausdrücklich scheitern. Aus f darf nicht durch Raten 0f oder f0 werden. Decken Sie außerdem den Fall ab, dass ein ungültiges Zeichen am Ende steht, und stellen Sie sicher, dass die Implementierung nicht den bereits dekodierten Anfang zurückgibt. Bei sicherheitsrelevanten Feldern muss ein Fehler die gesamte Verarbeitung scheitern lassen, statt ein weiterhin verwendbares Teilergebnis zu liefern.
Auf dem Cloud-Mac ausführen und ins Projekt integrieren
Prüfen Sie zunächst, ob die aktuellen Entwicklungswerkzeuge verfügbar sind, und führen Sie dann das Skript aus:
xcrun --find swift
xcrun swift HexContract.swift
Bei Erfolg erzeugt das Skript keine Ausgabe und endet mit dem Exitcode null. Eine fehlgeschlagene Assertion oder ein unbehandelter Fehler führt zu einem Exitcode ungleich null. Führen Sie dieses Skript nicht in einem Optimierungsmodus aus, der die Prüfungen deaktiviert, und ergänzen Sie in der Pipeline keine Behandlung, die den Exitcode ignoriert.
Das eigenständige Skript prüft die aktuelle Swift-Implementierung unter macOS. Für die reguläre Integration legen Sie den Codec in einem gemeinsam genutzten Modul der Anwendung ab und übernehmen die obigen Testvektoren in XCTest. Ersetzen Sie die Prüfungen durch XCTAssertEqual und XCTAssertThrowsError und führen Sie sie im vorhandenen iOS-Test-Target aus. Nur so lassen sich auch Probleme mit der Target-Zugehörigkeit, bedingter Kompilierung und den tatsächlichen Aufrufpfaden erkennen.
Gibt es bereits eine Implementierung, pflegen Sie keine zusätzliche Kopie nur für die Tests. Lassen Sie die Tests stattdessen direkt das Produktionsmodul importieren. Andernfalls kann das Skript erfolgreich durchlaufen, während die Anwendung weiterhin einen alten Decoder verwendet.
Die Regression mit einer Grenzfall-Checkliste abschließen
Prüfen Sie vor dem Zusammenführen jeden Punkt: Wird ein Nullbyte als 00 kodiert? Entspricht eine leere Eingabe den Feldregeln? Wird die maximal zulässige Größe akzeptiert und eine Überschreitung um ein Byte abgelehnt? Liefert eine ungültige Eingabe keinerlei Daten zurück? Ergänzen Sie außerdem deterministische lange Bytefolgen, statt sich ausschließlich auf zufällige Daten zu verlassen.
Gemischte Groß- und Kleinschreibung darf akzeptiert werden. Verwenden Sie die ursprüngliche Zeichenfolge aber nicht direkt als eindeutigen Schlüssel: aa und AA stehen für dieselben Bytes. Für eine normalisierte Speicherung dekodieren Sie zunächst strikt und kodieren anschließend einheitlich neu. Eine Umwandlung der Groß- und Kleinschreibung vor der Validierung ersetzt keine Byteprüfung.
Bewahren Sie abschließend die Toolchain-Version, die Namen der fehlgeschlagenen Testfälle und das Exit-Ergebnis des Testlaufs auf. Bei echten Gerätenachrichten oder Tokens dürfen vollständige Eingaben nicht ins Log gelangen; verwenden Sie zur Reproduktion feste synthetische Testvektoren. Ziel der Abnahme ist nicht, jede Zeichenfolge irgendwie umzuwandeln, sondern gültige Bytes unverändert hin und zurück zu übertragen und Eingaben außerhalb des Vertrags eindeutig zurückzuweisen.
Häufig gestellte Fragen
Soll der Decoder Leerzeichen entfernen und ungerade Eingaben auffüllen?
An einer Maschinenschnittstelle nicht. Solche Eingaben werden abgewiesen. Erlaubte Korrekturen für manuelle Eingaben gehören in einen separaten Adapter.
Reicht ein Test aus Codierung und anschließender Decodierung?
Nein. Beide Funktionen können denselben Fehler enthalten. Ergänzen Sie feste Vektoren, alle Einzelbytewerte, ungültige Eingaben und führende Nullen.
Miete einen exklusiven Mac für deinen Build.
Wähle einen nahegelegenen Standort und prüfe anschließend Arbeitsspeicher, Speicherplatz und Abrechnungszeitraum für dein Projekt. Du erhältst einen vollständigen physischen Rechner exklusiv, keine virtuelle Maschine. Den aktuellen Verfügbarkeitsstatus siehst du in Echtzeit in der Konsole.