Come rilevare UTF-8 in plain C?

Sto cercando uno snippet di codice nella semplice vecchia C che rileva che la stringa data è in codifica UTF-8. Conosco la soluzione con regex, ma per vari motivi sarebbe meglio evitare di utilizzare qualsiasi cosa tranne la C in questo caso particolare.

La soluzione con regex è simile a questa (avvertenza: varie verifiche omesse):

#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") } 

Ecco un’implementazione (auspicabilmente priva di bug) di questa espressione in chiaro C:

 _Bool is_utf8(const char * string) { if(!string) return 0; const unsigned char * bytes = (const unsigned char *)string; while(*bytes) { if( (// ASCII // use bytes[0] < = 0x7F to allow ASCII control characters bytes[0] == 0x09 || bytes[0] == 0x0A || bytes[0] == 0x0D || (0x20 <= bytes[0] && bytes[0] <= 0x7E) ) ) { bytes += 1; continue; } if( (// non-overlong 2-byte (0xC2 <= bytes[0] && bytes[0] <= 0xDF) && (0x80 <= bytes[1] && bytes[1] <= 0xBF) ) ) { bytes += 2; continue; } if( (// excluding overlongs bytes[0] == 0xE0 && (0xA0 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) ) || (// straight 3-byte ((0xE1 <= bytes[0] && bytes[0] <= 0xEC) || bytes[0] == 0xEE || bytes[0] == 0xEF) && (0x80 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) ) || (// excluding surrogates bytes[0] == 0xED && (0x80 <= bytes[1] && bytes[1] <= 0x9F) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) ) ) { bytes += 3; continue; } if( (// planes 1-3 bytes[0] == 0xF0 && (0x90 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF) ) || (// planes 4-15 (0xF1 <= bytes[0] && bytes[0] <= 0xF3) && (0x80 <= bytes[1] && bytes[1] <= 0xBF) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF) ) || (// plane 16 bytes[0] == 0xF4 && (0x80 <= bytes[1] && bytes[1] <= 0x8F) && (0x80 <= bytes[2] && bytes[2] <= 0xBF) && (0x80 <= bytes[3] && bytes[3] <= 0xBF) ) ) { bytes += 4; continue; } return 0; } return 1; } 

Si noti che questa è una traduzione fedele dell'espressione regolare raccomandata da W3C per la convalida del modulo, che in effetti rifiuta alcune sequenze UTF-8 valide (in particolare quelle contenenti caratteri di controllo ASCII).

Inoltre, anche dopo aver risolto ciò apportando la modifica menzionata nel commento, si presuppone ancora la terminazione zero, che impedisce l'incorporamento di caratteri NUL, anche se tecnicamente dovrebbe essere legale.

Quando mi sono dilungato a creare la mia libreria di stringhe, sono andato con UTF-8 modificato (cioè codifica NUL come una sequenza di due byte troppo lunga) - sentitevi liberi di usare questa intestazione come modello per fornire una routine di validazione che non soffre di le carenze di cui sopra.

Questo decodificatore di Bjoern Hoermann è il più semplice che ho trovato. Funziona anche alimentando un singolo byte, oltre a mantenere uno stato. Lo stato è molto utile per analizzare UTF8 in entrata in blocchi sulla rete.

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

 // Copyright (c) 2008-2009 Bjoern Hoehrmann  // 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 semplice validatore / rivelatore non ha bisogno del punto di codice, quindi potrebbe essere scritto in questo modo (lo stato iniziale è impostato su 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; } 

Se il testo è valido utf8 UTF8_ACCEPT viene restituito. Se non è valido UTF8_REJECT . Se sono necessari più dati, viene restituito un altro numero intero.

Esempio di utilizzo con l'alimentazione di dati in blocchi (ad es. Dalla rete):

 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"); } 

Non è ansible rilevare se una determinata stringa (o sequenza di byte) è un testo con codifica UTF-8 come ad esempio ogni serie di ottetti UTF-8 è anche una serie valida (se non sensata) di Latin-1 (o qualche altra codifica) ottetti. Tuttavia, non tutte le serie di ottetti Latin-1 validi sono serie UTF-8 valide. Quindi puoi escludere stringhe che non sono conformi allo schema di codifica 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 

Dovresti analizzare la stringa come UTF-8, vedi http://www.rfc-editor.org/rfc/rfc3629.txt È molto semplice. Se l’analisi fallisce, non è UTF-8. Esistono diverse semplici librerie UTF-8 in grado di farlo.

Potrebbe forse essere semplificato se si sa che la stringa è normale ASCII o contiene caratteri al di fuori ASCII che sono codificati in UTF-8. Nel qual caso spesso non è necessario preoccuparsi della differenza, il progetto di UTF-8 era che i programmi esistenti che potevano gestire ASCII, nella maggior parte dei casi potevano gestire in modo trasparente UTF-8.

Tieni presente che ASCII è codificato in UTF-8 come se stesso, quindi ASCII è UTF-8 valido.

La stringa AC può essere qualsiasi cosa, è il problema che devi risolvere che non sai se il contenuto è ASCII, GB 2312, CP437, UTF-16, o qualsiasi altra dozzina di codifiche di caratteri che renda difficile la vita di un programma. ?

È ansible utilizzare il rilevatore UTF-8 integrato in Firefox . Si trova nel rivelatore universale del charset ed è praticamente un supporto per la libreria C ++. Dovrebbe essere estremamente facile trovare la class che riconosce UTF-8 e prendere solo quella.
Ciò che questa class fa fondamentalmente è rilevare sequenze di caratteri che sono uniche per UTF-8.

  • prendi l’ultimo baule di firefox
  • vai a \ mozilla \ extensions \ universalchardet \
  • trova la class del rivelatore UTF-8 (non ricordo bene quale sia il suo nome esatto)

È imansible rilevare che una determinata matrice di byte è una stringa UTF-8. È ansible determinare in modo affidabile che non può essere UTF-8 valido (il che non significa che non sia UTF-8 non valido); e puoi determinare che potrebbe essere una sequenza UTF-8 valida, ma questa è soggetta a falsi positivi.

Per un semplice esempio, utilizzare un generatore di numeri casuali per generare una matrice di 3 byte casuali e utilizzarlo per testare il codice. Questi sono byte casuali e quindi non UTF-8, quindi ogni stringa che il tuo codice pensa sia “possibilmente UTF-8” è un falso positivo. La mia ipotesi è che (in queste condizioni) il tuo codice sarà sbagliato oltre il 12% delle volte.

Una volta riconosciuto che è imansible, puoi iniziare a pensare a restituire un livello di confidenza (oltre alla tua previsione). Ad esempio, la tua funzione potrebbe restituire qualcosa del tipo “Sono sicuro all’88% che sia UTF-8”.

Ora fai questo per tutti gli altri tipi di dati. Ad esempio, potresti avere una funzione che controlla se i dati sono UTF-16 che potrebbero restituire “Sono sicuro al 95% che si tratta di UTF-16”, e quindi decidere che (poiché il 95% è superiore all’88%) è più probabile che i dati fossero UTF-16 e non UTF-8.

Il prossimo passo è aggiungere trucchi per aumentare i livelli di confidenza. Ad esempio, se la stringa sembra contenere principalmente gruppi di sillabe valide separate da uno spazio bianco, è ansible essere molto più sicuri del fatto che sia effettivamente UTF-8. Allo stesso modo, se i dati potrebbero essere HTML, è ansible verificare la presenza di elementi che potrebbero essere un markup HTML valido e utilizzarli per aumentare la sicurezza.

Ovviamente lo stesso vale per altri tipi di dati. Ad esempio, se i dati hanno un’intestazione PE32 o ELF valida o un’intestazione BMP o JPG o MP3 corretta, puoi essere molto più sicuro che non sia UTF-8.

Un approccio molto migliore è quello di risolvere la causa reale del problema. Ad esempio, potrebbe essere ansible aggiungere una sorta di identificatore di “tipo documento” all’inizio di tutti i file che ti interessano, o forse dire “questo software presuppone UTF-8 e non supporta altro”; in modo che non sia necessario fare ipotesi azzardate al primo posto.

3 byte casuali sembrano avere una probabilità del 15,8% di essere UTF-8 validi secondo il mio calcolo:

128 ^ 3 possibili sequenze solo ASCII = 2097152

2 ^ 16-2 ^ 11 possibili caratteri UTF-8 a 3 byte (si suppone che siano consentite coppie surrogate e caratteri non consentiti) = 63488

1920 Caratteri UTF-8 a 2 byte prima o dopo un carattere ASCII = 1920 * 128 * 2 = 524288

Dividere per numero di sequenze a 3 byte = (2097152 + 63488 + 491520) /16777216.0 = 0,1580810546875

IMHO questo è molto sopravvalutando il numero di corrispondenze errate, perché il file è lungo solo 3 byte. L’intersezione scende al crescere del numero di byte. Anche il testo effettivo in non UTF-8 non è casuale, c’è un gran numero di byte solitari con il bit alto impostato, che non è UTF-8 valido.

Una metrica più utile per indovinare le probabilità di fallimento è la probabilità che una sequenza di byte con il set di bit elevato sia UTF-8 valida. Ottengo questi valori:

 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) 

È anche utile provare a trovare una stringa reale leggibile (in qualsiasi lingua e qualsiasi codifica) che sia anche una stringa UTF-8 valida. Questo è molto molto difficile, indicando che questo non è un problema con i dati reali.

Fondamentalmente controllo se la chiave data (una stringa di massimo 4 caratteri) corrisponde al formato da questo link: 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)); } 

