为什么在UTF-8中标记连续字节是必要的?

5

我最近在研究UTF-8可变长度编码,发现奇怪的是,UTF-8规定每个连续字节的前两位必须是10。

 Range           |  Encoding
-----------------+-----------------
     0 - 7f      |  0xxxxxx
    80 - 7ff     |  110xxxx 10xxxxxx
   800 - ffff    |  1110xxx 10xxxxxx 10xxxxxx
 10000 - 10ffff  |  11110xx 10xxxxxx 10xxxxxx 10xxxxxx

我在尝试其他可能的可变宽度编码时发现,通过使用以下方案,最多只需要3个字节即可存储所有Unicode字符。如果第一个比特是1,则该字符至少被编码为另一个字节(一直读取直到第一个比特为0)。

 Range           |  Encoding
-----------------+-----------------
     0 - 7f      |  0xxxxxx
    80 - 407f    |  1xxxxxx 0xxxxxxx
  4080 - 20407f  |  1xxxxxx 1xxxxxxx 0xxxxxxx

UTF-8中的连续位真的那么重要吗?第二种编码方式更加高效。


3
UTF-8编码方案允许你从任意代码单元位置恢复预期的解码结果。 - Kerrek SB
即使使用我的编码方式,这也是可能的。从任意位置开始,向后移动直到找到一个结束字节(以0结尾),然后下一个字节就是字符的开始。 - crb233
2
不完全准确:如果你指向一个 0xxxxxx 字节,我无法确定它是一个完整的字符还是多字节序列的损坏结尾。(假设我不能往回走。) - Kerrek SB
3个回答

12

UTF-8编码具备自我验证的功能,向前跳跃快速,并且更容易向后跳跃。

自我验证:由于序列的第一个字节指定了长度,接下来的X个字节必须符合10xxxxxx格式,否则就是无效的序列。当单独看到一个10xxxxxx字节时,这个序列就被立即识别为无效的。
你提出的编码方案没有内置验证功能。

向前跳跃快速:如果需要跳过字符,可以根据第一个字节立即跳过X个字节,而不必检查每个中间字节。

更容易向后跳跃:如果需要向后读取字节,可以通过10xxxxxx立即识别连续字符。然后,您将能够在不必扫描前导字节的情况下向后扫描过去的10xxxxxx字节,找到11xxxxxx的前导字节。

请参阅维基百科上的UTF-8 Invalid sequences and error handling


4
除了易于迭代之外,UTF-8旨在安全地处理基于ASCII(和其他不支持UTF-8的工具)进行的常见操作,例如搜索、连接、替换和转义。
对于互操作性和安全性而言,ASCII兼容性的优势超过了使用额外字节来表示U+0800到U+407F字符的成本。
因此,有一些东亚多字节编码是这样做的,但结果不太好,而UTF-8正是特别试图避免这种情况。
在这个方案中,连续字节与ASCII重叠,许多ASCII字符对于不同的语言和工具具有特殊含义。因此,如果您想要表示 ¢,那么它是0x80,0x27,对于任何在没有支持并了解该数据使用所提议的编码的情况下操作字节字符串的工具,第二个字节看起来像一个"
这为将用户输入组合成控制流的所有内容带来了安全隐患,如查询中的SQL注入、Web页面上的HTML注入、shell脚本中的命令注入等等。
(东亚多字节编码没有这个编码这么糟糕,因为它们没有重用ASCII控制代码作为连续字节。按照建议,使用此编码的文本无法存储在C中以空字符结尾的字符串中。尽管如此,Shift-JIS和其它编码还是导致了许多安全漏洞,我们都很高兴摆脱它们。)

3
使用您提出的方案,如果查看一个字节编码的0xxxxxxx,您无法确定它是单字节单元0x00..0x7F还是多字节单元的最后一个字节。您必须向后扫描并查看前面的字节才能知道(如果这是2个或3个字节代码点的最后一个字节,则必须向后检查两个单位)。如果有一个1xxxxxxx字节,您无法确定它是多字节单元的第一个字节还是中间字节。同样,您需要向后扫描。

相比之下,UTF-8方案允许您对于任何非连续字节都知道有多少个后续字节是代码点的一部分。对于连续字节,您只需向后扫描到起始字节即可。您还会得到错误检查; UTF-8中有许多无效序列,这实际上是一个好处。(字节0xC0、0xC1、0xF5..0xFF不能出现在有效的UTF-8中。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接