Tester un codec hexadécimal iOS strict sur Mac cloud
Une fois le code métier développé sous Windows, le transfert du projet iOS vers un Mac cloud pour générer le livrable doit aussi s’accompagner de tests sur les champs hexadécimaux des interfaces. Les messages d’appareils, les empreintes et les identifiants binaires utilisent souvent cette représentation : une chaîne qui semble correcte ne garantit pas que les octets décodés le soient. Il faut notamment éviter d’ignorer les caractères invalides, de perdre les zéros initiaux ou de compléter automatiquement une entrée incomplète au point d’en faire une autre donnée.
Cet article définit le contrat de conversion des octets à l’aide d’un script Swift autonome, puis explique comment l’intégrer aux tests du projet. Sans service réseau ni éléments de signature, ce contrôle peut être exécuté avant l’archivage.
Définir le contrat d’entrée et de sortie
Le décodeur accepte uniquement les caractères ASCII 0–9, a–f et A–F, à raison de deux caractères par octet. L’encodeur produit systématiquement des minuscules, avec exactement deux caractères par octet. Majuscules et minuscules peuvent être équivalentes, mais les espaces, les sauts de ligne, le préfixe 0x et les caractères pleine chasse ne font pas partie de ce protocole.
Au niveau du codec, une chaîne vide représente des données vides. Si un champ métier ne peut pas être vide, c’est à la validation métier de le refuser : le décodeur générique ne doit pas appliquer implicitement les règles propres au champ. La limite d’entrée se calcule en nombre d’octets décodés et doit être explicitement inclusive.
Le décodage strict et la préparation d’une saisie humaine relèvent de deux responsabilités distinctes. Pour autoriser le collage de données contenant des espaces, effectuez une conversion explicite dans la couche d’adaptation de l’interface utilisateur, puis transmettez le résultat au décodeur strict. Les interfaces machine ne doivent pas corriger les entrées par défaut.
De même, l’hexadécimal n’est qu’une représentation des octets : il n’assure ni chiffrement, ni intégrité, ni authentification de la source. Un décodage réussi ne permet donc pas de conclure qu’un message est digne de confiance.
Implémenter un décodage strict et un encodage à largeur fixe
Enregistrez le contenu suivant dans HexContract.swift. L’implémentation vérifie d’abord la longueur et la limite autorisée, puis alloue la sortie. Elle contrôle les caractères octet par octet, sans recourir à compactMap, qui pourrait écarter les éléments dont la conversion échoue. Cet exemple utilise un type d’erreur dédié afin que les tests puissent distinguer les causes d’échec.
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)
}
Le comptage porte ici sur les octets UTF-8, et non sur les caractères visibles par l’utilisateur ; toute entrée non ASCII doit finir par être rejetée. L’ordre de détection des erreurs fait lui aussi partie du contrat : limite négative en premier, puis longueur impaire, puis dépassement de capacité, et enfin vérification des caractères autorisés.
Le contrôle de limite ne calcule pas maxBytes * 2, ce qui évite un dépassement arithmétique lié à cette multiplication. Il limite l’allocation de sortie, pas la réception de la requête : l’interface doit toujours imposer une taille maximale pendant la lecture du corps de la requête. Il ne faut pas accepter une chaîne de taille illimitée en espérant que le décodeur réduira ensuite la consommation mémoire.
Ajouter des vecteurs fixes et des cas de rejet
Ajoutez les tests suivants à la suite, dans le même fichier. Les vecteurs fixes vérifient les zéros initiaux et la casse. Le test de toutes les valeurs possibles sur un octet utilise le résultat du formatage standard comme référence indépendante : cela évite qu’une erreur commune à l’encodeur et au décodeur passe inaperçue grâce à un aller-retour pourtant réussi.
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)
Les entrées de longueur impaire doivent échouer explicitement : il ne faut pas interpréter arbitrairement f comme 0f ou f0. Couvrez également le cas où un caractère invalide se trouve à la fin, afin de vérifier que l’implémentation ne renvoie pas la partie déjà décodée. Pour les champs sensibles du point de vue de la sécurité, l’échec doit être total, sans résultat partiel encore exploitable.
Exécuter les contrôles sur le Mac cloud et les intégrer au projet
Vérifiez d’abord que les outils de développement sont disponibles dans l’environnement courant, puis exécutez le script :
xcrun --find swift
xcrun swift HexContract.swift
En cas de réussite, le script ne produit aucune sortie et son code de sortie est nul. Une assertion en échec ou une erreur non gérée entraîne un code de sortie non nul. N’exécutez pas ce script avec un mode d’optimisation qui désactive les vérifications, et n’ajoutez pas au pipeline de traitement qui ignore le code de sortie.
Le script autonome valide l’implémentation Swift dans l’environnement macOS courant. Pour l’intégration au projet, placez le codec dans un module partagé de l’application, transférez les vecteurs ci-dessus dans XCTest, remplacez les vérifications par XCTAssertEqual et XCTAssertThrowsError, puis exécutez-les dans la cible de tests iOS existante. C’est ainsi que vous pourrez aussi détecter les problèmes d’appartenance aux cibles, de compilation conditionnelle et de chemins d’appel réels.
Si une implémentation existe déjà, n’en maintenez pas une copie réservée aux tests : faites importer directement le module de production par les tests. Sinon, le script pourrait réussir alors que l’application utilise encore l’ancien décodeur.
Finaliser la régression avec une liste de cas limites
Avant la fusion, vérifiez chaque point : un octet nul est-il encodé en 00 ? Une entrée vide respecte-t-elle les règles du champ ? La capacité maximale est-elle acceptée ? Un dépassement d’un octet est-il rejeté ? Une entrée invalide est-elle refusée sans renvoyer la moindre donnée ? Ajoutez aussi de longues séquences déterministes, plutôt que de compter uniquement sur des données aléatoires.
Vous pouvez accepter un mélange de majuscules et de minuscules, mais n’utilisez pas directement la chaîne d’origine comme clé unique : aa et AA représentent les mêmes octets. Pour un stockage sous forme canonique, commencez par un décodage strict, puis réencodez dans le format uniforme. Avant validation, une conversion de casse ne remplace pas le contrôle des octets.
Enfin, conservez simplement la version de la chaîne d’outils, le nom des cas en échec et le résultat de sortie des tests. Lorsque vous travaillez sur de véritables messages d’appareils ou des jetons, ne consignez pas l’entrée complète dans les journaux ; reproduisez les cas avec des vecteurs synthétiques fixes. L’objectif de la validation n’est pas de rendre toute chaîne convertible, mais de garantir un aller-retour exact des octets valides et un arrêt explicite pour les entrées hors contrat.
Questions fréquentes
Faut-il supprimer les espaces ou compléter une longueur impaire ?
Non pour une interface machine : ces entrées doivent être rejetées. Une correction autorisée dans un écran de saisie doit passer par un adaptateur distinct.
Un aller-retour encodage puis décodage suffit-il ?
Non, les deux fonctions peuvent partager le même défaut. Ajoutez des vecteurs fixes, tous les octets possibles, les entrées invalides et les zéros initiaux.
Louez un Mac dédié pour vos builds.
Choisissez le nœud le plus proche, puis vérifiez la mémoire, le stockage et la durée de facturation requis par votre projet. Vous disposez d’une machine physique entière, et non d’une machine virtuelle. Son état réel est indiqué en temps réel dans la console.