【问题标题】:Check for valid UTF-8 encoding in C在 C 中检查有效的 UTF-8 编码
【发布时间】:2021-03-19 21:07:27
【问题描述】:

假设您有一个 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 '\0'
  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。我过滤掉了。

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

【问题讨论】:

  • 是否有理由需要将字节加载到整数中?这似乎使问题更难编写和理解。为什么不把字节当作字节来处理呢?
  • @JoelFan 说了什么。此外,您提出的代码没有意义。看起来它希望 caller 已经砍掉了正确的字节数,这只有在调用者已经完成工作的情况下才有可能。但也许这只是给你的奇怪的(非常人为的,最有可能的)问题场景......?
  • @JoelFan:所有 UTF-8 字符都是 1-4 个字节,因此 4 个字节足以确定下一个字节是否构成字符。
  • 您的测试将接受非最小 UTF-8 代码作为有效代码,而实际上它们不是。您的测试将接受 UTF-16 代理代码点为有效且它们也无效。不,您的测试不会确认这些字节不是来自不同的编码,例如 ISO 8859-x 或 UTF-16。
  • 你有 if (0xC080 == c) return 1; // Accept 0xC080 as representation for '\0' — 这是 '\0' 的无效 UTF-8 编码,在应该严格检查有效性的函数中没有位置。

标签: c utf-8


【解决方案1】:

自我反应

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

我会尽力证明:

  1. 接受所有有效的编码(更简单)。
  2. 所有无效编码都被拒绝(更棘手)。

这是供参考的代码:

1:  if (c <= 0x7F) return 1;
2:  if (0xC080 == c) return 1;   // Accept 0xC080 as representation for '\0'
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) 后面没有后续字节

这些值都不在第 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,因此不在任何可接受值的范围内。

【讨论】:

    【解决方案2】:

    UTF-8 在RFC 3629 中定义,等效地在Unicode 标准和ISO 10646 中定义。第一个优点是使用简单的ABNF description 语法来确定哪些字节序列是有效的。您的函数将不得不复制这个,通过使用 32 位整数作为输入没有简单的捷径;显而易见的解决方案是将其分解为字节并对其执行 DFA。有一些优化的向量化实现适用于整个向量,但它们依赖于对向量中的单个字节进行范围检查的能力,这在 32 位算法中并不容易实现。

    【讨论】:

    • 谢谢,关键是我相信我的代码确实完全符合描述中的内容,只需查看整个编码的代码点即可。我可能错了,但我找不到任何我的代码失败的例子,我希望我们的集体智慧能看出我错在哪里。
    • @Remo.D:您的代码没有做任何与此类似的事情。试试 Jonathan Leffler 对测试向量的建议。
    • 例如,0xC2C0 和 0xC341 介于 0xC280 和 0xDFBF 之间,但不是有效的编码。
    • 0xC2C0 将被拒绝,因为 0xC2C0 & 0xE0C0 == 0xC0C0 不是 0xC080(参见测试)。 0xC341 也是如此,因为 0xC341 和 0xE0C0 是 0xC040,而不是 0xC080。所以,你提供的两个例子都被正确拒绝了。
    • 很抱歉,我的意思是我相信我会做与那里相同的检查,而不是“完全一样的事情”。我对糟糕的措辞不利。
    【解决方案3】:

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

    (如果第一个字节以“0”开头,那么它是一个 1 字节的序列,也就是 ASCII)

    【讨论】:

    • 这是有效性的必要条件,但不是充分条件。
    • @R..GitHubSTOPHELPINGICE,我认为这就足够了。如果字符串中的每个序列都遵循这些规则,则它是有效的 UTF-8 字符串。我想您还可以确保没有任何代码点具有 UTF-16 代理项的值,或者不是最小的 UTF-8,如果这就是您的意思的话。但如果你发现这样的字符串,它很可能在 any 标准编码中无效。
    • 不,这还不够。有效性规则很复杂。 C0、C1 和 F5-FF 永远无效。在 E0 之后只有 A0-BF 有效。 ED 之后只有 80-9F 有效。 F0 之后只有 90-BF 有效。 F4后只有80-8F有效。
    • @R..GitHubSTOPHELPINGICE,我在上面的评论中添加了“检查代理”和“检查非最小 UTF-8”。我认为这涵盖了您所有无效字节的示例。它不应该是“复杂的”
    • 这两个条件都不能涵盖F5-FF的无效性。
    【解决方案4】:

    要将 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
    

    它对应的codepoint是\u00e8

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

    【讨论】:

    • 谢谢路易斯。您提出的观点已在我的自我回复的第 2e) 部分中介绍。如果一个代码是非法的,因为它是一个非最小的,它会被拒绝,因为它必然低于相同长度的最小有效编码。上面的代码不是用于解码,而是用于检查编码的有效性。您能否发现我的代码将接受为有效的无效编码(或将拒绝为无效的有效编码)?
    猜你喜欢
    • 2010-10-26
    • 1970-01-01
    • 2011-02-03
    • 2014-11-03
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 1970-01-01
    • 2013-08-16
    相关资源
    最近更新 更多