Série 7 – Identification d'encodage : corrigé

Introduction

Le code du corrigé est disponible sous la forme d'une archive Zip et les solutions des différents exercices sont présentées ci-dessous.

Exercice 1

La méthode validCharsets doit faire évoluer l'automate ASCII, à partir de son état initial, en lui fournissant les octets successifs (et décompressés) du fichier dont le nom lui est passé en argument. Une fois la fin du fichier atteinte, on sait qu'il s'agit d'un fichier ASCII valide si et seulement si l'état de l'automate à ce moment est un état final.

L'état de l'automate est gardé dans la variable sASCII, initialisée avec l'état initial (obtenu de la méthode initialState) et modifiée à chaque itération au moyen de la méthode nextState appliquée à l'octet suivant du fichier. La variable sASCII a le type Optional<ASCIIState>, ce qui permet d'exprimer le fait que l'automate est bloqué — c-à-d qu'il n'a pu effectuer de transition à un moment ou à un autre — au moyen de la valeur optionnelle vide.

L'obtention des octets du fichier se fait facilement, il faut juste penser à filtrer les octets au moyen d'un flot filtrant de type GZIPInputStream afin de les obtenir sous forme décompressée.

public static Set<Charset> validCharsets(String fileName)
    throws IOException {
  Optional<ASCIIState> sASCII =
    Optional.of(ASCII.initialState());

  try (InputStream s = new GZIPInputStream(
                         new FileInputStream(fileName))) {
    int b;
    while ((b = s.read()) != -1) {
      sASCII = sASCII.isPresent()
        ? ASCII.nextState(sASCII.get(), b)
        : sASCII;
    }
  }

  Set<Charset> validCharsets = new HashSet<>();
  if (sASCII.isPresent()
      && ASCII.isAccepting(sASCII.get()))
    validCharsets.add(StandardCharsets.US_ASCII);

  return validCharsets;
}

Une fois la méthode validCharsets écrite, il reste à écrire le programme principal qui itère sur les fichiers. Lorsque la méthode validCharsets retourne un ensemble vide, le fichier est ignoré ; sinon, un encodage de caractères quelconque parmi ceux retournés est choisi — au moyen d'un itérateur dont on obtient le premier élément — et utilisé pour décoder le fichier et afficher son contenu.

public static void main(String[] args) throws IOException{
  for (int fileI = 1; fileI <= 6; ++fileI) {
    String fileN = String.format("f%d.txt.gz", fileI);
    Set<Charset> charsets =
      CharsetIdentifier.validCharsets(fileN);
    if (charsets.isEmpty())
      continue;
    Charset charset = charsets.iterator().next();
    try (Reader r = new InputStreamReader(
                      new GZIPInputStream(
                        new FileInputStream(fileN)),
                      charset)) {
      int c;
      while ((c = r.read()) != -1) {
        System.out.print((char)c);
      }
    }
  }
}

Exercice 2

L'automate correspondant à l'encodage ISO 8859-1 n'est pas très différent de celui correspondant à ASCII, et est constitué comme lui d'une seule règle de transition :

private enum ISO8859_1State { OK };
private static DFA<ISO8859_1State> dfaForISO8859_1() {
  return new DFA.Builder<>(ISO8859_1State.OK,
                           EnumSet.of(ISO8859_1State.OK))
    .addRule(ISO8859_1State.OK,
             b -> ((0 <= b && b <= 0x7F)
                   || (0xA0 <= b && b <= 0xFF)),
             ISO8859_1State.OK)
    .build();
}
private static DFA<ISO8859_1State> ISO8859_1 =
  dfaForISO8859_1();

Une fois cet automate défini, il n'est pas difficile d'augmenter la méthode validCharsets. Notez toutefois qu'il n'est pas possible, comme on le souhaiterait, de placer les automates dans une liste et de les traiter de manière uniforme, et il faut donc écrire du code similaire pour gérer chacun des automates.

