web-dev-qa-db-fra.com

Comment détecter UTF-8 en C simple?

Je cherche un extrait de code en C ancien qui détecte que la chaîne donnée est en codage UTF-8. Je connais la solution avec regex, mais pour diverses raisons, il serait préférable d’éviter d’utiliser autre chose que C en clair dans ce cas particulier.

La solution avec regex ressemble à ceci (avertissement: diverses vérifications omises):

#define UTF8_DETECT_REGEXP  "^([\x09\x0A\x0D\x20-\x7E]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*$"

const char *error;
int         error_off;
int         rc;
int         vect[100];

utf8_re = pcre_compile(UTF8_DETECT_REGEXP, PCRE_CASELESS, &error, &error_off, NULL);
utf8_pe = pcre_study(utf8_re, 0, &error);

rc = pcre_exec(utf8_re, utf8_pe, str, len, 0, 0, vect, sizeof(vect)/sizeof(vect[0]));

if (rc > 0) {
    printf("string is in UTF8\n");
} else {
    printf("string is not in UTF8\n")
}
32
Konstantin

Ce décodeur de Bjoern Hoermann est le plus simple que j'ai trouvé. Cela fonctionne aussi en lui donnant un seul octet et en gardant un état. Cet état est très utile pour analyser UTF8 entrant par fragments sur le réseau.

http://bjoern.hoehrmann.de/utf-8/decoder/dfa/

// Copyright (c) 2008-2009 Bjoern Hoehrmann <[email protected]>
// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.

#define UTF8_ACCEPT 0
#define UTF8_REJECT 1

static const uint8_t utf8d[] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
  8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
  0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
  0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
  0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
  1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
  1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
  1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};

uint32_t inline
decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
  uint32_t type = utf8d[byte];

  *codep = (*state != UTF8_ACCEPT) ?
    (byte & 0x3fu) | (*codep << 6) :
    (0xff >> type) & (byte);

  *state = utf8d[256 + *state*16 + type];
  return *state;
}

Un simple validateur/détecteur n'a pas besoin du point de code, il pourrait donc être écrit ainsi (l'état initial est défini sur UTF8_ACCEPT):

uint32_t validate_utf8(uint32_t *state, char *str, size_t len) {
   size_t i;
   uint32_t type;

    for (i = 0; i < len; i++) {
        // We don't care about the codepoint, so this is
        // a simplified version of the decode function.
        type = utf8d[(uint8_t)str[i]];
        *state = utf8d[256 + (*state) * 16 + type];

        if (*state == UTF8_REJECT)
            break;
    }

    return *state;
}

Si le texte est valide, utf8 UTF8_ACCEPT est renvoyé. Si ce n'est pas valide UTF8_REJECT. Si plus de données sont nécessaires, un autre entier est renvoyé.

Exemple d'utilisation avec alimentation de données en morceaux (par exemple depuis le réseau):

char buf[128];
size_t bytes_read;
uint32_t state = UTF8_ACCEPT;

// Validate the UTF8 data in chunks.
while ((bytes_read = get_new_data(buf, sizeof(buf))) {
    if (validate_utf8(&state, buf, bytes_read) == UTF8_REJECT)) {
        fprintf(stderr, "Invalid UTF8 data!\n");
        return -1;
    }
}

// If everything went well we should have proper UTF8,
// the data might instead have ended in the middle of a UTF8
// codepoint.
if (state != UTF8_ACCEPT) {
    fprintf(stderr, "Invalid UTF8, incomplete codepoint\n");
}
29
Joakim

