web-dev-qa-db-ja.com

プレーンCでUTF-8を検出する方法は?

与えられた文字列がUTF-8エンコーディングであることを検出するプレーンな古いCのコードスニペットを探しています。私は正規表現による解決策を知っていますが、さまざまな理由から、この特定のケースではプレーンC以外のものを使用しない方がよいでしょう。

正規表現を使用したソリューションは次のようになります(警告:さまざまなチェックが省略されています):

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

これが(うまくいけばバグのない)実装 this expression のプレーン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;
}

これは、フォーム検証のためにW3Cが推奨する正規表現を忠実に翻訳したものであることに注意してください。これにより、いくつかの有効なUTF-8シーケンス(特にASCII制御文字を含むシーケンス)が拒否されます。

また、コメントで言及されている変更を行ってこれを修正した後でも、技術的には合法であるはずですが、それでもゼロ終了と見なされ、NUL文字の埋め込みが妨げられます。

独自の文字列ライブラリを作成するときに、変更されたUTF-8を使用しました(つまり、NULを長すぎる2バイトシーケンスとしてエンコードしました)。検証を提供するためのテンプレートとして this header を自由に使用してください上記の欠点に悩まされないルーチン。

44
Christoph

Bjoern Hoermannによるこのデコーダーは、私が見つけた中で最も単純です。また、状態を保持するだけでなく、1バイトを供給することでも機能します。この状態は、ネットワーク上でチャンクで受信されるUTF8を解析するのに非常に役立ちます。

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

単純なバリデーター/検出器はコードポイントを必要としないため、次のように記述できます(初期状態は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;
}

テキストが有効な場合、utf8 UTF8_ACCEPTが返されます。無効な場合はUTF8_REJECT。さらにデータが必要な場合は、他の整数が返されます。

チャンクでデータをフィードする(ネットワークからなど)使用例:

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

たとえば、特定の文字列(またはバイトシーケンス)がUTF-8エンコードされたテキストであるかどうかを検出することはできません。また、UTF-8オクテットのすべてのシリーズは、Latin-1(またはその他のエンコーディング)の有効な(無意味な場合)シリーズでもあります。オクテット。ただし、有効なLatin-1オクテットのすべてのシリーズが有効なUTF-8シリーズであるとは限りません。したがって、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

文字列をUTF-8として解析する必要があります。詳細は http://www.rfc-editor.org/rfc/rfc3629.txt を参照してください。解析が失敗した場合、UTF-8ではありません。これを行うことができるいくつかの単純なUTF-8ライブラリがあります。

あなたがknow文字列が単純な古い場合ASCIIまたはUTF = 8でエンコードされたASCII外の文字が含まれています。この場合、違いを気にする必要がない場合が多く、 UTF-8は、ASCIIを処理できる既存のプログラムが、ほとんどの場合透過的にUTF-8を処理できるというものでした。

ASCIIはそのままUTF-8でエンコードされるため、ASCIIは有効なUTF-8です。

AC文字列は何でもかまいません。ASCII、GB 2312、CP437、UTF-16、またはプログラムを困難にする他の数十の文字エンコーディングのどれであるかがわからないという、解決する必要がある問題です。 ?

6
nos

指定されたバイト配列がUTF-8文字列であることを検出することは不可能です。あなたはそれが有効なUTF-8ではないことを確実に判断することができます(これはそれが無効なUTF-8ではないという意味ではありません)。そして、それが有効なUTF-8シーケンスである可能性があると判断できますが、これは誤検知の対象になります。

簡単な例として、乱数ジェネレータを使用して3つのランダムバイトの配列を生成し、それを使用してコードをテストします。これらはランダムなバイトであり、したがってUTF-8ではないため、コードが「おそらくUTF-8」であると考えるすべての文字列は誤検知です。私の推測では、(これらの条件下で)コードは12%の確率で間違っているでしょう。

不可能だとわかったら、(予測に加えて)信頼水準を返すことを考え始めることができます。たとえば、関数は「これはUTF-8であることを88%確信しています」のようなものを返す可能性があります。

