Valider le décodage UTF-8 incrémental pour iOS sur Mac distant

Valider le décodage UTF-8 incrémental pour iOS sur Mac distant

Valider le décodage UTF-8 incrémental pour iOS sur Mac distant

Une même réponse se lit correctement en local, mais produit parfois du texte corrompu dans le pipeline d’un Mac distant : commencez par vérifier les frontières de décodage. Le réseau livre des blocs d’octets, pas des caractères complets ; les trois octets de « 中 » peuvent arriver en trois fois. Convertir chaque bloc séparément en chaîne peut transformer un texte valide en caractères de remplacement, même sans provoquer d’erreur. Le script Swift autonome ci-dessous permet de formaliser et de vérifier ce contrat, sans dépendre du découpage aléatoire d’un vrai flux réseau.

Définir d’abord le contrat du flux d’octets

L’entrée doit être en UTF-8 et la sortie ne doit contenir que des valeurs scalaires Unicode entièrement décodées. Un bloc vide ne modifie pas l’état ; une séquence incomplète est conservée jusqu’au bloc suivant ; tout reliquat en fin de flux provoque un échec. L’absence de texte en sortie à un instant donné ne signifie pas que la connexion est fermée.

Nous choisissons ici de rejeter strictement les entrées invalides : octets de continuation isolés, encodages trop longs, encodages de substituts et séquences dépassant la limite Unicode. Remplacer les données malformées peut convenir à certains usages d’affichage, mais ne doit pas se faire silencieusement dans des champs de protocole, des configurations ou des données persistantes.

Les frontières des blocs réseau, celles des caractères et celles des messages applicatifs sont trois choses différentes. Un décodage UTF-8 réussi ne signifie pas qu’une ligne ou un message complet a été reçu.

Si le protocole annonce un autre encodage, rejetez-le à l’entrée ou confiez-le à une couche de conversion explicite. Ne tentez pas de deviner l’encodage par essais successifs. Ce code ne retire pas non plus automatiquement la marque d’ordre des octets (BOM) : c’est à la couche supérieure de décider si elle est autorisée.

Implémenter un état incrémental borné

Enregistrez le code suivant dans utf8-contract.swift. Il ne conserve que le caractère en cours de décodage, soit au maximum trois octets entre deux appels ; le volume de sortie de chaque appel reste proportionnel à la taille du bloc d’entrée. Après une erreur, abandonnez l’instance et ne lui fournissez plus de données.

import Foundation

enum DecodeFailure: Error {
    case invalidByte
    case truncated
}

struct StrictUTF8 {
    private var pending: [UInt8] = []
    private var expected = 0

    mutating func push(_ bytes: [UInt8]) throws -> String {
        var output: [UInt8] = []

        for byte in bytes {
            if pending.isEmpty {
                switch byte {
                case 0x00...0x7F:
                    output.append(byte)
                    continue
                case 0xC2...0xDF:
                    expected = 2
                case 0xE0...0xEF:
                    expected = 3
                case 0xF0...0xF4:
                    expected = 4
                default:
                    throw DecodeFailure.invalidByte
                }
                pending.append(byte)
                continue
            }

            var allowed: ClosedRange<UInt8> = 0x80...0xBF
            if pending.count == 1 {
                switch pending[0] {
                case 0xE0: allowed = 0xA0...0xBF
                case 0xED: allowed = 0x80...0x9F
                case 0xF0: allowed = 0x90...0xBF
                case 0xF4: allowed = 0x80...0x8F
                default: break
                }
            }

            guard allowed.contains(byte) else {
                throw DecodeFailure.invalidByte
            }
            pending.append(byte)

            if pending.count == expected {
                output.append(contentsOf: pending)
                pending.removeAll(keepingCapacity: true)
                expected = 0
            }
        }

        guard let text = String(bytes: output, encoding: .utf8) else {
            throw DecodeFailure.invalidByte
        }
        return text
    }

    func finish() throws {
        guard pending.isEmpty else {
            throw DecodeFailure.truncated
        }
    }
}

Le premier octet détermine la longueur de la séquence, le deuxième restreint la plage autorisée, puis les suivants sont vérifiés dans la plage des octets de continuation. Les encodages de substituts ou hors limites sont ainsi détectés avant la conversion en chaîne. L’implémentation ne tente aucune récupération automatique et ne traite pas les octets ASCII qui suivent un octet invalide comme le début d’un nouveau message fiable.

Énumérer les découpages plutôt que répéter les requêtes

Ajoutez les tests suivants au script. Sur cet échantillon court, énumérer toutes les combinaisons « couper ou ne pas couper » entre octets adjacents offre une couverture plus complète que la vérification d’un seul découpage en deux blocs. Des blocs vides sont également insérés pour vérifier qu’ils n’effacent pas l’état en attente.

let sample = "A中🙂e\u{301}"
let bytes = Array(sample.utf8)

