13min.

Ce que vous devez savoir sur les chaînes de caractères

Jouer avec les chaînes de caractères est certainement la première chose que l’on fait en tant que développeur. Qui n’a pas commencé sa carrière de dev par un “Hello World” ? Et pourtant, elles sont encore trop mal connues malgré tous les outils que nous possédons. Nous vous proposons aujourd’hui un récapitulatif de ce qu’est une chaîne de caractères, et des outils fournis par Symfony pour les manipuler.

Section intitulée qu-est-ce-qu-une-chaine-de-caracteresQu’est ce qu’une chaîne de caractères ? 🧐

Tout ce que vous lisez actuellement sont des chaînes de caractères. Tout ce que vous dites et entendez, et bien plus, se traduisent en chaînes de caractères. En revanche, pour l’ordinateur, il ne s’agit que d’une suite de 1 et de 0. Et nous allons nous intéresser à ce qu’il se passe entre les deux.

Section intitulée un-peu-de-vocabulaireUn peu de vocabulaire…

Un glyphe est une représentation visuelle d’un élément d’écriture. Par exemple, a, A, 1 ou µ sont des glyphes, tout comme ~, n, ñ et même les emojis.

Un graphème est un symbole qui ne peut pas être divisé en unité plus petite, comme ~ ou n.

Un groupe de graphèmes rassemble plusieurs graphèmes et peut être vu comme un caractère. Par exemple, ñ est un groupe de graphèmes : c’est l’association de ~ et n.

Au passage, un graphème qui ne peut pas être utilisé seul, comme ~, des accents, ou encore une cédille, est appelé un diacritique (ça ne nous servira à rien ici, mais vous pourrez peut-être briller en société grâce à ça).

Section intitulée et-un-peu-d-histoire…et un peu d’histoire

Au commencement il y avait le binaire. Certains constructeurs décidèrent qu’une certaine séquence de 1 et de 0 signifiait “a”, mais cela n’était vrai que pour ce constructeur. Et comme au début de l’informatique, un document ne quittait pas le système sur lequel il était créé, cela ne posait pas de problème.

En 1961, l’ANSI se réunit et publie deux ans plus tard l’American Standard Code for Information Interchange, le Standard Américain pour l’Échange d’Information, ou ASCII pour les intimes, que nous retrouvons sur la fameuse table ASCII :

USASCII code chart

Les lettres étaient encodées sur 7 bits, ce qui offrait les 127 combinaisons ci-dessus. Notons qu’il n’y a en réalité que 95 caractères, les autres étant de la ponctuation, ou des caractères de contrôle. Il y en a même un, EM, pour prévenir s’il n’y a plus de bande magnétique !

Fort pratique pour les anglophones, ce choix est moins sympathique pour le reste du monde qui écrit avec de nombreux accents et lettres différentes.

En 1972, les processeurs 8 bits débarquent – merci IBM – et avec eux, la possibilité d’encoder non plus 127 caractères, mais 255 ! Pour rappel, un octet (byte en anglais) est un bloc de 8 bits, et chaque caractère est encodé sur un octet. Et 2 puissance 8, ça donne 256 ! Donc tout le monde fait un peu sa sauce avec les 128 caractères supplémentaires selon ses besoins et de très nombreuses variantes apparaissent.

Puis dans les années 90, le World Wide Web apparaît et des documents sont échangés à travers le monde… et à travers différents encodages. Puisque tout texte n’est qu’un ensemble de 0 et de 1, il n’y a absolument aucun moyen de deviner l’encodage du texte, ce qui rend vraiment visible toute erreur de décodage.

Par exemple, le é, en ASCII, sera encodé 11101001 mais en UTF-8, il sera encodé 11000011 10101001. Mais cette chaîne décodée en ASCII donnera… é ! Ça ne vous rappelle rien ? Eh oui, le mojibake est né.

En 1991, la norme Unicode V1.0 fait son apparition ! Son nom est tiré de ses trois objectifs : Universel : correspond aux besoins linguistiques de toutes les langues et dialectes du monde; Uniforme : encodé sur une longueur fixe pour un accès plus efficace; Unique : chaque séquence de bits ne correspond qu’à une interprétation en caractères. Ce standard, encodé sur 16 bits, permet d’encoder 65 536 caractères ! Cette flexibilité le rend vite adopté par de nombreux langages de programmation tels que JavaScript, C, ou Java.