Vous ne pouvez pas détecter si une chaîne donnée (ou une séquence d'octets) est un texte codé en UTF-8, comme par exemple chaque série d'octets UTF-8 est également une série valide (si non sensuelle) de latin-1 octets. Cependant, toutes les séries d’octets Latin-1 valides ne sont pas des séries UTF-8 valides. Vous pouvez donc exclure les chaînes qui ne sont pas conformes au schéma de codage UTF-8:

U+0000-U+007F    0xxxxxxx
U+0080-U+07FF    110yyyxx    10xxxxxx
U+0800-U+FFFF    1110yyyy    10yyyyxx    10xxxxxx
U+10000-U+10FFFF 11110zzz    10zzyyyy    10yyyyxx    10xxxxxx   
9
Stefan Gehrig

Vous devez analyser la chaîne au format UTF-8, voir http://www.rfc-editor.org/rfc/rfc3629.txt C'est très simple. Si l'analyse échoue, ce n'est pas UTF-8. Plusieurs bibliothèques UTF-8 simples peuvent le faire.

Cela pourrait peut-être être simplifié si vous savez la chaîne est tout simplement ancienne ASCII ou elle contient des caractères extérieurs à ASCII qui sont UTF-8. codé. Dans ce cas, vous n'avez souvent pas besoin de vous soucier de la différence. UTF-8 a été conçu pour que les programmes existants capables de gérer l'ASCII puissent, dans la plupart des cas, gérer UTF-8 de manière transparente.

Gardez à l'esprit que ASCII est codé en UTF-8 en tant que tel, de sorte que ASCII est valide en UTF-8.

La chaîne AC peut être n'importe quoi, c’est le problème que vous devez résoudre et que vous ne savez pas si le contenu est ASCII, GB 2312, CP437, UTF-16, ou l’un des 12 autres encodages de caractères qui rendent difficile la vie d’un programme. ?

6
nos

Il est impossible de détecter qu'un tableau d'octets donné est une chaîne UTF-8. Vous pouvez déterminer de manière fiable qu'il ne peut s'agir d'un UTF-8 valide (ce qui ne signifie pas que ce n'est pas un UTF-8 non valide); et vous pouvez déterminer qu'il peut s'agir d'une séquence UTF-8 valide, mais elle est sujette à des faux positifs.

Pour un exemple simple, utilisez un générateur de nombres aléatoires pour générer un tableau de 3 octets aléatoires et utilisez-le pour tester votre code. Ce sont des octets aléatoires et donc pas UTF-8, donc chaque chaîne que votre code pense être "éventuellement UTF-8" est un faux positif. Mon hypothèse est que (dans ces conditions) votre code sera erroné plus de 12% du temps.

Une fois que vous reconnaissez que c'est impossible, vous pouvez commencer à penser à rétablir un niveau de confiance (en plus de votre prédiction). Par exemple, votre fonction peut renvoyer quelque chose du type "Je suis à 88% sûr qu'il s'agit du format UTF-8".

Maintenant, faites cela pour tous les autres types de données. Par exemple, vous pouvez avoir une fonction qui vérifie si les données sont au format UTF-16, ce qui pourrait renvoyer "95% de confiance: il s’agit du format UTF-16", puis décider que plus probablement que les données étaient UTF-16 et non UTF-8.

La prochaine étape consiste à ajouter des astuces pour augmenter les niveaux de confiance. Par exemple, si la chaîne semble contenir principalement des groupes de syllabes valides séparées par un espace, vous pouvez être beaucoup plus confiant qu’il s’agit bien de UTF-8. De la même manière, si les données peuvent être au format HTML, vous pouvez rechercher des balises pouvant être des balises HTML valides et les utiliser pour augmenter votre confiance.

Bien entendu, la même chose s'applique aux autres types de données. Par exemple, si les données ont un en-tête PE32 ou ELF valide ou un en-tête BMPou JPG ou MP3 correct, vous pouvez être beaucoup plus confiant que ce n'est pas du tout UTF-8.

Une approche bien meilleure consiste à résoudre la cause réelle du problème. Par exemple, il peut être possible d'ajouter une sorte d'identificateur de "type de document" au début de tous les fichiers qui vous intéressent, ou peut-être de dire "ce logiciel suppose UTF-8 et ne prend en charge rien d'autre"; de sorte que vous n'avez pas besoin de faire des suppositions douteuses en premier lieu.

2
Brendan

