Une équipe compacte les éléments « lus » dans un ensemble de bits. Les membres en Asie-Pacifique écrivent les données, puis ceux d’Amérique du Nord les lisent : les tests de relecture en local passent tous, mais une autre version du client révèle que le premier et le huitième indicateur sont inversés. Le problème ne vient généralement pas du réseau, mais de conventions binaires mal définies. Un Mac dans le cloud permet de systématiser ces vérifications : chaque modification est comparée aux mêmes échantillons d’octets, au lieu de se fier au seul bon fonctionnement de l’interface.
Définir d’abord le format d’échange, sans reproduire la disposition en mémoire
Cet article propose un format de référence indépendant de la logique métier : les quatre premiers octets contiennent le nombre de bits sous forme d’un entier non signé en grand-boutiste, immédiatement suivi de la charge utile de l’ensemble de bits. Les indices commencent à zéro et, dans chaque octet, le bit de poids faible vient en premier. L’indice zéro correspond donc au bit de poids faible du premier octet, et l’indice huit à celui du deuxième.
Le nombre d’octets de la charge utile se calcule en divisant le nombre de bits par huit, puis en ajoutant un si le reste est non nul. Pour zéro bit, seul l’en-tête est présent. Les bits de poids fort inutilisés du dernier octet doivent être à zéro ; tout octet supplémentaire est également rejeté. Ce format ne comporte aucun champ de version. Pour l’intégrer à un protocole existant, il faut définir explicitement la version dans l’enveloppe du protocole, et non remplacer directement les anciennes données.
Un aller-retour réussi prouve seulement que les deux extrémités sont d’accord entre elles. Si l’encodeur et le décodeur inversent tous deux l’ordre des bits, le test passe quand même. Il faut donc aussi disposer de vecteurs d’octets calculés à la main.
Ne sérialisez pas directement la mémoire d’une structure, des mots machine ou des tampons d’entiers natifs. Cela ferait entrer l’alignement, le boutisme et les détails d’implémentation dans le format de stockage.
Écrire une implémentation de référence avec une limite d’entrée
Dans un répertoire dédié du Mac dans le cloud, enregistrez le fichier sous le nom BitsetCheck.swift, puis exécutez-le avec Swift fourni par la chaîne d’outils de développement. La limite est ici fixée à un million de bits : c’est une mesure de protection propre à cet exemple, pas une limite théorique du format. Le décodeur vérifie d’abord cette limite et la longueur totale, puis crée le tableau de résultat, afin d’éviter une allocation importante fondée sur un en-tête non fiable.
import Foundation
enum FormatError: Error {
case invalid
}
let maxBits = 1_000_000
func encode(_ bits: [Bool]) throws -> [UInt8] {
guard bits.count <= maxBits else {
throw FormatError.invalid
}
let count = UInt32(bits.count)
var bytes = [
UInt8(truncatingIfNeeded: count >> 24),
UInt8(truncatingIfNeeded: count >> 16),
UInt8(truncatingIfNeeded: count >> 8),
UInt8(truncatingIfNeeded: count)
]
let size = bits.count / 8 + (bits.count % 8 == 0 ? 0 : 1)
bytes += Array(repeating: UInt8(0), count: size)
for index in bits.indices where bits[index] {
bytes[4 + index / 8] |= UInt8(1) << (index % 8)
}
return bytes
}
func decode(_ bytes: [UInt8]) throws -> [Bool] {
guard bytes.count >= 4 else {
throw FormatError.invalid
}
let raw = bytes.prefix(4).reduce(UInt32(0)) {
($0 << 8) | UInt32($1)
}
guard raw <= UInt32(maxBits) else {
throw FormatError.invalid
}
let count = Int(raw)
let size = count / 8 + (count % 8 == 0 ? 0 : 1)
guard bytes.count == 4 + size else {
throw FormatError.invalid
}
let remainder = count % 8
if remainder != 0 {
let allowed = UInt8((1 << remainder) - 1)
guard bytes[bytes.count - 1] & ~allowed == 0 else {
throw FormatError.invalid
}
}
return (0..<count).map {
bytes[4 + $0 / 8] & (UInt8(1) << ($0 % 8)) != 0
}
}
func require(_ condition: Bool) {
if !condition {
fatalError("Bitset check failed")
}
}
func mustReject(_ bytes: [UInt8]) {
do {
_ = try decode(bytes)
} catch {
return
}
fatalError("Invalid input accepted")
}
let expected: [UInt8] = [0, 0, 0, 9, 0x81, 0x01]
var sample = Array(repeating: false, count: 9)
for index in [0, 7, 8] {
sample[index] = true
}
require(try encode(sample) == expected)
require(try decode(expected) == sample)
for count in [0, 1, 7, 8, 9, 63, 64, 65] {
let bits = (0..<count).map { $0 % 3 == 0 }
let bytes = try encode(bits)
require(try decode(bytes) == bits)
}
mustReject([])
mustReject([0, 0, 0, 9, 0x81])
mustReject(expected + [0])
mustReject([0, 0, 0, 1, 0x80])
mustReject([0xFF, 0xFF, 0xFF, 0xFF])
Exécutez xcrun swift BitsetCheck.swift. En cas de succès, le programme se termine sans rien afficher ; une assertion non satisfaite ou une erreur non gérée provoque la sortie du processus en échec. Lors de l’intégration au projet, remplacez les fonctions de référence par les véritables points d’entrée d’encodage et de décodage, tout en conservant les vecteurs et les tests de rejet. Sinon, les tests ne valideront toujours que l’exemple.
Tester séparément l’encodage et le décodage avec des vecteurs de référence
Pour neuf bits dont ceux d’indices zéro, sept et huit sont activés, la charge utile doit être 81 01 en hexadécimal. Le premier octet couvre à la fois le bit de poids faible et celui de poids fort ; le deuxième vérifie la continuité au passage d’un octet à l’autre. L’en-tête précise quant à lui que la longueur logique n’est pas la longueur de la charge utile.
Ajoutez aussi quatre catégories d’échantillons : tous les bits à zéro, tous à un, seul le premier à un et seul le dernier à un. Huit bits tous à un doivent donner FF, et neuf bits tous à un doivent donner FF 01, jamais FF FF. Les bits inutilisés ne sont pas une zone où laisser des valeurs quelconques sous prétexte qu’elles ne seront pas lues. Une représentation canonique est nécessaire pour garantir la cohérence des comparaisons d’octets, du calcul des empreintes et des clés de cache.
Les vecteurs de référence doivent être conservés dans le dépôt de code. Lors d’une modification du protocole, faites d’abord valider les octets attendus, puis mettez l’implémentation à jour. Ne laissez pas l’encodeur à tester générer automatiquement les fichiers de résultats attendus et les écraser : cela reviendrait à laisser le composant testé fournir ses propres réponses.
Traiter les entrées invalides comme un axe de test à part entière
Les échantillons tronqués doivent couvrir deux cas : un en-tête de moins de quatre octets, et un en-tête complet suivi d’une charge utile à laquelle il manque un octet. Les octets supplémentaires doivent eux aussi être rejetés, faute de quoi une erreur d’assemblage des paquets en amont passerait inaperçue. Pour tester un bourrage non nul, associez une longueur d’un bit à 80 : ce cas permet de détecter aussi bien une confusion dans l’ordre des bits que l’absence de validation des bits inutilisés en fin de charge utile.
La limite de longueur doit être définie selon le nombre d’éléments autorisé par l’application. Il faut aussi limiter le nombre total d’octets à la réception des données dans la couche applicative. Le tableau reçu par le décodeur d’exemple existe déjà : celui-ci ne peut donc pas empêcher la couche réseau de charger au préalable une réponse trop volumineuse. Une réception en flux nécessite sa propre limite sur le cumul des octets reçus.
N’allouez pas de mémoire d’après la longueur indiquée dans l’en-tête avant de vérifier la charge utile réelle. N’utilisez pas non plus la formule « ajouter sept à la longueur, puis diviser par huit » sur un entier arbitraire provenant de l’extérieur. Ici, on vérifie d’abord la limite, puis on calcule la taille par division et reste. L’ordre des vérifications fait lui aussi partie du contrat de test.
Conserver des preuves comparables dans les tests de non-régression
Placez le script dans le répertoire de tests du dépôt et faites-le exécuter l’implémentation métier issue du même commit. Consignez l’identifiant du commit, la sortie de xcrun swift --version et le nom du vecteur en échec. Ne journalisez ni l’état de lecture réel des utilisateurs ni l’intégralité des charges utiles métier.
Il est recommandé d’attribuer un nom stable à chaque type d’erreur, par exemple troncature, bourrage invalide ou dépassement de longueur, plutôt que de tout ramener à une erreur réseau. L’erreur unique utilisée ci-dessus sert seulement à raccourcir le code de référence ; l’implémentation de production doit conserver des catégories utiles au diagnostic.
Terminez par une vérification en introduisant volontairement des défauts : inversez l’ordre des bits dans chaque octet et confirmez que les vecteurs de référence échouent ; supprimez la vérification des bits inutilisés en fin de charge utile et confirmez que les tests détectent le bourrage invalide. Ce n’est que si ces défauts font effectivement échouer la tâche que les tests de non-régression dans le cloud protègent réellement les limites du format, au lieu de simplement répéter un aller-retour réussi.
Questions fréquentes
Pourquoi un test d’encodage puis de décodage ne suffit-il pas ?
L’encodeur et le décodeur peuvent partager la même erreur d’ordre des bits. Des octets de référence calculés à la main permettent de vérifier le format indépendamment de cette erreur commune.
Que faire des bits inutilisés dans le dernier octet ?
Ce format impose des bits de poids fort inutilisés à zéro. Le décodeur refuse tout bourrage non nul afin de garantir une représentation canonique unique.
Louez un Mac mini au rythme de votre projet
Choisissez un nœud physique dédié et payez à la journée, à la semaine, au mois ou au trimestre. Ajoutez du temps en période de forte demande ; la location s’arrête à l’échéance sans renouvellement. La disponibilité réelle est indiquée en temps réel dans la console.