Section intitulée point-de-codePoint de code

Ou “Code point” en anglais. Il s’agit de la position numérique d’un caractère – glyphe, graphème, ou même diacritique – au sein d’un espace de codage bien défini. Mais il ne s’agit pas de sa valeur hexadécimale une fois encodé. Par exemple, le caractère é dans Unicode se situe au point de code U+000E9, mais sa valeur hexadécimale une fois encodé est C3A9. Si vous êtes perdus, n’hésitez pas à jouer avec des convertisseurs texte/binaire et binaire/décimal/hexadécimal.

Dans le standard Unicode, les points de code s’écrivent U (pour Unicode) suivis d’un + puis d’un numéro hexadécimal. Par exemple, le point de code de ñ est U+000F1. Mais ~ a aussi un point de code qui est le U+00303.

Wait, what? Le tilde a son propre point de code ?

Eh oui, il est possible de combiner plusieurs caractères. Par exemple ~ (U+00303) + n (U+0006E) donnera un caractère composé, ñ, mais qui sera différent de ñ (U+000F1), qui est un caractère à part entière.

Section intitulée l-arrivee-d-utfL’arrivée d’UTF

Unicode introduit plusieurs encodages pour ces points de code : d’abord l’UTF-16, sur 16 bits, donc 2 octets. Mais on s’est rendu compte que 16 bits, c’était encore trop peu pour encoder tous les caractères du monde ! UTF-32 a donc été créé : un encodage sur 32 bits, donc 4 octets. Mais pour de nombreux caractères, les trois premiers octets n’étaient remplis que de zéros, ce qui crée une énorme perte de place en mémoire. Arrive alors UTF-8 dont chaque caractère peut être encodé sur 1 à 4 octets ! Le concept d’une taille variable est introduit, et cela permet de ne prendre que la taille nécessaire en mémoire.

À noter : UTF-8 assure une rétrocompatibilité avec l’ASCII pour tous les caractères dont le point de code est avant 128.

UTF-8 est maintenant largement adopté sur le Web, 98,3% quand j’écris ces lignes, et nous vous encourageons à n’utiliser que lui. Aujourd’hui, 149 186 caractères et environ 245 000 points de code peuvent être assignés sur un espace qui peut contenir jusqu’à 1 114 112 points de code différents.

Section intitulée pour-resumerPour résumer ☕

Traduction du mot café en graphèmes, glyphes, points de codes, et en différents encodages

Une chaîne de caractères peut être vue de trois manières : Sous forme d’un ensemble de graphèmes, de points de code ou d’octets.

Section intitulée l-approche-du-composant-string-de-symfonyL’approche du composant String de Symfony 🧩

Afin de pouvoir gérer ces trois différents aspects, le composant String nous propose de créer une chaîne de trois manières différentes :

new UnicodeString('Å');    	// ou u('Å')	// graphèmes
new CodePointString('Å');                	// points de code
new ByteString('Å');    	// ou b('Å')	// octets

La documentation de ce composant est très complète, n’hésitez pas à la consulter pour découvrir toutes les manipulations qu’il est possible d’effectuer avec le composant string !

$text = (new UnicodeString('This is a déjà-vu situation.'))
	->trimEnd('.')
	->replace('déjà-vu', 'jamais-vu')
	->append('!');
