Tester le parsing XML iOS sur Mac cloud : entités, espaces de noms et quotas

Tester le parsing XML iOS sur Mac cloud : entités, espaces de noms et quotas

Tester le parsing XML iOS sur Mac cloud : entités, espaces de noms et quotas

Un import XML qui fonctionne en local peut bloquer dans un pipeline sans surveillance : le fichier contient une déclaration d’entité externe, les éléments sont trop profondément imbriqués ou des champs de même nom appartiennent à des espaces de noms différents. Le rôle du Mac cloud n’est pas de « réparer au mieux » ces entrées, mais de rejeter systématiquement les documents hors limites et de conserver des fichiers permettant de reproduire les échecs.

Définir le contrat d’entrée avant d’appeler le parseur

Prenons une fonction d’import de configuration iOS. Fixons d’abord les règles : UTF-8 uniquement, aucune DTD, une taille maximale de 256 KiB par entrée, une profondeur maximale de 16 niveaux et un total maximal de 32 KiB pour l’ensemble des données reçues par les callbacks de texte. Ces quotas sont des exemples propres à cet article, et non des limites de la norme XML ou des valeurs système par défaut. Adaptez-les aux besoins de votre application.

Consignez séparément les erreurs de format, les dépassements de quota et les erreurs sur les champs métier. Le succès du parseur ne suffit pas à accepter un document dont l’élément racine est inconnu, qui répète un champ à valeur unique ou qui omet des champs obligatoires.

Désactiver la résolution des entités externes ne revient ni à interdire les DTD, ni à garantir une isolation complète des ressources. Les contraintes de format, les réglages du parseur et le délai maximal d’exécution du processus doivent être validés séparément.

Exécuter un contrôle de sécurité minimal sur Mac cloud

Enregistrez le code suivant dans xml-guard.swift. Il vérifie uniquement la syntaxe et les limites de ressources, sans construire de modèle métier. Le script adopte une approche prudente : il rejette même le marqueur de déclaration DTD lorsqu’il apparaît dans un commentaire. Ce simple contrôle textuel ne doit pas être présenté comme une analyse lexicale XML complète.

import Foundation

final class GuardDelegate: NSObject, XMLParserDelegate {
    var depth = 0
    var textBytes = 0
    var exceeded = false

    func stop(_ parser: XMLParser) {
        exceeded = true
        parser.abortParsing()
    }

    func parser(
        _ parser: XMLParser,
        didStartElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String: String] = [:]
    ) {
        depth += 1
        if depth > 16 { stop(parser) }
    }

    func parser(
        _ parser: XMLParser,
        didEndElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?
    ) {
        depth -= 1
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        textBytes += string.utf8.count
        if textBytes > 32 * 1024 { stop(parser) }
    }

    func parser(_ parser: XMLParser, foundCDATA data: Data) {
        textBytes += data.count
        if textBytes > 32 * 1024 { stop(parser) }
    }

    func parser(
        _ parser: XMLParser,
        resolveExternalEntityName name: String,
        systemID: String?
    ) -> Data? {
        nil
    }
}

func validate(_ path: String) throws -> Bool {
    let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer { try? handle.close() }

    var data = Data()
    while true {
        guard let chunk = try handle.read(upToCount: 8192),
              !chunk.isEmpty else { break }
        data.append(chunk)
        if data.count > 256 * 1024 { return false }
    }

    guard let source = String(data: data, encoding: .utf8),
          !source.contains("\0"),
          !source.contains("<!DOCTYPE") else {
        return false
    }

    let delegate = GuardDelegate()
    let parser = XMLParser(data: data)
    parser.delegate = delegate
    parser.shouldProcessNamespaces = true
    parser.shouldResolveExternalEntities = false
    parser.externalEntityResolvingPolicy = .never

    let parsed = parser.parse()
    return parsed && !delegate.exceeded && delegate.depth == 0
}

guard CommandLine.arguments.count == 2 else { exit(2) }
do {
    exit(try validate(CommandLine.arguments[1]) ? 0 : 1)
} catch {
    exit(2)
}

Compilez dans un terminal où la chaîne d’outils a été sélectionnée, puis exécutez le programme sur un fichier valide :

xcrun swiftc xml-guard.swift -o xml-guard
printf '%s\n' '<settings><name>demo</name></settings>' > valid.xml
./xml-guard valid.xml

Les codes de sortie sont les suivants : 0 pour une validation réussie, 1 pour une entrée rejetée et 2 pour un échec d’appel ou de lecture du fichier. Le pipeline doit classer les résultats à partir de ces codes, sans analyser des messages d’erreur qui dépendent de la langue du système. Le point d’entrée doit accepter des fichiers ordinaires placés temporairement par la tâche elle-même, et non des chemins de périphériques arbitraires.

Couvrir les cas de rejet avec des fichiers de test fixes

