Valider et normaliser les UUID iOS sur un Mac cloud
L’API passe l’UUID d’un même enregistrement des majuscules aux minuscules, mais le client crée un second enregistrement. Le problème vient rarement du générateur de nombres aléatoires : c’est généralement la chaîne utilisée directement comme clé primaire qui est en cause, avec des règles différentes pour la comparaison, le cache et la persistance. Sur un Mac cloud, vous pouvez d’abord valider cette frontière au moyen d’un script Swift autonome, puis intégrer ces vérifications aux tests du projet iOS, sans lancer de simulateur.
Définir d’abord le contrat d’entrée
Nous acceptons ici le format textuel standard de 36 octets : des caractères hexadécimaux répartis en cinq groupes de 8, 4, 4, 4 et 12 caractères, séparés par des traits d’union ASCII. L’entrée peut mêler majuscules et minuscules ; la sortie est toujours en minuscules. Les espaces en début ou en fin de chaîne, les accolades et les formes sans traits d’union sont refusés.
Cela ne signifie pas que toutes les autres représentations sont invalides, mais que l’API adopte une seule forme d’entrée. Si une ancienne API accepte une autre forme, traitez-la dans une couche de compatibilité explicite, plutôt que de laisser le parseur principal la corriger silencieusement.
Il faut aussi distinguer validité syntaxique et règles métier. Un UUID entièrement composé de zéros est structurellement valide, mais peut être interdit comme identifiant de commande ou de document. De même, une éventuelle restriction de version doit relever du contrat de l’API : le premier caractère du troisième groupe ne suffit pas à justifier un rejet au seul motif qu’il ne correspond pas à un chiffre attendu.
Un UUID n’est pas un justificatif d’accès. Même si son format est correct, le serveur doit vérifier que l’utilisateur courant est autorisé à lire l’enregistrement correspondant.
Écrire un parseur strict et exécutable
Sur le nœud, exécutez xcrun --find swift pour vérifier que la chaîne d’outils active fournit Swift. Enregistrez le code suivant dans uuid-contract.swift. Il vérifie d’abord la structure des octets, puis confie l’analyse à Foundation, afin de ne pas dépendre de la tolérance d’une version du système envers des entrées non conformes.
import Foundation
enum IdentifierError: Error {
case invalidSyntax
case zeroNotAllowed
}
func parseIdentifier(
_ text: String,
allowZero: Bool = false
) throws -> UUID {
let bytes = Array(text.utf8)
let separators: Set<Int> = [8, 13, 18, 23]
guard bytes.count == 36 else {
throw IdentifierError.invalidSyntax
}
for (index, byte) in bytes.enumerated() {
if separators.contains(index) {
guard byte == 45 else {
throw IdentifierError.invalidSyntax
}
} else {
let isHex = (48...57).contains(byte)
|| (65...70).contains(byte)
|| (97...102).contains(byte)
guard isHex else {
throw IdentifierError.invalidSyntax
}
}
}
guard let value = UUID(uuidString: text) else {
throw IdentifierError.invalidSyntax
}
let zero = UUID(
uuid: (0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0)
)
guard allowZero || value != zero else {
throw IdentifierError.zeroNotAllowed
}
return value
}
func canonical(_ value: UUID) -> String {
value.uuidString.lowercased()
}
Le contrôle porte sur les octets UTF-8, et non sur le nombre de caractères visibles, afin d’exclure explicitement les caractères pleine chasse et les symboles Unicode ressemblant à un trait d’union. Ici, lowercased() ne s’applique qu’à la représentation d’un UUID déjà validé : il ne sert pas à transformer une entrée utilisateur quelconque avant de l’accepter.
Vérifier les cas limites avec des jeux d’essai fixes
Ajoutez ensuite les tests à la fin du même fichier. Les cas valides vérifient l’égalité des valeurs, la normalisation et l’idempotence ; les cas invalides couvrent les espaces, les substitutions de caractères et les variations de longueur. Ne remplacez pas les contre-exemples fixes par des UUID aléatoires : les échecs seraient plus difficiles à reproduire.
let upper = "550E8400-E29B-41D4-A716-446655440000"
let lower = upper.lowercased()
let value = try parseIdentifier(upper)
precondition(value == (try parseIdentifier(lower)))
precondition(canonical(value) == lower)
let reparsed = try parseIdentifier(canonical(value))
precondition(canonical(reparsed) == lower)
let invalid = [
"",
" " + lower,
lower + "\n",
"{" + lower + "}",
lower.replacingOccurrences(of: "-", with: ""),
lower.replacingOccurrences(of: "-", with: "-"),
lower.replacingOccurrences(of: "5", with: "g"),
lower + "0"
]
for text in invalid {
do {
_ = try parseIdentifier(text)
fatalError("Invalid UUID accepted")
} catch IdentifierError.invalidSyntax {
}
}
let zero = "00000000-0000-0000-0000-000000000000"
do {
_ = try parseIdentifier(zero)
fatalError("Zero UUID accepted")
} catch IdentifierError.zeroNotAllowed {
}
_ = try parseIdentifier(zero, allowZero: true)
Exécutez xcrun swift uuid-contract.swift. En cas de succès, le script ne produit aucune sortie ; en cas d’échec, le processus se termine avec un code de sortie non nul. Les blocs catch vides indiquent que l’erreur attendue a bien été interceptée. Les autres erreurs ne sont pas ignorées et font toujours échouer le script. Lors du passage à XCTest, utilisez des assertions portant sur les erreurs précises pour conserver cette distinction.
Normaliser à la frontière des données
L’analyse doit intervenir au point d’entrée dans le modèle de domaine : réponse réseau, paramètre de lien profond ou fichier importé. La couche métier conserve un UUID et n’appelle canonical que pour envoyer une requête ou écrire dans un champ textuel. Évitez que l’interface et le cache effectuent chacun leurs propres substitutions de chaînes.
La migration des données stockées demande une vigilance particulière. Sur une copie en lecture seule, commencez par associer les anciennes clés à leur forme canonique, puis recensez les cas où plusieurs anciennes clés aboutissent à la même forme. En cas de collision, appliquez les règles métier de fusion des enregistrements avant de mettre à jour les clés étrangères et les contraintes d’unicité. Une conversion en minuscules en masse risque de réduire deux enregistrements historiques à un seul.
Ne pas confondre tri textuel des UUID et ordre chronologique
L’ordre lexicographique des UUID aléatoires ne reflète pas leur ordre de création. Pour obtenir un ordre stable dans une liste, utilisez un champ explicite de date de création, puis l’identifiant normalisé comme clé de tri secondaire en cas d’égalité. Si vous choisissez un schéma d’UUID comportant une dimension temporelle, définissez également sa version et ses règles de tri dans un contrat distinct.
Intégrer ces contrôles à la validation sur Mac cloud
Avant d’exécuter le script sur un nœud ArmVNC, consignez l’identifiant du commit ainsi que les sorties de sw_vers et de xcrun swift --version. Ces informations servent à expliquer les différences entre chaînes d’outils et ne contiennent pas de données métier. Utilisez des valeurs de test fixes pour les cas d’échec, sans archiver directement des enregistrements utilisateur.
Une fois les tests intégrés à l’intégration continue, vérifiez au minimum les quatre points suivants :
- Le format textuel standard est accepté, et les variantes en majuscules et en minuscules donnent la même valeur.
- Les espaces, les séparateurs incorrects et les caractères non hexadécimaux doivent être rejetés.
- L’UUID entièrement composé de zéros est traité selon une politique explicite, sans que ce cas soit masqué par une simple erreur de syntaxe.
- Les collisions sont recherchées avant la migration des anciennes clés, et le nombre d’enregistrements liés reste identique avant et après la migration.
Ajoutez enfin un test d’intégration : soumettez deux fois la même entité, avec un UUID en majuscules puis en minuscules. Le stockage doit toujours ne contenir qu’un seul enregistrement, et la mise à jour doit s’appliquer à l’enregistrement d’origine. Le parseur garantit une entrée cohérente ; seul le test d’intégration prouve que le cache, la base de données et les mises à jour métier respectent réellement ce contrat.
Questions fréquentes
Deux UUID de casse différente désignent-ils deux identifiants ?
Non. Comparez les valeurs UUID après analyse et utilisez une forme textuelle standard en minuscules pour le stockage. Vérifiez les collisions des anciennes clés avant la migration.
Faut-il toujours rejeter un UUID composé uniquement de zéros ?
Non. Sa structure est valide, mais votre contrat métier peut l'interdire comme identifiant d'entité. Rendez cette règle explicite et testez les deux politiques.
Confiez vos builds à un Mac cloud dédié
Choisissez un nœud physique Apple Silicon et disposez de la machine entière, avec macOS natif, interface graphique et ligne de commande. Le modèle, la durée et le nœud sont ceux indiqués dans la confirmation de commande.