LoginSignup
32
34

More than 5 years have passed since last update.

UTF-8 の後続バイトの範囲チェックは 0x80 から 0xBF までだけでは不十分

Last updated at Posted at 2015-01-18

stackoverflow の質問と回答 (たとえばこちら) を読むと、後続バイトの範囲を 0x80 から 0xBF までだけであるととして、次のようなビット演算を使うコードがたくさん見られます。

#include <stdio.h>

#define utf8_trail(c) (((c) & 0xC0) == 0x80)

int main(void) {

    for (int i = 0; i < 0x100; ++i) {

        if (!utf8_trail(i)) {
            printf("%X は不正な後続バイトです。\n", i);
        }

    }

    return 0;
}

UTF-8 の仕様では先行バイトに応じて2番目のバイトの値の範囲が変わることがあるので、上記のコードでは不完全です。

不正なバイト列を示します。先行バイトが 0xE0 の場合、2番目のバイトの範囲が 0xA0 から 0xBF になりますので、0x80 から 0xBF までの範囲のバイトは不正なバイトになります。

#include <stdio.h>

#define utf8_trail(c) (((c) & 0xC0) == 0x80)

int main(void) {

    // 2番目のバイトは不正です。
    unsigned char str[] = {0xE0, 0x80, 0xBF, 0x0};

    if (utf8_trail(str[1])) {
        printf("%X は不正なバイトにも関わらず見逃されました。\n", str[1]);
    }

    return 0;
}

UTF-8 を扱う文字列関数を自分で定義するとき、バイトの値の範囲表を仕様のドキュメントで確認しましょう。
Unicode Standard 7.0 の3章に次のような表が記載されています (Table 3-7. Well-Formed UTF-8 Byte Sequences)。

      コードポイント  1番目のバイト 2番目のバイト 3番目のバイト 4番目のバイト
  U+0000 -   U+007F   00 - 7F
  U+0080 -   U+07FF   C2 - DF    80 - BF
  U+0800 -   U+0FFF   E0         A0 - BF     80 - BF
  U+1000 -   U+CFFF   E1 - EC    80 - BF     80 - BF
  U+D000 -   U+D7FF   ED         80 - 9F     80 - BF
  U+E000 -   U+FFFF   EE - EF    80 - BF     80 - BF
 U+10000 -  U+3FFFF   F0         90 - BF     80 - BF    80 - BF
 U+40000 -  U+FFFFF   F1 - F3    80 - BF     80 - BF    80 - BF
U+100000 - U+10FFFF   F4         80 - 8F     80 - BF    80 - BF

RFC 3629 では次のように定義されています。

UTF8-octets = *( UTF8-char )
UTF8-char   = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1      = %x00-7F
UTF8-2      = %xC2-DF UTF8-tail
UTF8-3      = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / %xF4 %x80-8F 2( UTF8-tail )
UTF8-tail   = %x80-BF

範囲表をもとに妥当な3バイト文字であるかを判定するコードを書いてみると次のようになります。PCRE の pcre_valid_utf8.c を参考にさせていただきました (mpyw さん情報提供ありがとうございました)。3バイト文字以外を含む完全な実装はこちらの記事をご参照ください。

#include <stdio.h>

#define utf8_trail(c) (((c) & 0xC0) == 0x80)
int utf8mb3_is_valid(const char*);

int main(void)
{
    // 2番目が不正なバイト
    char str[] = {0xE0, 0x80, 0xBF, 0x0};
    // 妥当な3バイト文字
    char str2[] = {0xE0, 0xA0, 0xBF, 0x0};

    if (!utf8mb3_is_valid(str)) {
        puts("str は不正なバイト列です。");
    }

    if (utf8mb3_is_valid(str2)) {
        puts("str2 は妥当な3バイト文字です。");
    }

    return 0;
}

int utf8mb3_is_valid(const char *str)
{
    unsigned char lead = *str;
    unsigned char trail = *(str + 1);
    unsigned char ab = 2; /* Number of additional bytes */

    if (0xE0 > lead || lead > 0xED) {
        return 0;
    }

    if (lead == 0xE0 && 0xA0 > trail) {
        return 0;
    }

    if (lead == 0xED && trail > 0x9F) {
        return 0;
    }

    do {

        trail = *(++str);
        if (!utf8_trail(trail)) {
            return 0;
        }

    } while (--ab);

    return 1;
}

utf8mb3_is_valid の引数を const unsigned char にすれば、leadtrail を宣言しなくてすみますが、標準関数との互換性を考慮して、const char* にしました (STR04-C)。

文字列関数を定義する場合、不正なバイト列であっても、一部の有効な範囲をもとにバイト数を数える必要があるので、先行バイトが 0xE0 と 0xED である場合の範囲チェックは前のほうにしました。

0xE0, 0xA0, 0xBF の場合、先行バイトが正しく、2番目のバイトが正しくないので、バイト数は1バイトになります。そして、次の 0xA00xBF はそれぞれ先行バイトとして正しくないので、文字列関数はこの不正なバイト列は3文字として扱うことになります。

くわしくは Unicode Standard の「Table 3-8. Use of U+FFFD in UTF-8 Conversion」をご参照ください。Ruby の文字列メソッドや PHP の mbstring や htmlspecialchars などが対応しています。

32
34
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
34