// $text = 'This is a jamais-vu situation!'
u('foo BAR')->upper(); // 'FOO BAR'
u('FOO Bar')->lower();  // 'foo bar'
u('Die O\'Brian Straße')->folded(); // "die o'brian strasse"
u('Foo: Bar-baz.')->camel(); // 'fooBarBaz'
u('Foo: Bar-baz.')->snake(); // 'foo_bar_baz'
u('Foo: Bar-baz.')->camel()->title(); // 'FooBarBaz'
u('abc')->indexOf('B');           	// null
u('abc')->ignoreCase()->indexOf('B'); // 1
u('hello')->append('world');  	// 'helloworld'
u('hello')->append(' ', 'world'); // 'hello world'
u('User')->ensureEnd('Controller');       	// 'UserController'
u('UserController')->ensureEnd('Controller'); // 'UserController'
u(' Lorem Ipsum ')->padBoth(20, '-');	// '--- Lorem Ipsum ----'
u('_.')->repeat(10);			// '_._._._._._._._._._.'
u('   Lorem Ipsum   ')->trim();			// 'Lorem Ipsum'
u('http://symfony.com')->replace('http://', 'https://');	// 'https://symfony.com'
u('Symfony is great')->slice(0, 7); 		// 'Symfony'
u('template_name.html.twig')->split('.');	// ['template_name', 'html', 'twig']

Certaines méthodes sont spécifiques à l’objet ByteString :

// returns TRUE if the string contents are valid UTF-8 contents
b('Lorem Ipsum')->isUtf8();	// true
b("\xc3\x28")->isUtf8();	// false

D’autres sont spécifiques aux objets CodePointString et UnicodeString, comme récupérer un point de code positionné à une certaine position dans la chaîne, ou convertir du texte en ascii :

u('नमस्ते')->codePointsAt(0); // न [2344]
u('नमस्ते')->codePointsAt(1); // म [2350]
u('नमस्ते')->codePointsAt(2); // स्ते [2360, 2381, 2340, 2375]

u('नमस्ते')->ascii();	// 'namaste'
u('さよなら')->ascii();	// 'sayonara'
u('спасибо')->ascii();	// 'spasibo'
u('🦄')->ascii();		// '?'

La méthode ->ascii() utilise la translittération – dont nous parlerons juste après – mais si vous avez besoin d’un slugger, utilisez plutôt ceci :

Section intitulée asciisluggerAsciiSlugger

L’AsciiSlugger de Symfony est un outil très utile qui permet simplement de transformer n’importe quelle chaîne en chaîne valide pour des URLs ou encore des noms de fichiers. Il s’utilise comme ceci :

$slugger = new AsciiSlugger();
$slug = $slugger->slug('Wôrķšƥáçè ~~sèťtïñğš~~', '/');
// $slug = 'Workspace/settings'

Mais si vous voulez un peu plus de paillettes dans votre code, vous pouvez transformer ce slugger en EmojiSlugger ! Ajoutez simplement l’option ->withEmoji(), choisissez une locale, et amusez-vous :

$slugger = $slugger→withEmoji();

$slug = $slugger→slug('a 😺, and a 🦁 go to 🏞️', '-', 'en');
// $slug = 'a-grinning-cat-and-a-lion-go-to-national-park';

$slug = $slugger→slug('un 😺, et un 🦁 vont au 🏞️', '-', 'fr');
// $slug = 'un-chat-qui-sourit-et-un-tete-de-lion-vont-au-parc-national';

Section intitulée le-concept-de-translitterationLe concept de translittération 🌎

Avec l’arrivée d’internet, la question de l’internationalisation s’est rapidement posée. Tous les claviers ne possèdent pas les mêmes caractères, et pourtant, lorsque je recherche des artistes comme Eivør ou Rebūke dans ma playlist Spotify, je m’attends à les trouver en tapant simplement “Eivor” ou “Rebuke”. Convertir une chaîne en une autre similaire visuellement ou phonétiquement, c’est ce qu’on appelle la translittération.

Cette opération est particulièrement pratique avant d’effectuer un tri ou une opération d’indexation. En PHP natif, elle s’effectue comme ceci :

transliterator_transliterate(
'Any-Latin; Latin-ASCII; Lower()',
"A æ Übérmensch på høyeste nivå! И я люблю PHP! fi"
);
// "a ae ubermensch pa hoyeste niva! i a lublu php! fi"

Le premier argument est une série d’instructions séparées par un point virgule. Ici on demande à traduire par n’importe quel caractère latin, en ASCII, et on passe tout en minuscules.

