Manipulation de base des buffers, jeux de caractères et endianness

Avant-propos

Le but de ces execercices est de manipuler les ByteBuffer. Comme nous n'avons pas encore vu de primitives réseau, nous utiliserons les fichiers pour lire et écrire des octets.

Dans ces exercices, vous écrirez tout votre code dans le main. Nous ferons mieux plus tard mais pour l'instant ce n'est pas le but des exercices.

Endianness

On veut écrire un programme StoreWithByteOrder qui lit des long au clavier et qui les écrits dans un fichier. Le programme prend en argument sur la ligne de commande :
  • l'ordre dans lequel les octets des longs seront écrits dans le fichier : LE pour little-endian et BE pour big-endian;
  • le nom du fichier dans lequel écrire les longs.

Par exemple, l'appel java fr.uge.net.buffer.StoreWithByteOrder LE foo.bin écrira les longs entrés au clavier dans foo.bin en little-endian.

En commençant avec le fichier StoreWithByteOrder.java, écrire le program StoreWithByteOrder.

Tests

Pour tester votre code, vous devez savoir exécuter un programme java depuis la ligne de commande (dans un terminal). Vous devez aussi savoir comment lancer un jar. Vous trouverez des instructions détaillées ici.
Test 1

Si vous exécutez :

% java StoreWithByteOrder BE long-be.bin
1
^D
vous devriez obtenir un fichier long-be.bin contenant 7 octets valant 0 suivis d'un octet valant 1. Pour voir la valeur des octets dans un fichier, nous vous fournissons l'outil File2Hex.jar qui affiche la valeur en hexadecimal de chaque octet du fichier. En utilisant cet outil, vous devriez obtenir :
% java -jar File2Hex.jar long-be.bin
00 00 00 00 00 00 00 01

Test 2

Si vous exécutez :

% java StoreWithByteOrder LE long-le.bin
1
^D
vous devriez obtenir un fichier long-le.bin contenant un octet valant 1 suivi de 7 octets valant 0. En utilisant File2Hex.jar, vous devriez obtenir :
% java -jar File2Hex.jar long-le.bin
01 00 00 00 00 00 00 00

Lecture d'un fichier texte

Nous voulons écrire un program FileWithEncoding qui prend deux paramètres :

Le programme lit le fichier, le décode avec le charset donné et affiche la chaîne obtenue.

L'API Java offre la méthode String Files.readString​(Path path, Charset cs) qui fait précisément cela. Cependant dans cet exercice, nous vous demandons d'accéder au fichier par un FileChannel qui permet simplement de lire (et d'écrire) les octets bruts. Vous devrez utiliser la méthode charset.decode pour décoder ces octets. Ce n'est pas la manière optimale de procéder mais on s'en satisfera pour ce cours (cf. Exercice 4).

Dans le fichier ReadFileWithEncoding.java, complétez la méthode stringFromFile en utilisant un FileChannel.

Vous pouvez obtenir la taille en octets d'un fichier par la méthode fileChannel.size().
Attention : Même si buffer a la même taille que votre fichier, la méthode fileChannel.read(buffer) ne garantit pas de remplir buffer en un seul appel. Vous devrez l'appeler jusqu'à ce que buffer soit plein (i.e., !buffer.hasRemaining()).

Tests

Vous pouvez tester votre programme avec le fichier test.txt. Attention, il faut faire "enregistrez-sous" pour télécharger le fichier et ne surtout pas faire de copier-coller. Si votre code est correct, vous devriez voir :

% java ReadFileWithEncoding utf8 test.txt
a€
Alors que,
% java ReadFileWithEncoding iso-8859-1 test.txt
aâ ¬

Il n'y a pas de magie ! Le fichier test.txt contient les 5 octets 61 E2 82 AC 0A. Le fichier n'a pas d'encodage préféré.

Si on le decode en utilisant le charset UTF8, les 5 octets seront interprétés comme suit :

61 -> a
E2 82 AC -> €
0A -> line return
En UTF8, les caractères sont représentés par un nombre variable d'octets. Vous pouvez apprendre ici comment cela marche.

Dans le charset iso-8859-1, chaque caractère est codé par un octet. Les octets du fichier sont interprétés comme suit :

61 -> a
E2 -> â
82 -> control caracter that cannot be printed 
AC -> ¬
0A -> line return

Lecture sur l'entrée standard

On veut écire un programme ReadStandardInputWithEncoding qui lit tous les octets de l'entrée standard et les décode dans un charset donné. Le programme prendra le nom du charset sur la ligne de commande.

Ce programme a vocation à être utilisé comme suit :

$ cat test.text | java ReadStandardInputWithEncoding utf8

D'habitude, vous accédez à l'entrée standard par un Scanner mais pour cet exercice, nous vous demandons d'y accéder comme un flux d'octets. On obtient un ReadableByteChannel correspondant à l'entrée standard avec :

ReadableByteChannel in = Channels.newChannel(System.in);

Un ReadableByteChannel se comporte comme un FileChannel en lecture sauf que nous ne savons pas à l'avance combien d'octets vont arriver. Le seul moyen de savoir que tous les octets ont été lus est d'attendre que la méthode readableByteChannel.read renvoie -1. Vous devrez donc lire dans un buffer de taille fixe et l'agrandir quand il est plein.

Dans le fichier ReadStandardInputWithEncoding.java, écrire la méthode stringFromStandardInput qui lit tous les octets de l'entrée standard et renvoie la chaîne correspondante.

Tests

Pour tester votre code, vous pouvez utiliser :

% cat test.txt | java fr.uge.net.buffers.ReadStandardInputWithEncoding utf8 

Pour tester que vous agrandissez bien votre buffer de lecture, vous pouvez utiliser le fichier test2.txt.

% cat test2.txt | java ReadStandardInputWithEncoding utf8 
Vérifiez que vous avez bien toutes les lignes de test2.txt. En particulier, vous devriez obtenir:
 
% cat test2.txt | wc
   10000   40000 2208890   
% cat test2.txt | java ReadStandardInputWithEncoding utf8 | wc
   10000   40000 2208890  

Encoders et Decoders (facultatif, à faire à la maison)

Dans les exercices précédents, nous avons codé des chaînes de caractères en utilisant différents jeux de caractères. Pour ce faire, nous avons utilisé les méthodes encode et decode du Charset correspondant.

Cette approche, qui est la seule que nous utilisons dans le cours (pour simplifier), a plusieurs inconvénients majeurs :

Il existe une méthode plus efficace : utiliser les classes CharsetEncoder et CharsetDecoder.

Par exemple, pour décoder un ByteBuffer buffer avec un Charset charset, l'appel charset.decode(buffer) est équivalent à
      charset.newDecoder()
        .onMalformedInput(CodingErrorAction.REPLACE)
        .onUnmappableCharacter(CodingErrorAction.REPLACE)
        .decode(buffer); 
 
Dans l'exercice précédent, il fallait attendre la fin de l'entrée avant de commencer le décodage, afin d'être sûr de pouvoir décoder la fin. En utilisant le CharsetDecoder produit lors de l'appel charset.newDecoder(), on va pouvoir décoder les octets au fur et à mesure.

Les Decoder (ainsi que les Encoder), consomment des octets (ou caractères) d'un buffer d'entrée, les traduisent et écrivent les caractères (ou octets) résultants dans un CharBuffer de sortie. La classe CharsetDecoder a une méthode final CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput), où le dernier paramètre : endOfInput indique si nous sommes arrivés à la fin de l'entrée. Chaque invocation de la méthode decode (encode) décode (encode) autant d'octets que possible du buffer d'entrée, en écrivant les caractères résultants dans le buffer de sortie.

