Conversion de l'encodage URL en UTF-8

Décodage de application/x-www-form-urlencoded en UTF-8 correspondant

Lors d'une communication entre un navigateur et un serveur Web, les données hors us-ascii sont converties dans un format spécial, x-www-form-urlencoded par défaut.

Par exemple la chaîne l'hiver est froid est convertie en l%E2%80%99hiver est froid.

la chaîne %E2%80%99 traduit l'entité ’ mais nous voyons bien que les deux valeurs sont différentes. Pour traduire la valeur URl en valeur humainement exploitable, il faut faire un opération de conversion.

Conversion

Méthode simple

La première méthode consiste à récupérer au fur et à mesure la correspondance entre le code URL et le résultat entité HTML. Mais cette méthode est longue et nécessite de tester tous les caractères potentiellement utilisés puis de la passer dans un filtre qui va effectuer la conversion; sed le fait très bien. Il suffira d'inscrire toutes les conversions dans un fichier texte (enregistré au format utf-8) :

s/%E2%80%99/\&#8217\;/g;s/%E2%82%AC/\&#8364\;/g;s/%2F/\//g;s/%3F/\?/g;s/%24/\$/g;s/%21/\!/g;s/%25/%/g;s/%2B/+/g;s/%3C/\\&lt\;/g;s/%3E/\&gt\;/g;s/%5C/\\/g;s/%B0/°/g
s/%E0/à/g;s/%E2/â/g
s/%E8/è/g;s/%E9/é/g;s/%EA/ê/g;s/%EB/ë/g
s/%EE/î/g;s/%EF/ï/g
s/%F4/ô/g
s/%F9/ù/g;s/%FB/û/g
s/%2C/,/g;s/%21/!/g;s/%3F/?/g;s/%40/@/g;s/%23/#/g;s/%24/$/g
s/%25/%/g;s/%26/\&#x26\;/g;s/%28/(/g;s/%29/)/g;s/%2B/+/g;s/%3A/:/g;s/%3D/=/g;s/%5B/[/g;s/%5D/]/g
s/%3B/;/g;s/%2F/\//g;s/%27/''/g;s/%22/"/g;s/%C3%A7/ç/g;s/%E7/ç/g
s/+/ /g;s/%7E/~/g;s/\\/\&#92\;/g
s/%0D/\&#x0D\;/g;s/%0A/\&#x0A\;/g
s/%C3%A0/à/g;s/%C3%A2/â/g
s/%C3%A9/é/g;s/%C3%A8/è/g;s/%C3%AA/ê/g
s/%C3%AE/î/g;s/%C3%AF/ï/g
s/%C3%B4/ô/g
s/%C3%B9/ù/g;s/%C3%BB/û/g
s/%C2%B7/-/g;s/%C2%AB/«/g;s/%C2%BB/»/g
...

et de l'invoquer avec la commande sed :

cat ${données à convertir} | sed -f ${fichier de paramétrage}

Mais l'on est jamais à l'abri d'un oubli. Cette méthode n'est pas fiable.

Méthode algorithmique

Pour cette méthode, il faut déjà comprendre l'opération qui est effectuée entre le code URL et l'entité HTML.

La méthode d'encodage stocke de façon précise non seulement le caractère mais aussi la longueur sur lequel il est codé. Ce qui permet d'encoder plusieurs caractères à la suite sans avoir besoin de mettre de séparateur qui serait difficilement interprétable, le décodage ne sachant pas alors s'il s'agit d'un caractère d'origine ou un caractère encodé.

Format binaire de la séquence de bytes
1er Byte2ème Byte3ème ByteNombre de bits libresValeur maximale disponible

0xxxxxxx

7

007F hex (127)

110xxxxx

10xxxxxx

(5+6)=11

07FF hex (2047)

1110xxxx

10xxxxxx

10xxxxxx

(4+6+6)=16

FFFF hex (65535)

La valeur de chaque byte pris individuellemement indique l'encodage UTF-8 de la façon suivante :

  • de 00 à 7F hex (0 à 127): un et un seul byte pour la séquence.

  • de 80 à BF hex (128 à 191): byte suivant d'une séquence multi-bytes.

  • de C2 à DF hex (194 à 223): premier byte d'une séquence de 2 bytes.

  • de E0 à EF hex (224 à 239): premier byte d'une séquence de 3 bytes.