public static Set<Charset> validCharsets(String fileName)
    throws IOException {
  Optional<ISO8859_1State> sISO8859_1 =
    Optional.of(ISO8859_1.initialState());
  try (/* … comme avant */) {
    int b;
    while ((b = s.read()) != -1) {
      // … comme avant
      sISO8859_1 = sISO8859_1.isPresent()
        ? ISO8859_1.nextState(sISO8859_1.get(), b)
        : sISO8859_1;
    }
  }
  Set<Charset> validCharsets = new HashSet<>();
  // … comme avant
  if (sISO8859_1.isPresent()
      && ISO8859_1.isAccepting(sISO8859_1.get()))
    validCharsets.add(StandardCharsets.ISO_8859_1);
  return validCharsets;
}

Exercice 3

L'automate correspondant à l'encodage UTF-8 est relativement complexe, mais sa régularité peut être exploitée pour simplifier le code. Ainsi, les transitions étiquetées H sont similaires, et la méthode utf8InitB ci-dessous permet de générer le prédicat correspondant à l'une d'elles. Quant à la transition C, son prédicat peut être défini une fois pour toutes (variable utf8ContB) et réutilisé.

private enum UTF8State {
  OK,
  B2_2,
  B2_3, B3_3,
  B2_4, B3_4, B4_4
};

private static Predicate<Integer> utf8InitB(int length) {
  int mask = (0xFF << (7 - length)) & 0xFF;
  int value = (mask << 1) & 0xFF;
  return b -> (b & mask) == value;
}

private static DFA<UTF8State> dfaForUTF8() {
  Predicate<Integer> utf8ContB =
    b -> (b & 0b11000000) == 0b10000000;
  return new DFA.Builder<>(UTF8State.OK,
                           EnumSet.of(UTF8State.OK))
    .addRule(UTF8State.OK,
             b -> (b & 0x80) == 0,
             UTF8State.OK)

    .addRule(UTF8State.OK, utf8InitB(2), UTF8State.B2_2)
    .addRule(UTF8State.B2_2, utf8ContB, UTF8State.OK)

    .addRule(UTF8State.OK, utf8InitB(3), UTF8State.B2_3)
    .addRule(UTF8State.B2_3, utf8ContB, UTF8State.B3_3)
    .addRule(UTF8State.B3_3, utf8ContB, UTF8State.OK)

    .addRule(UTF8State.OK, utf8InitB(4), UTF8State.B2_4)
    .addRule(UTF8State.B2_4, utf8ContB, UTF8State.B3_4)
    .addRule(UTF8State.B3_4, utf8ContB, UTF8State.B4_4)
    .addRule(UTF8State.B4_4, utf8ContB, UTF8State.OK)
    .build();
}

private static DFA<UTF8State> UTF8 = dfaForUTF8();

L'augmentation de la méthode validCharsets se fait comme d'habitude et n'est donc pas présentée en détail.

Exercice 4

L'automate correspondant à l'encodage UTF-16BE n'est pas très difficile à définir :

private enum UTF16BEState {
  OK, B2_2,
  B2_4, B3_4, B4_4
};

private static DFA<UTF16BEState> dfaForUTF16BE() {
  return new DFA.Builder<>(UTF16BEState.OK,
                           EnumSet.of(UTF16BEState.OK))
    .addRule(UTF16BEState.OK,
             b -> ((0 <= b && b <= 0xD7)
                   || (0xE0 <= b && b <= 0xFF)),
             UTF16BEState.B2_2)
    .addRule(UTF16BEState.B2_2,
             b -> true,
             UTF16BEState.OK)

    .addRule(UTF16BEState.OK,
             b -> 0xD8 <= b && b <= 0xDB,
             UTF16BEState.B2_4)
    .addRule(UTF16BEState.B2_4,
             b -> true,
             UTF16BEState.B3_4)
    .addRule(UTF16BEState.B3_4,
             b -> 0xDC <= b && b <= 0xDF,
             UTF16BEState.B4_4)
    .addRule(UTF16BEState.B4_4,
             b -> true,
             UTF16BEState.OK)
    .build();
}

private static DFA<UTF16BEState> UTF16BE =
  dfaForUTF16BE();

L'augmentation de la méthode validCharsets se fait comme d'habitude et n'est donc pas présentée en détail.