检查 C 中的有效 UTF-8 编码

Check for valid UTF-8 encoding in C

假设您有一个 UTF-8 编码的字符串 s。您提取似乎是 UTF-8 编码代码点的第一个字节,并将它们放入 32 位整数 c.

例如:

问题是检查 c 是否是有效编码。
我写的函数是下面这个,但我不确定这是否正确(我可能会错过一些边缘情况)。

我知道我可以按照标准指定的 FSM 逐字节进行,但我需要检查这是否是正确的方法。

int chr_isvalid(uint32_t c)
{
  if (c <= 0x7F) return 1;
  if (0xC080 == c) return 1;   // Accept 0xC080 as representation for '[=10=]'
  if (0xC280 <= c && c <= 0xDFBF) return ((c & 0xE0C0) == 0xC080);
  if (0xEDA080 <= c && c <= 0xEDBFBF) return 0; // Reject UTF-16 surrogates
  if (0xE0A080 <= c && c <= 0xEFBFBF) return ((c & 0xF0C0C0) == 0xE08080);
  if (0xF0908080 <= c && c <= 0xF48FBFBF) return ((c & 0xF8C0C0C0) == 0xF0808080);
  return 0;
}

澄清:

为了更好地查明我的错误,请提供此代码拒绝的有效编码代码点或此代码接受为有效的无效编码的示例。

我不确定您的总体要求是什么,但识别有效 UTF-8 代码点的方法是计算第一个字节中前导 1 位的数量,以告诉您序列的长度.然后序列中的每个后续字节都必须以“10”开头。

(如果第一个字节以“0”开头,那么它是一个单字节序列,又名 ASCII)

UTF-8 在语法的 RFC 3629, and equivalently in the Unicode standard and in ISO 10646. The first has the advantage of using a simple ABNF description 中定义了什么字节序列是有效的。您的函数将不得不复制它,通过使用 32 位整数作为输入没有简单的捷径;显而易见的解决方案是将其分解回字节并对它们执行 DFA。有一些针对整个向量的优化向量化实现,但它们依赖于对向量中的各个字节进行范围检查的能力,这不容易用 32 位算法实现。

自我回复

为了说明为什么我认为这是正确的,我将在这里总结一下我的推理。请指出我可能遗漏的任何内容。

我将尝试证明:

  1. 接受所有有效编码(更容易)。
  2. 拒绝所有无效编码(更棘手)。

这是参考代码:

1:  if (c <= 0x7F) return 1;
2:  if (0xC080 == c) return 1;   // Accept 0xC080 as representation for '[=10=]'
3:  if (0xC280 <= c && c <= 0xDFBF) return ((c & 0xE0C0) == 0xC080);
4:  if (0xEDA080 <= c && c <= 0xEDBFBF) return 0; // Reject UTF-16 surrogates
5:  if (0xE0A080 <= c && c <= 0xEFBFBF) return ((c & 0xF0C0C0) == 0xE08080);
6:  if (0xF0908080 <= c && c <= 0xF48FBFBF) return ((c & 0xF8C0C0C0) == 0xF0808080);
7:  return 0;

1) 接受所有有效编码

按编码字节数细分,我将展示有效的编码 接受范围 U+000000 - U+10FFFF。

1a) 1 字节 (U+0000 - U+007F)

第 1 行接受了有效的 ASCII 编码(范围从 0x00 到 0x7F)。

1b) 2 字节 (U+0080 - U+07FF)

U+0080 的正确编码是 0xC280,U+07FF 的正确编码是 0xDFBF 所有中间代码点都在这个范围内 由于 UTF-8 编码属性。
这是在第 3 行检查的。
此范围内的有效编码必须采用 110xxxxx 10xxxxxx 形式,这意味着屏蔽 x 位我们必须具有:

110xxxxx 10xxxxxx  &
11100000 11000000      <-- 0xE0C0
-------- --------
11000000 10000000      <-- 0xC080

因此,第 3 行接受所有有效的 2 字节编码。

第 2 行管理 Modified UTF-8 的特殊情况,将 U+0000 编码为 0xC080 (参见 https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8)。

1c) 3 字节 (U+0800 - U+FFFF)

U+0800 的正确编码是 0xE0A080,U+FFFF 的正确编码是 0xEFBFBF 所有中间代码点都在这个范围内 由于 UTF-8 编码属性。
这是在第 3 行检查的。
此范围内的有效编码必须采用 1110xxxx 10xxxxxx 10xxxxxx 形式,这意味着屏蔽 x 位我们必须具有:

1110xxxx 10xxxxxx 10xxxxxx  &
11110000 11000000 11000000     <-- 0xF0C0C0
-------- -------- --------
11100000 10000000 10000000     <-- 0xE08080

因此,第 5 行接受所有有效的 3 字节编码。

1d) 4 字节 (U+010000 - U+10FFFF)

U+010000 的正确编码是 0xF0908080,U+10FFFF 的正确编码是 0xF48FBFBF 所有中间代码点都在这个范围内 由于 UTF-8 编码属性。
这是在第 3 行检查的。
此范围内的有效编码必须采用 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 形式,这意味着屏蔽 x 位我们必须具有:

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  &
11111000 11000000 11000000 11000000     <-- 0xF8C0C0C0
-------- -------- -------- --------
11110000 10000000 10000000 10000000     <-- 0xF0808080

