Tester les limites d’un décodeur binaire iOS sur Mac cloud

Tester les limites d’un décodeur binaire iOS sur Mac cloud

Tester les limites d’un décodeur binaire iOS sur Mac cloud

La compilation à distance a réussi, mais un message binaire reçu sur l’appareil fait planter l’application. Avant d’incriminer le réseau, vérifiez le décodage : la longueur annoncée peut dépasser le contenu réel, la position de lecture peut être mal alignée, ou un entier big-endian peut avoir été interprété dans l’ordre natif de la machine. Sur un Mac cloud VpsKeep, vous pouvez d’abord vérifier ces entrées avec des cas fixes en exécutant Swift en ligne de commande, puis reprendre les mêmes échantillons dans des tests unitaires iOS. Le critère d’acceptation consiste à rejeter les messages invalides, pas à renvoyer des données pour chaque entrée.

Définir d’abord le format des trames et leurs limites

Cet article utilise un protocole minimal : le premier octet indique la version, les quatre suivants contiennent la longueur non signée de la charge utile en big-endian, puis vient la charge utile brute. Seule la version 1 est acceptée. La longueur peut être nulle et ne doit pas dépasser 1 MiB. Chaque décodage doit consommer la totalité de l’entrée ; des octets supplémentaires en fin de trame constituent aussi une erreur.

Ce contrat portant sur une seule trame ne définit pas le fonctionnement d’un flux. Une lecture réseau peut ne fournir qu’une demi-trame, ou contenir deux trames à la fois. La couche de réception doit accumuler les données et les découper en trames ; le parseur ne reçoit qu’une trame dont les limites ont déjà été établies. Une erreur due à une trame incomplète ne doit donc pas être assimilée d’emblée à une connexion défectueuse.

Le champ de longueur est une entrée non fiable. Vérifiez la limite maximale et le nombre d’octets restants avant d’extraire une portion des données ou d’allouer de la mémoire. Ne créez pas de tampon en vous fondant directement sur la longueur annoncée par l’autre extrémité.

Commencez par exécuter xcrun swift --version et conservez la sortie décrivant la chaîne d’outils. L’exemple ne dépend que de Foundation : aucun simulateur ni aucune configuration de signature ne sont nécessaires.

Utiliser un curseur explicite plutôt que des hypothèses sur les pointeurs

Enregistrez le code suivant dans packet-check.swift. Le lecteur utilise un tableau d’octets en interne ; le curseur n’avance qu’après une lecture réussie. La vérification consiste à s’assurer que « la quantité demandée ne dépasse pas la quantité restante », afin d’éviter d’additionner deux valeurs non fiables avant de les comparer.

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
}

Assembler l’entier octet par octet rend l’ordre big-endian explicite, sans dépendre de l’alignement des adresses. Ne remplacez pas les contrôles de longueur par un déballage forcé des valeurs optionnelles. Surtout, ne masquez pas les erreurs en renvoyant une charge utile vide : il deviendrait impossible de distinguer une trame vide valide d’un message endommagé.

La conversion en tableau entraîne une copie. Ici, la taille totale de la trame est limitée en amont, ce qui borne aussi le coût de cette copie. Pour un protocole transportant de gros fichiers, préférez des lectures par blocs de taille bornée, plutôt que de relever la limite tout en continuant à copier des trames entières. La couche de réception doit également limiter son tampon d’accumulation ; sinon, la mémoire peut être épuisée avant même que les données atteignent le parseur.

Tronquer un message valide octet par octet

Ajoutez le code suivant à la fin du même fichier. La charge utile fixe contient un octet nul et des octets dont le bit de poids fort est à un, pour ne pas limiter les tests à du texte. Le test parcourt tous les préfixes incomplets de la trame valide : même s’il ne manque qu’un octet, le décodeur doit lever une erreur, sans planter ni renvoyer un résultat partiel.

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

