Les fonctions pack, unpack et vec de Perl

Supplément au cours PLURITAL n°1 du 24 septembre 2013

Révision du 15/10/2013 (merci à Pierre Marchal !).

Jean-François Perrot

Du point de vue du cours GIM, ces fonctions permettent d'explorer les phénomènes de codage et de décodage de l'information.
Pour l'information disponible en mémoire centrale elles jouent un rôle analogue à celui de l'éditeur hexadécimal pour les fichiers.

Attention ! leur maniement est délicat, et l'absence de typage en Perl, allié à la plasticité de la syntaxe, peut conduire à des résultats déroutants.
Pour de plus amples explications, voyez par exemple ce tutoriel.
Pour un traitement systématique des rapports entre Perl et le binaire, voyez le chapitre Working with Bits d'un ouvrage au titre ambitieux (Mastering Perl) paru chez O'Reilly en 2007.

  1. La fonction pack
    1. Caractères
    2. Hexa
    3. Binaire

  2. La fonction unpack

  3. Combinaison des deux fonctions pack et unpack

  4. Et les nombres entiers ?
    1. Obtenir un entier par pack
      1. Attention ! chaînes !
      2. Conséquence : pour avoir des entiers...
    2. Retrouver une chaîne par unpack d'un entier
      1. Chaîne décimale
      2. Chaîne binaire ou hexadécimale
    3. Conversion entre chaînes représentant le même entier
      1. À partir d'une chaîne décimale
      2. À partir d'une chaîne hexadécimale
      3. À partir d'une chaîne en octal
      4. À partir d'une chaîne binaire

  5. La fonction vec
    1. Accès aux octets
    2. Accès aux bits
    3. Autres formats


  1. La fonction pack

    Attention ! Il est essentiel que les valeurs de la liste L et le format F soient compatibles...
    C'est une observation de bon sens, mais prenez garde qu'en programmation le bon sens n'est pas toujours facile à discerner. Perl ne vous enverra que rarement un message d'erreur intelligible...
    En outre, la syntaxe des chaînes de format n'est pas totalement banale.

    Exemples : 
    N.B. Les suites d'octets construites par pack sont destinées à la mémoire centrale et ne sont donc que rarement imprimables directement avec un résultat compréhensible. Il faut en général faire appel à la fonction inverse unpack pour les observer.
    Nos premiers essais seront donc limités.
    1. Caractères

      La chaîne de format 'C' annonce que la valeur correspondante dans la liste L représente un caractère (8 bits).
      Cette valeur doit être un entier entre 0 et 255.

      my @lc = (77, 109, 68, 100, 69, 101);
      pack('C', @lc) ---> la chaîne 'M' de longueur 1
      pack('CCC', @lc) ---> la chaîne 'MmD' de longueur 3
      pack('C5', @lc) ---> la chaîne 'MmDdE' de longueur 5
      pack('C*', @lc) ---> la chaîne 'MmDdEe' de longueur égale à celle de la liste @lc

      En particulier, on voit que pack('C', $x) est synonyme de chr($x).

    2. Hexa

      La chaîne de format 'H' annonce que la valeur correspondante dans la liste représente un entier en hexadécimal.
      Cette valeur doit être un chiffre hexadécimal, et le nombre de chiffres dans le format doit correspondre à la longueur du nombre : 'H2' permet de lire un octet.

      Pour obtenir le même résultat 'MmDdEe' que ci-dessus, il faut partir de
      my @lh = ('4d', '6d', '44', '64', '45', '65');
      et demander
      pack('H2', @lh) ---> 'M'
      pack('H2H2', @lh) ---> 'Mm'
      pack('H2'x6, @lh) ---> 'MmDdEe'

      ou bien de
      my $nh = '4d6d44644565';
      en demandant pack('H12', $nh) ou pack('H*', $nh) ---> 'MmDdEe'

    3. Binaire

      La chaîne de format 'B' annonce que la valeur correspondante dans la liste représente un entier en binaire.
      Cette valeur doit être un chiffre binaire, et le nombre de chiffres dans le format doit correspondre à la longueur du nombre : 'B8' permet de lire un octet.

      Pour obtenir le même résultat 'MmDdEe' que ci-dessus, il faut partir de
      my @lb = ('01001101', '01101101', '01000100', '01100100', '01000101', '01100101');
      et demander
      pack('B8', @lb) ---> 'M'
      pack('B8B8', @lb) ---> 'Mm'
      pack('B8'x6, @lb) ---> 'MmDdEe'

      ou bien de
      my $nb = '010011010110110101000100011001000100010101100101';
      en demandant pack('B48', $nb) ou pack('B*', $nb) ---> 'MmDdEe'
       
  2. La fonction unpack

    Comme son nom l'indique, elle effectue la transformation inverse de celle de pack.
    Elle

    Exemples inverses de exemples ci-dessus :

    my $chn = 'MmDdEe';

    unpack('C*', $chn) ---> (77, 109, 68, 100, 69, 101)
    unpack('C2', $chn) ---> (77, 109)
    et unpack('C', $x) s'avère synonyme de ord($x).

    unpack('H*', $chn) ---> (4d6d44644565)
    unpack('H2'x2, $chn) ---> (4d, 6d)

    unpack('B*', $chn) ---> (010011010110110101000100011001000100010101100101)
    unpack('B8', $chn) ---> (01001101)

    Attention ! Ces exemples fonctionnent correctement parce que la chaîne est compatible avec les formats employés.
    Si cette chaîne comportait des caractères chinois, par exemple, nous aurions des avertissements (incompréhensibles)
    comme Character in 'H' format wrapped in unpack at...
    et des résultats complètement faux.
    Ceci est dû à la sémantique des chaînes en Perl (chaînes de caractères vs. chaînes d'octets) dont nous parlerons plus tard
    dans les cours GIM n°3 et 4.
  3. Combinaison des deux fonctions pack et unpack

    Elle permet d'effectuer différentes sortes de conversions.

  4. Et les nombres entiers ?

  5. La fonction vec

    Elle permet d'accéder en lecture et en écriture, individuellement, aux octets qui composent une chaîne,
    et même aux bits eux-mêmes !
    On peut ainsi effectuer des opérations "chirurgicales" au niveau le plus intime de l'information.
    N.B. La sémantique des chaînes évoquée précédemment (au sujet de unpack()) ne concerne pas la fonction vec(),
    qui travaille sur les octets, quel que soit le codage des caractères en vigueur.

    1. Accès aux octets

      Étant donné une chaîne $chn, et un entier k l'expression "vec ($chn, k, 8)" désigne le k-ième octet de $chn,
      donné par sa valeur entière.

      Exemple : $chn = 'MmDdEe';
      vec ($chn, 0, 8)
      ---> 77, vec ($chn, 5, 8) ---> 101, vec ($chn, 3, 8) ---> 100.
      et l'affectation
      vec ($chn, 2, 8) = 97;
      a pour effet de modifier in situ la chaîne 'MmDdEe' en 'MmadEe'.
      (in situ, c'est-à-dire que la chaîne modifiée se trouve à la même adresse en mémoire,
      au contraire de ce que produirait l'affectation $chn = 'MmadEe';
      qui provoquerait la création d'une nouvelle chaîne, à une autre adresse.)
      Application : une fonction qui échange majuscules et minuscules dans une chaîne ASCII.

      sub altMajMin8($){
      my ($str) = @_;
          my $k = length($str); #longueur en octets
          for( my $i = 0; $i < $k; $i++ ){
              my $n = vec($str, $i, 8);
              if( ($n > 96) && ($n < 123) ){ #minuscule
                  $n -= 32;
              }elsif( ($n > 64) && ($n < 91) ){ #majuscule
                  $n += 32;
              }
              vec($str, $i, 8) = $n;
          }#for
          return $str;
      }#altMajMin8

      altMajMin8('Just Another Perl Hacker,') ---> 'jUST aNOTHER pERL hACKER,'

      N.B.1 : Le caractère in situ de la transformation ne joue ici aucun rôle, puisqu'en raison de l'appel par valeur
      la fonction travaille sur une copie de son argument.

      N.B.2 : Dans ce petit programme, l'emploi de la fonction length() pour obtenir le nombre d'octets de la chaîne-argument
      n'est valable que si le mode "utf8" n'est pas activé (par use utf8;).
      Dans le cas contraire, et si la chaîne traitée n'est pas en ASCII, il faut demander d'abord use Encode;,
      et appeler ensuite length(encode_utf8($str)), ou encore length(Encode::_utf8_off($str)).
      C'est à nouveau la sémantique des chaînes de Perl qui est en cause...
    2. Accès aux bits

      Le troisième argument de la fonction vec doit prendre comme valeur une puissance de 2 : 1, 2, 4, 8, 16, 32.
      Nous venons de voir la signification de la valeur 8 : on découpe la chaîne (1er arg.) en blocs de 8 bits, et le deuxième argument repère le bloc choisi, dans l'ordre de gauche à droite.
      Les autres les valeurs signifient aussi : on découpe la chaîne en blocs de n bits, et le deuxième argument repère le bloc choisi, mais dans un ordre plus compliqué.

      Voyons le cas de vec ($chn, k, 1), où les blocs sont les bits.
      avec $chn = 'MmDdEe'
      c'est à dire '01001101', '01101101', '01000100', '01100100', '01000101', '01100101'.
      • vec ($chn, 0, 1) désigne le dernier bit du premier octet ---> 1
      • vec ($chn, 7, 1) désigne le premier bit du premier octet ---> 0
      • vec ($chn, 9, 1) désigne l'avant-dernier bit du deuxième octet ---> 0
      • vec ($chn, 14, 1) désigne le deuxième bit du deuxième octet ---> 1

      et en général,  vec ($chn, p+8q, 1) , avec p < 8, désigne le p-ième bit du q-ième octet lu de droite à gauche.

      La transformation majInit(), synonyme de ucfirst(), que nous avons laborieusement programmée avec pack et unpack,
      se réalise en une seule instruction : vec ($chn, 5, 1) = 0;

      Application : autre réalisation de la fonction ci-dessus.

      sub altMajMin1($){
      my ($str) = @_;
          my $k = length($str); #longueur en octets
          for( my $i = 0; $i < $k; $i++ ){
              my $n =  vec($str, $i, 8);
              if( ($n > 96) && ($n < 123) ){ #minuscule
                   vec($str, 8*$i+5, 1) = 0;
              }elsif( ($n > 64) && ($n < 91) ){ #majuscule
                   vec($str, 8*$i+5, 1) = 1;
              }
          }#for
          return $str;
      }#altMajMin1

      altMajMin1('Just Another Perl Hacker,') ---> 'jUST aNOTHER pERL hACKER,'

    3. Autres formats

      Comme on l'a dit, le troisième argument de la fonction vec règle la taille des blocs du découpage,
      et le deuxième désigne lequel de ces bloc est visé. Il s'agit de savoir comment se fait cette désignation.

      • Si la taille est 8, les blocs sont les octets, énumérés de gauche à droite,
        et les valeurs de la fonction sont leurs valeurs numériques.

      • si la taille est inférieure à 8 (1, 2 ou 4), les blocs sont repérés de droite à gauche dans les octets énumérés de gauche à droite :
        • on a vu ci-dessus le cas de la taille 1, où les blocs sont les bits eux-mêmes ;
        • en taille 2, donc 4 blocs par octet, le n° p+4q (p < 4)
          désigne le p-ième bloc de 2 bits (de droite à gauche) dans le  q-ième octet ;
        • en taille 4, donc 2 blocs par octet, le n° p+2q (p = 0 ou 1)
          désigne le dernier (resp. le premier) bloc de 4 bits dans le q-ième octet.

        Exercice : écrivez deux versions de la fonction altMajMin() en tailles 2 et 4.

        Ici aussi, les valeurs de la fonction sont les valeurs numériques des blocs binaires.

      • Si la taille est supérieure à 8 (16 ou 32), on retrouve la numérotation des blocs de gauche à droite,
        et les valeurs sont les entiers obtenus en leur appliquant selon le cas pack('n',...) ou pack('N',...).
        On peut donc examiner la structure de ces blocs en appelant unpack avec le format idoine, modifier cette structure ad libitum, la packer de nouveau et réaffecter l'entier pour modifier la chaîne in situ...

        Exemples :
        • en taille 16, avec $chn = 'MmDdEe'
          vec ($chn, 0, 16) ---> 19821
          vec ($chn, 1, 16) ---> 17508
          vec ($chn, 2, 16) ---> 17765

        • en taille 32, avec $chn = 'Just Another Perl Hacker'
          vec ($chn, 0, 32) ---> 1249211252
          vec ($chn, 5, 16) ---> 1667982706