Attention la méthode decode ne va pas nécessairement consommer toute la zone de travail du buffer d'entrée. Il faudra bien prendre soin de ne pas perdre les octets restants qui seront utilisés lors du prochain appel à encode

L'état partiel du décodage est indiqué par l'objet de classe CoderResult. L'appel à decode (encode) se termine pour l'une des quatre raisons suivantes :
  1. Underflow : il n'y a plus d'entrée à traiter, ou l'entrée est insuffisante.
  2. Overflow : un débordement est signalé lorsque la place restante dans le buffer de sortie est insuffisante.
  3. Malformed-input error : une erreur d'entrée mal formée est signalée lorsque la séquence d'entrée n'est pas bien formée.
  4. Unmappable-character error : une erreur de caractère non applicable est signalée si une séquence d'entrée désigne un caractère qui ne peut être représenté dans le jeu de caractères de sortie.
Ceci est indiqué par un objet de classe CoderResult. Notez dans le code équivalent à charset.decode(buffer) plus en haut qu'il est possible de fixer des actions à faire en cas d'erreur (malformed-input ou unmappable-character). Ici, nous voulons ignorer les symboles produisant l'erreur en les remplaçant par la chaîne vide "". L'action par défaut pour les erreurs de type "malformed-input" et "unmappable-character" est de les signaler (soit avec un objet de classe CodeResult ou une exception).

Lorsque la méthode decode (encode) est appelée pour la dernière fois, en passant true pour l'argument endOfInput, il faut faire suivre cet appel par un appel à la méthode flush pour que l'encodeur puisse vider tout état interne dans le buffer de sortie. Si la méthode flush se termine avec succès, elle renvoie CoderResult.UNDERFLOW.

Nous allons reprendre l'exercice 2 en utilisant un CharsetDecoder. Votre programme doit lire un fichier (avec FileChannel) avec un buffer d'entrée de taille fixe. On vous demande en plus d'afficher les caractères décodés au fur et à mesure du décodage.
Complétez la méthode stringFromFile du code DecoderOnTheFly.java.

Votre programme doit être capable de gérer des gros fichiers, ainsi que des fichiers invalides, en levant une IllegalArgumentException si le fichier est "Malformed" ou s'il y a un "Unmpappable character".
Testez avec les exemples test-utf8.txt et utf8-malformed.txt. Par exemple :
 
% java  fr.uge.net.buffers.ReadFileWithDecoder utf8 test-utf8.txt 4
a€
#¶a
sfs

a€#¶asfs

% java  fr.uge.net.buffers.ReadFileWithDecoder utf8 utf8-malformed.txt 4
Exception in thread "main" java.lang.IllegalArgumentException: MALFORMED INPUT
...