Conservez au moins six catégories de fichiers : document valide, balise non fermée, présence d’une DTD, imbrication sur 17 niveaux, dépassement du quota de texte et dépassement de la taille d’entrée en octets. Les commandes suivantes génèrent deux fichiers pour tester les limites ; Python 3 doit être disponible dans l’environnement d’exécution.

python3 - <<'PY'
from pathlib import Path

Path("deep.xml").write_text(
    "<n>" * 17 + "</n>" * 17, encoding="utf-8"
)
Path("text-limit.xml").write_text(
    "<n>" + "x" * (32 * 1024 + 1) + "</n>", encoding="utf-8"
)
PY
./xml-guard deep.xml
./xml-guard text-limit.xml

Les deux exécutions doivent renvoyer 1. Préparez également une paire de fichiers, l’un exactement au quota et l’autre le dépassant d’un seul octet, afin de détecter une erreur d’opérateur de comparaison. Le fichier contenant une entité externe doit être rejeté avant le parsing. Pour vérifier séparément les réglages du parseur relatifs aux ressources externes, utilisez une cible de test contrôlée et vérifiez par une assertion qu’aucun accès effectif n’a eu lieu. Un résultat vide ne suffit pas à conclure que le traitement est sûr.

Traiter les espaces de noms et les callbacks de texte selon la structure

Lorsque la gestion des espaces de noms est activée, identifiez les champs métier par le couple « URI de l’espace de noms + nom local ». Ne comparez ni le préfixe seul, ni uniquement elementName. Le préfixe peut changer ; à l’inverse, un même name associé à des URI différentes peut désigner des concepts distincts. Les tests doivent vérifier qu’un changement de préfixe reste accepté et qu’un changement d’URI entraîne bien un rejet.

foundCharacters ne garantit pas de fournir un champ complet en un seul appel. Le décodeur réel doit maintenir une pile d’éléments, ajouter progressivement le texte reçu, puis vérifier la longueur, le format et les doublons de champs à la fermeture de l’élément. Les données CDATA passent par un callback distinct et doivent elles aussi être comptabilisées. Évitez également de supprimer systématiquement les espaces en début et en fin de tous les champs : un nom d’affichage et un corps de texte dont les espaces doivent être conservés peuvent relever de contrats différents.

Ne pas valider de résultat partiel

Placez d’abord tous les enregistrements dans un modèle temporaire propre à la tâche. N’effectuez qu’une seule validation en base, une fois tous les contrôles réussis : syntaxe, quotas, élément racine et règles métier. Ajoutez volontairement une balise incorrecte en fin de document et vérifiez que les enregistrements déjà lus ne sont pas enregistrés dans la base de données.

Intégrer les vérifications au build plutôt que se fier à un test manuel ponctuel

Sur un Mac cloud OSXBuild, commencez par exécuter les fichiers de test en ligne de commande, puis ajoutez les mêmes fichiers à la cible de tests iOS pour appeler le décodeur réellement utilisé par l’application. Les deux implémentations partagent le même contrat, mais la réussite du script sur le Mac ne remplace pas la validation de l’implémentation dans le simulateur.

Pour les tests de non-régression, il suffit de conserver le nom du fichier, le code de sortie attendu, le résultat obtenu et la version de la chaîne d’outils, sans afficher l’intégralité du contenu importé. Limitez la durée des tâches anormales au moyen d’un délai maximal dans le pipeline. Les quotas appliqués dans les callbacks ne bornent que les données déjà transmises ; ils ne constituent pas une limite absolue de la mémoire interne du parseur.

Pour finir, vérifiez chaque point : les fichiers valides sont importés, les fichiers hors limites échouent de façon systématique, les espaces de noms ne provoquent aucune confusion entre champs et les échecs ne laissent aucun résultat partiel. Lors de la prochaine modification du parsing, la validation reposera ainsi sur des entrées reproductibles et des assertions explicites, et non sur le simple constat que « cette fois, rien n’a bloqué ».

Questions fréquentes

Désactiver les entités externes suffit-il ?

Non. Ce réglage n’interdit pas les DTD internes. Le format retenu accepte uniquement du XML UTF-8 sans DTD, avec un contrôle conservateur avant le parsing.

foundCharacters fournit-il toujours un champ complet ?

Non. Le texte peut arriver en plusieurs appels. Il faut le cumuler jusqu’à la fermeture de l’élément et enregistrer le résultat seulement après validation du document entier.

Un Mac cloud dédié pour vos builds

Une machine physique entièrement dédiée, pas une machine virtuelle. Choisissez le modèle et la durée selon votre volume de builds, et gardez archives, signatures et caches sur votre propre nœud.

Choisir une configuration Mac cloud

Votre prochaine archive, prête sur un Mac cloud dédié

Choisissez la machine selon votre volume de builds et le nœud selon l’emplacement de votre équipe. Comparez les offres, puis transférez votre chaîne d’outils.