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
にすれば、lead
と trail
を宣言しなくてすみますが、標準関数との互換性を考慮して、const char* にしました (STR04-C)。
文字列関数を定義する場合、不正なバイト列であっても、一部の有効な範囲をもとにバイト数を数える必要があるので、先行バイトが 0xE0
と 0xED
である場合の範囲チェックは前のほうにしました。
0xE0, 0xA0, 0xBF
の場合、先行バイトが正しく、2番目のバイトが正しくないので、バイト数は1バイトになります。そして、次の 0xA0
、0xBF
はそれぞれ先行バイトとして正しくないので、文字列関数はこの不正なバイト列は3文字として扱うことになります。
くわしくは Unicode Standard の「Table 3-8. Use of U+FFFD in UTF-8 Conversion」をご参照ください。Ruby の文字列メソッドや PHP の mbstring や htmlspecialchars
などが対応しています。