Opération

Reprenons notre exemple %E2%80%99.

Commençons par enlever les % qui servent de séparateurs. La première partie est codée sur E2. En nous référant à la table, nous voyons que :

  • E2 est compris entre E0 et EF. Il s'agit donc du premier byte sur une séquence de 3 bytes.

  • 80 est compris entre 80 et BF. Il s'agit d'un byte suivant dans une séquence multi-bytes.

  • 99 est compris entre 80 et BF. Il s'agit d'un byte suivant dans une séquence multi-bytes.

Nous savons donc déjà que nous n'utiliserons que 3 caratères : E2, 80, 99 encodés en hexadécimal. S'il y en a plus, ils feront partie d'une ou plusieurs autres séquences (et il faudra donc recommencer l'opération).

Ensuite, enlevons les informations ajoutées. Pour cela, convertissons le caractère en binaire :

  • E2 --> 11100010

  • 80 --> 00000000

  • 99 --> 10011001

soit au total, pour 3 bytes : 111000100000000010011001

Soustrayons ensuite le masque d'encodage à ce résultat :

Le masque d'encodage est 1110xxxx 10xxxxxx 10xxxxxx, soit pour notre opération 11100000 10000000 10000000.

L'opération binaire simple donne :

  • E2 --> 11100010 - 11100000 = 00000010

  • 80 --> 00000000 - 10000000 = 00000000

  • 99 --> 10011001 - 10000000 = 00011001

Soit, au total : 00000010 00000000 00011001 --> 000000100000000000011001

Mais l'opération n'est pas finie car l'encodage nécessite d'ajouter 4 bits sur le premier octet et 2 bits sur les autres. Il faut donc les enlever :

  • 00000010 --> 10

  • 00000000 --> 000000

  • 00011001 --> 011001

Et de reconcaténer le tout :

10000000011001, ce qui, en hexadécimal, donne : 2019. ou en décimal : 8217

Le compte est bon !

Attention

Les valeurs binaires converties commencent toujours par la première valeur significative. Or, une valeur 0xxxxxxx verra son premier nombre disparaître. Il faudra donc restituer le byte en entier avant de faire les opérations.

Remarques

Ce format d'encodage possède de nombreux avantages. Il permet notamment d'obtenir des motifs uniques. En effet, l'on pourait se poser la question de savoir si E2, 80 ou 99 ne pourraient pas apparaître dans d'autres motifs que ceux de longueur 3 bytes. Et bien non ! Le simple fait d'ajouter des bits au début de chaque chaîne et que cette séquence soit séparée du reste pas un 0 rend automatiquement chaque groupe du motif unique dans son ensemble. E0 ne pourra pas être retrouvé dans un groupement de 1 ou 2 bytes. Tout comme 80 ou 99, etc.

Cela permet donc d'utiliser sed pour notre conversion. Car chaque élément étant unique dans son groupe, nous sommes certains de ne pas avoir plusieurs possibilités de décodage.

Amélioration

Nous pouvons à ce stade faire des remarques et, à partir de celles-ci, faire quelques améliorations, ou plutôt quelques raccourcis pour l'algorithme de décodage.

Le debut de l'octet encodé comprend obligatoirement un morceau de paramétrage de l'encodage et une partie codant la valeur. La permière partie est une partie fixe. Les deux parties sont obligatoirement séparées par un 0. Ce qui fait que, au lieu d'effectuer une soustraction binaire plus un troncage de la partie inutile, il suffit juste d'ôter la première partie (y compris avec le 0 intermédiaire) et nous obtenons directement le résultat. La seule différence est que nous avons considéré d'un seul coup la chaîne non pas comme une valeur binaire, mais comme une valeur chaîne. Ainsi, il n'y a qu'une seule opération à effectuer : le troncage de la partie de paramétrage.

Automatisation

Maintenant que nous avons compris la méthode, automatisons la avec, par exemple, un shell UNIX.

Nous pouvons faire quelques observations. Il faudra passer d'une partie encodée en hexadécimal en binaire, plus l'opération inverse. Ce qui fait que l'opérateur bc s'impose naturellement.