因此,第 6 行接受所有有效的 4 字节编码。

2) 拒绝所有无效编码

这更棘手。我将按无效类型将它们分解。

2a) 非 ASCII 单字节值 (0x80 - 0xFF)

这包括:

  • 可能存在杂散连续字节 (0x80-0xBF)
  • 无效的起始字节(0xC0-0xC1、0xF5-0xFF)
  • 有效的起始字节 (0xC2-0xF4) 后面没有后续字节

None 个值在第 1-6 行接受的范围内,然后第 7 行将拒绝它们。

2b) 缺少连续字节

2a
中涵盖了完全没有连续字节的情况 如果假定的 3 字节编码丢失了一个,则意味着候选代码点 在 0xE000-0xEFFF 范围内,第 1-6 行中的任何一行都不接受,因此被拒绝。

如果假定的 4 字节编码少了两个,则表示候选代码点 在 0xF000-0xFFFF 范围内,第 1-6 行中的任何一行都不接受,因此被拒绝。

如果一个假定的 4 字节编码丢失了一个,则意味着候选代码点 在 0xF00000-0xFFFFFF 范围内,第 1-6 行中的任何一行都不接受,因此被拒绝。

2c) 无效的“延续”字节

如果连续字节之一超出有效范围 (0x80-0xBF),它将被屏蔽拒绝 第 3,5 和 6 行的操作。

例如,对于 0xC26A(在第 3 行接受的范围内),值 0x6A 无效。 事实上它会被拒绝因为:

11000010 01101010  &   <-- 0xC26A
11100000 11000000      <-- 0xE0C0
-------- --------
11000000 01000000      <-- 0xC040 (expected 0xC080)

类似地,对于 0xE3DE82(在第 5 行接受的范围内),值 0xDE 无效。 事实上它会被拒绝因为:

11100011 11011110 10000010  &  <-- 0xE3DE82
11110000 11000000 11000000     <-- 0xF0C0C0
-------- -------- --------
11100000 11000000 10000000     <-- 0xE0C080 (expected 0xE08080)

0x80-0xBF 之外的任何值在用 0xC0 屏蔽时都会产生不同于 0x80 的值,并且会被拒绝。

2d) UTF-16 代理项

他们的编码被第 4 行明确拒绝。

2e) 超长编码

要创建超长(无效)编码,代码点向左扩展为 0,然后是编码 使用相应的位数。

例如,假设我们要为 'A' (U+41) 创建一个 2 字节编码。
我们认为代码点在 11 位上(下面从最低位到最高位命名为 abcdefhijk)并使用 2字节的编码规则:

 |----------| 11 bits
 kji hgfedcba -> 110kjihg 10fedcba
 000 01000001 -> 11000001 10000001   (U+41 -> 0xC181)

但由于 kh 的位为 0,因此生成的代码将始终低于 0xC280,因此不在任何接受的范围内 第 1-6 行。

再举一个例子,让我们为字母 'è' (U+E8) 构建一个 3 字节编码:

   |--------------| 16 bits
   ponmlkj hgfedcba -> 1110ponm 10lkjihg 10fedcba
   0000000 11101000 -> 11100000 10000011 10101000   (U+E8 -> 0xE083A4)

pl 的位等于 0,因此超出了可接受的范围(它低于 E0A080,最小 3 字节编码)。

换句话说:拒绝任何超长编码,因为它会低于接受的最小编码值 第 1-6 行.

2f) 代码点高于 U+10FFFF

它们的编码将大于 0xF48FBFBF,因此不在任何可接受值的范围内。

要将 utf-8 字节序列解码为代码点,在序列的第一个字节中使用 1 位前缀,后跟 0

0xxxxxxx  <-- code point is 7 bit only, and can be in the range \u0000 to \u007f
110xxxxx  10xxxxxx  <-- code point is 11 bit only, and can be in the range \u0080 to \u007ff
1110xxxx  10xxxxxx 10xxxxxx  <-- code point is 16 bit and can be in the range \u0800 to \uffff
11110xxx  10xxxxxx 10xxxxxx 10xxxxxx <-- codepoint is 21bit and can be in the range \U00010000 to \U0010ffff

认为对一个 utf-8 序列编码超过必要的最小字节数是非法的。所以编码 \u0000 为

11000000 10000000

不合法,应编码为

00000000

0xc3 0xa8 解码为 Unicode 代码点 0xc3a8 是错误的,因为您首先必须消除为编码添加的 extra 位:

   c3       a8
11000011 10101000
   vvvvv   vvvvvv  these are the bits taken to form the code point.
   |||||   ||||||
   00011   101000
   |||||  //////
   ||||| //////
   vvvvvvvvvvv
   00011101000     this is the decoded codepoint:
     0   e   8     0xe8

对应的codepoint是\u00e8.

我建议您阅读 Unicode specification 的相应章节,更准确地说是 2.4 代码点和字符 2.5 编码形式,这最后涵盖了 Unicode 的不同编码。