Vous pouvez utiliser le détecteur UTF-8 intégré à Firefox . Il se trouve dans le détecteur de jeux de caractères universel et constitue en quelque sorte une base sur la bibliothèque C++. Il devrait être extrêmement facile de trouver la classe UTF-8 reconnue et de ne prendre que cela.
En gros, cette classe détecte les séquences de caractères propres à UTF-8.

  • obtenir le dernier coffre de firefox
  • aller à\mozilla\extensions\universalchardet \
  • trouver la classe de détecteurs UTF-8 (je ne me souviens plus très bien de son nom exact)
2
shoosh

3 octets aléatoires semblent avoir 15,8% de chance d'être en UTF-8 valide selon mon calcul:

128 ^ 3 séquences possibles uniquement en ASCII = 2097152

2 ^ 16-2 ^ 11 caractères UTF-8 possibles sur 3 octets (en supposant que les paires de substitution et les non-caractères sont autorisés) = 63488

1920 caractères UTF-8 à 2 octets, soit avant, soit après un caractère ASCII = 1920 * 128 * 2 = 524288

Diviser par le nombre de séquences de 3 octets = (2097152 + 63488 + 491520) /16777216.0 = 0.1580810546875

IMHO c'est surestimer considérablement le nombre de correspondances incorrectes, car le fichier ne fait que 3 octets de long. L'intersection diminue à mesure que le nombre d'octets augmente. De plus, le texte effectif dans les formats non-UTF-8 n'est pas aléatoire. Il existe un grand nombre d'octets isolés avec le bit haut défini, qui n'est pas valide UTF-8.

Une mesure plus utile pour deviner les probabilités de défaillance est la probabilité qu'une séquence d'octets avec le bit élevé soit définie sur une valeur UTF-8 valide. Je reçois ces valeurs:

1 byte = 0% # the really important number that is often ignored
2 byte = 11.7%
3 byte = 3.03% (assumes surrogate halves are valid)
4 byte = 1.76% (includes two 2-byte characters)

Il est également utile d'essayer de trouver une chaîne lisible (dans n'importe quelle langue et tout codage) qui soit également une chaîne UTF-8 valide. Ceci est très très difficile, indiquant que ce n'est pas un problème avec des données réelles.

1
user3080602