Exécutez xcrun swift packet-check.swift. Le résultat attendu est une absence de sortie et un code de retour nul. Si une entrée invalide est acceptée à tort, le programme s’interrompt. Le pipeline peut vérifier directement le code de retour, sans analyser des journaux localisés.

N’utilisez pas ici d’option qui désactive les vérifications dans les compilations optimisées. Une fois le code intégré à la cible de tests, remplacez les assertions de réussite par XCTAssertEqual et celles de rejet par XCTAssertThrowsError, puis vérifiez également les types d’erreur précis.

Ajouter des contre-exemples pour la longueur et l’ordre des octets

Les tests de troncature ne suffisent pas à prouver que l’ordre des octets est correct. Ajoutez une trame fixe dont la longueur annoncée est 256 et dont la charge utile contient réellement 256 octets. Le champ de longueur doit être 00 00 01 00. Un parseur correct doit renvoyer l’intégralité de la charge utile ; une implémentation utilisant par erreur le little-endian interprétera cette longueur comme une autre valeur.

Testez aussi la limite elle-même, puis la limite augmentée de un : le premier cas doit réussir si la charge utile est complète, tandis que le second doit être rejeté immédiatement, même si seul l’en-tête est présent. Ajoutez également une entrée dont la longueur annoncée est inférieure au contenu réel, afin de vérifier que le mode strict à trame unique n’ignore pas silencieusement les octets finaux.

Évitez de générer tous les en-têtes de test avec l’encodeur de production. L’encodeur et le décodeur peuvent partager la même erreur, auquel cas un test aller-retour réussira malgré tout. Conservez au moins quelques échantillons d’octets vérifiés manuellement et indiquez la signification de chaque champ.

Inclure le comportement en erreur dans les critères d’acceptation

Avant la fusion, vérifiez quatre points : toutes les trames complètes et valides restituent exactement les octets d’origine ; tous les préfixes tronqués échouent explicitement ; les versions inconnues et les longueurs dépassant la limite sont rejetées ; les données finales ne permettent pas de contourner la frontière d’une trame unique. Consignez l’identifiant du commit, la version de la chaîne d’outils et les noms des échantillons dans les comptes rendus de test, sans enregistrer de véritables charges utiles provenant des utilisateurs.

Lorsqu’un test échoue, conservez d’abord les octets d’origine, puis réduisez l’entrée au contre-exemple le plus court. Ne commencez pas par modifier la limite pour faire réussir le test. Si les besoins métier introduisent de la compression ou des champs imbriqués, bornez séparément la taille après décompression et la profondeur d’imbrication. La limite de longueur externe ne remplace pas ces règles.

Ces critères d’acceptation portent uniquement sur la structure des trames, pas sur la validité métier de la charge utile. Séparez l’analyse structurelle, le décodage du texte et la validation métier : lors de l’ajout de nouveaux champs, vous pourrez ainsi identifier la couche qui a modifié le comportement de rejet.

Questions fréquentes

Pourquoi ne pas lire un UInt32 directement depuis un pointeur Data ?

Une position arbitraire peut ne pas respecter l’alignement requis. Il faut aussi traiter l’ordre des octets. Assembler les octets explicitement évite ces hypothèses.

Faut-il rejeter une charge utile vide ?

Cela dépend du contrat du protocole. L’exemple accepte une longueur nulle. Si le contenu est obligatoire, ajoutez une validation explicite et un test dédié.

Les tests de troncature remplacent-ils le fuzzing ?

Non. Ils couvrent uniquement les préfixes d’une trame valide donnée. Les combinaisons de champs et les échanges à plusieurs trames demandent des tests supplémentaires bornés.

Exécutez vos étapes sur un Mac cloud dédié

Louez un Mac mini physique complet, et non une machine virtuelle. Choisissez une durée à la journée, à la semaine, au mois ou au trimestre. Vérifiez le modèle et la région avant de confirmer votre commande ; la disponibilité réelle est indiquée en temps réel dans la console.