Funziona bene per me:

 unsigned char buf[5]; ft_to_utf8(L'歓', buf); printf("%d\n", char_is_utf8(buf)); // => 1 

So che è un thread vecchio, ma ho pensato di pubblicare la mia soluzione qui poiché penso che sia un miglioramento rispetto alla meravigliosa soluzione di @Christoph (che ho upvoted).

Non sono esperto, quindi potrei aver letto la RFC errata, ma a me sembra ansible utilizzare una mappa a 32 byte invece di una mappa a 256 byte, risparmiando sia la memoria che il tempo.

Questo mi ha portato a una semplice macro che avanza di un puntatore a stringa di un carattere UTF-8, memorizzando il punto di codice UTF8 in un intero con segno a 32 bit e memorizzando il valore -1 in caso di errore.

Ecco il codice con alcuni commenti.

 #include  /** * 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; } 

Il programma seguente legge le stringhe utf-8 (ascii, caratteri non ascii come euro ecc …) da stdin. Ogni riga è passata a func_find_utf8. Poiché i caratteri utf-8 sono caratteri a byte multipli, la funzione func_find_utf8 controlla i bit di caricamento per trovare il carattere whetehr sia ascii o non-ascii. Se il charcter è non-ascii, conosci la larghezza dei byte. Passa la larghezza dei byte e posizionalo in modo che funzioni print_non_ascii.

 #include #include /* UTF-8 : BYTE_BITS*/ /* B0_BYTE : 0XXXXXXX */ /* B1_BYTE : 10XXXXXX */ /* B2_BYTE : 110XXXXX */ /* B3_BYTE : 1110XXXX */ /* B4_BYTE : 11110XXX */ /* B5_BYTE : 111110XX */ /* B6_BYTE : 1111110X */ #define B0_BYTE 0x00 #define B1_BYTE 0x80 #define B2_BYTE 0xC0 #define B3_BYTE 0xE0 #define B4_BYTE 0xF0 #define B5_BYTE 0xF8 #define B6_BYTE 0xFC #define B7_BYTE 0xFE /* Please tune this as per number of lines input */ #define MAX_UTF8_STR 10 /* 600 is used because 6byteX100chars */ #define MAX_UTF8_CHR 600 void func_find_utf8 (char *ptr_to_str); void print_non_ascii (int bytes, char *pbyte); char strbuf[MAX_UTF8_STR][MAX_UTF8_CHR]; int main (int ac, char *av[]) { int i = 0; char no_newln_str[MAX_UTF8_CHR]; i = 0; printf ("\n\nYou can enter utf-8 string or Q/q to QUIT\n\n"); while (i < MAX_UTF8_STR) { fgets (strbuf[i], MAX_UTF8_CHR, stdin); if (!strlen (strbuf[i])) break; if ((strbuf[i][0] == 'Q') || (strbuf[i][0] == 'q')) break; strcpy (no_newln_str, strbuf[i]); no_newln_str[strlen (no_newln_str) - 1] = 0; func_find_utf8 (no_newln_str); ++i; } return 1; } void func_find_utf8 (char *ptr_to_str) { int found_non_ascii; char *pbyte; pbyte = ptr_to_str; found_non_ascii = 0; while (*pbyte) { if ((*pbyte & B1_BYTE) == B0_BYTE) { pbyte++; continue; } else { found_non_ascii = 1; if ((*pbyte & B7_BYTE) == B6_BYTE) { print_non_ascii (6, pbyte); pbyte += 6; continue; } if ((*pbyte & B6_BYTE) == B5_BYTE) { print_non_ascii (5, pbyte); pbyte += 5; continue; } if ((*pbyte & B5_BYTE) == B4_BYTE) { print_non_ascii (4, pbyte); pbyte += 4; continue; } if ((*pbyte & B4_BYTE) == B3_BYTE) { print_non_ascii (3, pbyte); pbyte += 3; continue; } if ((*pbyte & B3_BYTE) == B2_BYTE) { print_non_ascii (2, pbyte); pbyte += 2; continue; } } } if (found_non_ascii) printf (" These are Non Ascci chars\n"); } void print_non_ascii (int bytes, char *pbyte) { char store[6]; int i; memset (store, 0, 6); memcpy (store, pbyte, bytes); i = 0; while (i < bytes) printf ("%c", store[i++]); printf ("%c", ' '); fflush (stdout); }