Je sais que c'est un vieux fil, mais je pensais poster ici ma solution car je pense que c'est une amélioration par rapport à la merveilleuse solution de @Christoph (que j'ai votée à la hausse).

Je ne suis pas un expert et j'ai peut-être mal lu le RFC, mais il me semble qu'une carte de 32 octets peut être utilisée à la place d'une carte de 256 octets, ce qui permet d'économiser de la mémoire et du temps.

Cela m'a conduit à une simple macro qui avance un pointeur de chaîne d'un caractère UTF-8, stockant le point de code UTF8 dans un entier signé 32 bits et stockant la valeur -1 en cas d'erreur.

Voici le code avec quelques commentaires. 

#include <stdint.h>
/**
 * Maps the last 5 bits in a byte (0b11111xxx) to a UTF-8 codepoint length.
 *
 * Codepoint length 0 == error.
 *
 * The first valid length can be any value between 1 to 4 (5== error).
 *
 * An intermidiate (second, third or forth) valid length must be 5.
 *
 * To map was populated using the following Ruby script:
 *
 *      map = []; 32.times { map << 0 }; (0..0b1111).each {|i| map[i] = 1} ;
 *      (0b10000..0b10111).each {|i| map[i] = 5} ;
 *      (0b11000..0b11011).each {|i| map[i] = 2} ;
 *      (0b11100..0b11101).each {|i| map[i] = 3} ;
 *      map[0b11110] = 4; map;
 */
static uint8_t fio_str_utf8_map[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                                     1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 5,
                                     5, 5, 2, 2, 2, 2, 3, 3, 4, 0};

/**
 * Advances the `ptr` by one utf-8 character, placing the value of the UTF-8
 * character into the i32 variable (which must be a signed integer with 32bits
 * or more). On error, `i32` will be equal to `-1` and `ptr` will not step
 * forwards.
 *
 * The `end` value is only used for overflow protection.
 */
#define FIO_STR_UTF8_CODE_POINT(ptr, end, i32)                                 \
  switch (fio_str_utf8_map[((uint8_t *)(ptr))[0] >> 3]) {                      \
  case 1:                                                                      \
    (i32) = ((uint8_t *)(ptr))[0];                                             \
    ++(ptr);                                                                   \
    break;                                                                     \
  case 2:                                                                      \
    if (((ptr) + 2 > (end)) ||                                                 \
        fio_str_utf8_map[((uint8_t *)(ptr))[1] >> 3] != 5) {                   \
      (i32) = -1;                                                              \
      break;                                                                   \
    }                                                                          \
    (i32) =                                                                    \
        ((((uint8_t *)(ptr))[0] & 31) << 6) | (((uint8_t *)(ptr))[1] & 63);    \
    (ptr) += 2;                                                                \
    break;                                                                     \
  case 3:                                                                      \
    if (((ptr) + 3 > (end)) ||                                                 \
        fio_str_utf8_map[((uint8_t *)(ptr))[1] >> 3] != 5 ||                   \
        fio_str_utf8_map[((uint8_t *)(ptr))[2] >> 3] != 5) {                   \
      (i32) = -1;                                                              \
      break;                                                                   \
    }                                                                          \
    (i32) = ((((uint8_t *)(ptr))[0] & 15) << 12) |                             \
            ((((uint8_t *)(ptr))[1] & 63) << 6) |                              \
            (((uint8_t *)(ptr))[2] & 63);                                      \
    (ptr) += 3;                                                                \
    break;                                                                     \
  case 4:                                                                      \
    if (((ptr) + 4 > (end)) ||                                                 \
        fio_str_utf8_map[((uint8_t *)(ptr))[1] >> 3] != 5 ||                   \
        fio_str_utf8_map[((uint8_t *)(ptr))[2] >> 3] != 5 ||                   \
        fio_str_utf8_map[((uint8_t *)(ptr))[3] >> 3] != 5) {                   \
      (i32) = -1;                                                              \
      break;                                                                   \
    }                                                                          \
    (i32) = ((((uint8_t *)(ptr))[0] & 7) << 18) |                              \
            ((((uint8_t *)(ptr))[1] & 63) << 12) |                             \
            ((((uint8_t *)(ptr))[2] & 63) << 6) |                              \
            (((uint8_t *)(ptr))[3] & 63);                                      \
    (ptr) += 4;                                                                \
    break;                                                                     \
  default:                                                                     \
    (i32) = -1;                                                                \
    break;                                                                     \
  }

/** Returns 1 if the String is UTF-8 valid and 0 if not. */
inline static size_t fio_str_utf8_valid2(char const *str, size_t length) {
  if (!str)
    return 0;
  if (!length)
    return 1;
  const char *const end = str + length;
  int32_t c = 0;
  do {
    FIO_STR_UTF8_CODE_POINT(str, end, c);
  } while (c > 0 && str < end);
  return str == end && c >= 0;
}
0
Myst

En gros, je vérifie si la clé donnée (une chaîne de 4 caractères maximum) correspond au format de ce lien: http://www.fileformat.info/info/unicode/utf8.htm

/*
** Checks if the given string has all bytes like: 10xxxxxx
** where x is either 0 or 1
*/

static int      chars_are_folow_uni(const unsigned char *chars)
{
    while (*chars)
    {
        if ((*chars >> 6) != 0x2)
            return (0);
        chars++;
    }
    return (1);
}

int             char_is_utf8(const unsigned char *key)
{
    int         required_len;

    if (key[0] >> 7 == 0)
        required_len = 1;
    else if (key[0] >> 5 == 0x6)
        required_len = 2;
    else if (key[0] >> 4 == 0xE)
        required_len = 3;
    else if (key[0] >> 5 == 0x1E)
        required_len = 4;
    else
        return (0);
    return (strlen(key) == required_len && chars_are_folow_uni(key + 1));
}

Fonctionne bien pour moi:

unsigned char   buf[5];

ft_to_utf8(L'歓', buf);
printf("%d\n", char_is_utf8(buf)); // => 1
0
Emil Terman