これを他のすべてのタイプのデータに対して実行します。たとえば、データがUTF-16であるかどうかをチェックして「これはUTF-16であることが95%確信している」かどうかをチェックし、次に(95%が88%より高いため)決定する関数があるとします。データがUTF-8ではなくUTF-16である可能性が高くなります。

次のステップは、信頼レベルを上げるためのトリックを追加することです。たとえば、文字列に空白で区切られた有効な音節がほとんど含まれている場合は、実際にはUTF-8であることをはるかに確信できます。同様に、データがHTMLである可能性がある場合は、有効なHTMLマークアップであるかどうかを確認し、それを使用して信頼性を高めることができます。

もちろん、同じことが他のタイプのデータにも当てはまります。たとえば、データに有効なPE32またはELFヘッダー、または正しいBMPまたはJPGまたはMP3ヘッダーが含まれている場合、UTF-8ではないことをはるかに確信できます。

はるかに良いアプローチは、問題の実際の原因を修正することです。たとえば、気になるすべてのファイルの先頭に、ある種の「ドキュメントタイプ」識別子を追加したり、「このソフトウェアはUTF-8を想定していて、それ以外はサポートしていない」と言ったりすることができます。そもそも最初から怪しい推測をする必要がないように。

2
Brendan

Firefoxに統合されたUTF-8検出器 を使用できます。それは普遍的な文字セット検出器にあり、C++ライブラリに沿ったものです。がUTF-8を認識するクラスを見つけて、それだけを取ることは非常に簡単なはずです。
このクラスが基本的に行うことは、UTF-8に固有の文字シーケンスを検出することです。

  • 最新のFirefoxトランクを入手する
  • \ mozilla\extensions\universalchardet \に移動します
  • uTF-8検出器クラスを見つけます(正確な名前が何であるかはよく覚えていません)
2
shoosh

私の計算によれば、ランダムな3バイトは有効なUTF-8である可能性が15.8%あるようです:

128 ^ 3の可能なASCIIのみのシーケンス= 2097152

2 ^ 16-2 ^ 11の可能な3バイトUTF-8文字(これはサロゲートペアと非文字が許可されていると想定しています)= 63488

ASCII文字の前または後の1920バイトの2バイトUTF-8文字= 1920 * 128 * 2 = 524288

3バイトシーケンスの数で除算=(2097152 + 63488 + 491520)/16777216.0 = 0.1580810546875

私見これは、ファイルの長さが3バイトしかないため、誤った一致の数を大幅に過大評価しています。バイト数が増加するにつれて、交差ははるかに小さくなります。また、UTF-8以外の実際のテキストはランダムではなく、有効なUTF-8ではない、上位ビットが設定された多数の孤立バイトがあります。

失敗の確率を推測するためのより有用なメトリックは、高ビットセットのバイトシーケンスが有効なUTF-8である可能性の高さです。私はこれらの値を取得します:

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)

また、有効なUTF-8文字列である実際の読み取り可能な文字列(任意の言語および任意のエンコーディング)を検索することも役立ちます。これは非常に困難であり、実際のデータには問題がないことを示しています。

1
user3080602

私はそれが古いスレッドであることを知っていますが、@ Christophの素晴らしいソリューション(私が賛成した)に対する改善だと思うので、ここに私のソリューションを投稿すると思いました。

私はエキスパートではないので、RFCを間違って読んだ可能性がありますが、メモリと時間の両方を節約して、256バイトマップの代わりに32バイトマップを使用できるようです。

これにより、文字列ポインタを1つのUTF-8文字だけ進め、UTF8コードポイントを32ビットの符号付き整数に格納し、エラーが発生した場合は値-1を格納する単純なマクロに私は導いた。

これがいくつかのコメント付きのコードです。

#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

基本的に、指定されたキー(最大4文字の文字列)がこのリンクの形式と一致するかどうかを確認します。 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));
}

私にとってはうまくいきます:

unsigned char   buf[5];

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