Le site decodeunicode.org, que j’apprécie beaucoup pour la quantité d’informations qu’il fournit, indique parfois les autres glyphes qui entrent dans la composition d’un graphème. Plus tôt, je vous parlais de ñ; sa page sur le site indique, entre autres, sa décomposition, c’est-à-dire les autres caractères qui le composent.

Section intitulée homoglyphesHomoglyphes

J’en profite pour aborder le concept d’homoglyphes : il s’agit de caractères particulièrement similaires visuellement ! Ceux-ci sont aussi visibles sur le site de decodeunicode. Le principe de la normalisation de composition ou de décomposition (passer de ñ à la combinaison de n et ~ et vice versa) permet de transformer des homoglyphes en caractères par défaut.

Cela a laissé la place à une faille de sécurité survenue chez Github il y a quelques années : il était possible de voler le compte de quelqu’un simplement en utilisant adresse mail visuellement similaire à la sienne ! Cela était dû au fait que l’adresse mail entrée dans le champ “mot de passe oublié” était normalisée avant d’être recherchée dans la base de données. Ainsi, pour attaquer mike@example.org, il suffisait d’entrer miᏦᎬ@example.org dans le champ. Le compte de mike était bien retrouvé, mais le token de récupération de mot de passe était ensuite généré et envoyé à miᏦᎬ ! L’attaquant n’avait alors plus qu’à se connecter avec le compte de mike et son nouveau mot de passe. Alors soyez vigilant.e.s avant d’utiliser la normalisation à tout va !

Si votre base de données est encodée en UTF-8, alors ce problème ne devrait pas se poser.

Un autre exemple d’attaque par homoglyphe est celui d’utilisation de noms de domaine visuellement similaires. Par exemple, certains attaquants ont créé le domaine www.airfrạnce.com. Si vous regardez bien, le a de France est en fait un a avec un point dessous (U+01EA1) ! Les attaquants envoyaient donc ce lien avec la promesse d’un billet d’avion à gagner et il était très compliqué pour les utilisateurs de voir la différence.

Un homoglyphe du site de Air France vu par mon navigateur

Aujourd’hui, ces URLs sont régulièrement traduit en “punycode” par les navigateurs et applications. Il s’agit d’une version sans accents et précédée de xn-- et dont les caractères spéciaux sont supprimés. Vous pouvez trouver plus de détails dans cet article.

Section intitulée en-pratique-la-taille-d-une-chaineEn pratique : la taille d’une chaîne 🧮

Vous pensez que c’est facile ? Spoiler : oui, mais pas comme vous le pensiez !

Compter le nombre de caractères sur son site Web est une action très courante, qu’il s’agisse de vérifier la longueur d’un mot de passe, ou de compter les caractères d’un champ texte par exemple.

Pour un article très détaillé et technique sur le sujet, n’hésitez pas à lire It’s Not Wrong that « 🤦🏼‍♂️ ».length == 7 de Henri Sivonen ; mais si vous avez la flemme, en voici un résumé qui ne s’intéresse qu’à la partie PHP.

En effet, d’un langage à l’autre, la taille de la chaîne de caractères 🤦🏼‍♂️ peut être 1, 5, 7 ou encore 17. Rien qu’en PHP, vous obtiendrez 17 avec strlen('🤦🏼‍♂️'); et 5 avec mb_strlen('🤦🏼‍♂️', 'UTF-8');. Vous vous en doutez, cela dépend de ce qu’on compte : des octets ? Des points de code ? Des graphèmes ?

Décomposons cet emoji pour comprendre ce qu’il se passe. Il est constitué de 5 caractères distincts : l’emoji facepalm, un modificateur de couleur de peau, ainsi qu’un modificateur de genre. À ces caractères s’ajoutent le “zero width joiner”, ou “jointure de taille zéro”, qui permet d’indiquer que les caractères se superposent, ainsi que le “variation selector” ou “sélecteur de variation” qui explicite le fait que l’on souhaite afficher un emoji coloré plutôt qu’un dingbat monochrome.

D’un langage à l’autre, les différents caractères qui composent un graphème sont enregistrés dans des “code units” ou unités de code qui contiennent plus ou moins d’octets.

Le tableau suivant décompose cet emoji, et pour chaque part, nous donne sa taille en unités de code et en octets.

