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



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

例如:

  • 如果您有s="AB"(即{0x41,0x42,0x00}),则c将是0x41
  • 如果您有s="èB"(即{0xC3,0xA8,0x42,0x00}),则c将是0xC3A8

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

我知道我可以一个字节接一个字节地遵循标准指定的FSM,但我需要检查这是否是一种正确的方法。

int chr_isvalid(uint32_t c)
{
if (c <= 0x7F) return 1;
if (0xC080 == c) return 1;   // Accept 0xC080 as representation for ''
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还是任何其他编码。假设字符串是UTF-8编码的
  • 只需查看字节中的起始1就可以提取候选代码点,但这并不相关,因为函数必须适用于c的任何值
  • 任何有效码点(U+000000-U+10FFFF)的编码都适合32位整数
  • 我需要提取的编码形式的c用于其他目的
  • 感谢Jonathan Leffler在下面对UTF-16替代品U+D800-U+DFFF的评论,我现在拒绝了它们
  • 感谢Jonathan Leffler在下面对超长编码的评论,我修复了它,现在应该是正确的。任何2字节超长编码必须小于0xC280,任何3字节超长编码小于0xE0A080,任何4字节超长编码少于0xF0908080。我把它们过滤掉了

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

自我响应

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

我会努力证明:

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

这是供参考的代码:

1:  if (c <= 0x7F) return 1;
2:  if (0xC080 == c) return 1;   // Accept 0xC080 as representation for ''
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+000-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

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

第2行管理修改的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

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

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

U+0010000的正确编码为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

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

2) 所有无效编码都被拒绝

这更棘手。我将按残疾类型对其进行分类。

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

这包括:

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

这些值都不在第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)

当用0xC0屏蔽时,0x80-0xBF之外的任何值都将导致与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在RFC 3629中进行了定义,在Unicode标准和ISO 10646中进行了等效定义。第一种方法的优点是使用简单的ABNF描述哪些字节序列是有效的语法。您的函数将不得不复制这一点,通过使用32位整数作为输入,这没有简单的快捷方式;显而易见的解决方案是将其分解回字节,并对其执行DFA。有一些优化的矢量化实现可以处理整个矢量,但它们取决于对矢量中的单个字节进行范围检查的能力,这在32位算术中不容易实现。

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

(如果第一个字节以前导"0"开头,则它是一个1字节序列,也称为ASCII)

要将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是错误的,因为您首先必须消除为编码而添加的额外位:

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

它对应的代码点是u00e8

我建议您阅读Unicode规范的相应章节,更精确的章节2.4代码点和字符2.5编码形式,最后一节涵盖Unicode的不同编码。

最新更新