La procédure à suivre pour convertir un texte encodé est la suivante :

  1. Convertir de tous les éléments déjà référencés :

  2. QUERY_STRING=${1}
    file0=$(echo ${QUERY_STRING} | sed -f html2utf8.sed)

    Cela a pour effet d'éliminer une partie du décodage. Si le fichier html2utf8.sed n'existe pas, il faut le créer.

  3. Isoler tous les éléments de la forme %xx :

  4. echo ${file0} | sed 's/\%\([0-9A-F][0-9A-F]\)/\$\%\1\$/g' | tr '$' '\n' | grep "^%" | sed 's/^\%//g' | awk '{print NR ";" $0}' > ${file1}

    On découpe ici le texte en entrée en isolant chaque %xx sur une ligne, puis on ne récupère que cette ligne par un grep "^%". Les sauts de lignes sont forcés avec le $ introduit par sed 's/\%\([0-9A-F][0-9A-F]\)/\$\%\1\$/g' puis appliqués par tr '$' '\n'.

    awk '{print NR ";" $0}' permet de numéroter les lignes, cela sera important pour la suite, car il faudra conserver le lien entre la valeur d'origine et la valeur finale.

  5. Convertir ces lignes en binaire :

  6. cat ${file1} | awk -F";" 'BEGIN { print "ibase=16;obase=2"} {print $2}'| bc | awk '{print "00000000" $1}' | awk '{print NR ";" substr($1,length($1)-7,8)}' > ${file2}

    La partie

    awk '{print "00000000" $1}' | awk '{print NR ";" substr($1,length($1)-7,8)}'

    permet de rajouter les 0 manquants à une valeur binaire commençant par une valeur non significative. On ajoute à gauche 8 zéro, puis on ne récupère que les 8 derniers binaires pour reformer un octet valide et complet.

    De la même façon, on numérote les lignes. Ainsi la ligne binaire n correspond à a ligne n de départ.

  7. Décoder le format :

  8. join -t";" -o 1.2,2.2 -j 1 ${file1} ${file2} | awk -F";" -f html2utf8.awk | awk '{print NR ";" $0}' > ${file3}

    join -t";" -o 1.2,2.2 -j 1 ${file1} ${file2} permet de récupérer le lien source-->binaire.

    La conversion se fait par application de l'algorithme de décodage, grâce au processeur awk :

    BEGIN {}
    
    (substr($2,1,4)==1110) {
    # cas où le caractère est codé sur 3
    n=3
    m=substr($2,5,4) 
    p="\\%" $1 }
    
    (substr($2,1,3)==110) { 
    # cas où le caractère est codé sur 2
    n=2
    m=m substr($2,4,5)
    p=p "\\%" $1}
    
    (substr($2,1,2)==10) { 
    # cas d'une suite 
    n=n-1
    m=m substr($2,3,6)
    p=p "\\%" $1
    if (n==1) {
    n=0
    print p ";" m
    p=""
    m=""
    }}
    
    (substr($2,1,1)==0) { 
    # cas où le caractère est codé sur 1 
    print "\\%" $1 ";" $2
    }
    
    END {}

    Le retour commence à la fois à former le motif d'entrée de sed et de recomposer (décoder) le motif de sortie, ici sous forme binaire.

  9. Encoder le motif binaire en décimal :

  10. cat ${file3} | awk -F";" 'BEGIN { print "ibase=2"} {print $3}'| bc | awk '{print NR ";" $0}' > ${file4}

    De la même façon, on conserve la numérotation des lignes pour conserver le lien avec le motif d'origine.

  11. Formation de la commande sed :

  12. join -t";" -o 1.2,2.2 -j 1 ${file3} ${file4} | sort | uniq | awk -F";" '{print "s/" $1 "/\\&#" $2 "\\;/g"}' > ${file5}

    sort | uniq permet d'éliminer les doublons.

    La jointure permet de rassembler le motif de départ avec le motif d'arrivée sans perdre le lien.

  13. Recontruire le fichier final en ajoutant les nouveautés, le fichier s'enrichit donc automatiquement au fur et à mesure :

  14. cat html2utf8.sed > html2utf8.temp
    cat ${file5} >> html2utf8.temp
    cat html2utf8.temp | sort | uniq > html2utf8.sed
    
  15. Repasser le texte de départ dans le filtre afin de remplacer ce qui manquait :

  16. echo ${file0} | sed -f html2utf8.sed

    Ou bien, pour un traitement plus rapide, uniquement sur la partie nouvelle :

    echo ${file0} | sed -f ${file5}