for mask in 0..<(1 << (bytes.count - 1)) {
    var decoder = StrictUTF8()
    var chunk: [UInt8] = []
    var result = ""

    for index in bytes.indices {
        chunk.append(bytes[index])
        let cut = index == bytes.count - 1
            || (mask & (1 << index)) != 0
        if cut {
            result += try decoder.push(chunk)
            result += try decoder.push([])
            chunk.removeAll(keepingCapacity: true)
        }
    }

    try decoder.finish()
    precondition(Array(result.utf8) == bytes)
}

La comparaison porte ici sur les octets UTF-8 pour éviter que l’équivalence canonique des chaînes ne masque une modification du contenu. L’accent combinant rappelle aussi qu’une valeur scalaire complète n’est pas nécessairement un caractère complet tel que le perçoit l’utilisateur. Le décodeur peut émettre d’abord la lettre, puis la marque combinante ; si l’interface doit manipuler des graphèmes, ce traitement relève d’une couche supérieure.

Le nombre de découpages croît de façon exponentielle avec la longueur de l’échantillon : réservez cette énumération à de courts vecteurs fixes. Pour les textes longs, générez plutôt les découpages avec une graine pseudo-aléatoire fixe et conservez la séquence des longueurs de blocs en cas d’échec.

Vérifier aussi les rejets par des assertions

Accepter les textes valides ne vérifie que la moitié du contrat. Ajoutez ces vecteurs pour couvrir, dans l’ordre, un octet de continuation isolé, un encodage trop long, un substitut, un dépassement de la limite Unicode, un octet de continuation incorrect et une séquence tronquée en fin de flux. Chaque entrée est fournie octet par octet pour vérifier que le découpage ne permet pas de contourner la détection des erreurs.

let invalid: [[UInt8]] = [
    [0x80],
    [0xC0, 0xAF],
    [0xED, 0xA0, 0x80],
    [0xF4, 0x90, 0x80, 0x80],
    [0xE4, 0x41],
    [0xF0, 0x9F, 0x99]
]

for input in invalid {
    var decoder = StrictUTF8()
    var rejected = false

    do {
        for byte in input {
            _ = try decoder.push([byte])
        }
        try decoder.finish()
    } catch {
        rejected = true
    }

    precondition(rejected)
}

Complétez également les tests avec les bornes des plages valides, notamment la séquence de quatre octets correspondant à la plus grande valeur scalaire autorisée. La présence d’un caractère de remplacement dans la chaîne ne doit pas être le seul critère : ce caractère peut lui-même figurer légitimement dans l’entrée. Un décodage strict doit se fonder sur les octets d’origine.

Intégrer le test au pipeline sans perdre les frontières d’erreur

Exécutez le script dans l’environnement des outils en ligne de commande Xcode sélectionné :

xcrun swift utf8-contract.swift

Une fin d’exécution normale signifie que les tests ont réussi ; toute erreur levée ou assertion échouée doit bloquer les tâches suivantes. Pour une intégration durable, placez le décodeur dans les sources partagées et déplacez les vecteurs vers une cible de tests. Le script autonome permet de valider d’abord l’algorithme, mais ne remplace pas la compilation et l’exécution de la cible iOS réelle.

Chaque connexion doit disposer de sa propre instance, appelée de manière séquentielle dans l’ordre de réception. Une reconnexion crée une nouvelle instance : la fin d’un caractère de l’ancienne connexion ne doit pas être assemblée au début de la nouvelle réponse. Une annulation doit elle aussi abandonner l’état, sans emprunter le chemin de fin normale.

Vérifiez enfin le code consommateur : les sorties successives doivent uniquement alimenter un tampon temporaire. N’écrivez dans le stockage persistant et ne déclenchez d’action qu’une fois le message applicatif complet et ses validations réussies. Si la première partie a déjà été enregistrée en base lorsque la seconde révèle un encodage invalide, le décodage strict ne peut pas annuler automatiquement les effets de bord. Pour les erreurs, consignez simplement le décalage en octets, la longueur du bloc et la catégorie d’erreur ; évitez de téléverser par défaut le contenu intégral de la réponse. Le critère de validation ne se limite alors plus à l’absence de texte corrompu : le résultat reste identique quel que soit le découpage, et une erreur ne peut pas être assimilée à une livraison réussie.

Questions fréquentes

Peut-on convertir chaque bloc Data séparément en texte ?

Non. Un bloc peut couper un caractère UTF-8. La conversion stricte peut échouer et la conversion tolérante peut insérer un caractère de remplacement. Conservez les octets incomplets.

Peut-on ignorer les derniers octets incomplets ?

Non. Pour un texte UTF-8 complet, ils signalent une troncature. La fin du flux doit échouer, sauf si le protocole définit explicitement une stratégie de récupération distincte.

Réservez un Mac entier aux builds de votre équipe

Choisissez un Mac mini Apple Silicon M4 dédié et gardez votre propre chaîne d’outils et configuration de runner. Facturation à la journée, à la semaine, au mois ou au trimestre. La disponibilité affichée dans la console fait foi.

Louer un nœud de build

Choisissez la configuration adaptée à vos builds

Commencez par vérifier la mémoire et le stockage, puis choisissez la durée de facturation et le nœud.