Scalaire Unicode Unités de code UTF-32 Unités de code UTF-16 Unités de code UTF-8 UTF-32 code octets UTF-16 code octets UTF-8 code octets
U+1F926 FACE PALM 🤦 1 2 4 4 4 4
U+1F3FC EMOJI MODIFIER FITZPATRICK TYPE-3 🏼 1 2 4 4 4 4
U+200D ZERO WIDTH JOINER 1 1 3 4 2 3
U+2642 MALE SIGN ♂ 1 1 3 4 2 3
U+FE0F VARIATION SELECTOR-16 1 2 3 4 2 3
Total 5 7 17 20 14 17

Dans le cas d’un emoji, on s’attendra bien souvent à ce que son nombre de caractères soit égal à 1. On veut donc compter les graphèmes. Eh bien le composant String nous permet cette granularité :

(new ByteString('🤦🏼‍♂️'))->length();    	// 17 octets
(new CodePointString('🤦🏼‍♂️'))->length();	// 5 points de code
(new UnicodeString('🤦🏼‍♂️'))->length();	// 1 graphème

Section intitulée et-dans-la-base-de-donneesEt dans la base de données ? 💾

Section intitulée petit-point-vocabulairePetit point vocabulaire

Character set : Un set de caractères. Il définit l’encodage de chaque glyphe. Par exemple, utfmb4 est un set de caractères correspondant à l’UTF-8 dans les BDD MySQL. Collation : Une règle de tri des caractères les uns par rapport aux autres. Est-ce que a doit être rangé entre A et B ou plutôt après Z ? Par exemple, avec la collation utf8mb4_unicode_ci de MySQL, le ß, le double s allemand, sera considéré comme ss et œ est considéré comme oe. En revanche, avec la collation utf8mb4_general_ci, le ß est considéré comme un s simple et le œ est considéré comme un e.

Section intitulée les-differents-sets-de-cracteresLes différents sets de cractères

PostgeSQL c’est le bon élève de cette histoire : son set de caractère correspondant à UTF-8 se nomme sobrement UTF8, et se comporte bien comme tel : il est encodé sur 1 à 4 octets de 8 bits.

Chez Oracle, il faudra utiliser le set de caractères nommé AL32UTF8. Leur UTF8 correspond à la norme Unicode 3.0 mais n’est plus à jour aujourd’hui.

Chez MariaDB et MySQL, il faut utiliser utfmb4, et pas utf8 qui est encodé sur 3 octets. Vous pouvez lire cet article très intéressant, si vous souhaitez en apprendre plus sur l’existence de cet étrange set de caractères.

En terme de sécurité, ici aussi l’encodage a été source de problèmes. Lorsqu’on veut insérer un caractère dont la valeur est supérieure à 0xFFFF dans une colonne dont le charset est utf8, MySQL va tronquer la chaîne au niveau du caractère en question. Par exemple, si je veux insérer “Hello 👋 world 🌍 !”, seul “Hello ” sera inséré puisque l’emoji a pour valeur 0xF09F918B, qui est donc bien supérieure à 0xFFFF.

Ainsi, un attaquant peut s’inscrire sur un site dont l’inscription est restreinte par domaine simplement en ajoutant un emoji à la fin de la chaîne qu’il veut voir insérer en base. Par exemple, l’attaquant s’inscrit avec “attacker@gmail.com🍕@allowed-domain.com”. Le site effectue un check sur “@allowed-domain.com” et autorise l’inscription, mais seulement “attacker@gmail.com” !

C’est ici un exemple assez naïf, mais j’espère que vous faites des vérifications plus solides sur les mails de vos utilisateurs !

En tous cas nous vous encourageons à faire attention aux sets de caractères que vous utilisez.

Section intitulée pour-conclurePour conclure

Éviter les erreurs d’encoding est particulièrement simple à notre époque, du moment que tout est configuré avec UTF-8. Symfony nous propose également des outils dédiés afin que nous puissions développer sans nous poser de questions. Alors plus d’excuses pour laisser traîner du mojibake !

Commentaires et discussions

Ces clients ont profité de notre expertise