关于modified_utf-8的坑与思考
背景
事情缘于同事调试代码中的一段未知bug,java的jni传输数据到unity层,发现在unity层中的emoji表情不能显示,处于对技术的好奇我研究了一下,刚开始以为是一个简单问题,随着深入和对资料的查阅发现这个是java中对于utf8字符处理方式给不熟悉的人挖了个坑,具体可以看:stack overflow的提问,java为了解决2个问题,使用了变种的utf-8:
第一,空字符(null character,U+0000)使用双字节的0xc0 0x80,而不是单字节的0x00。这保证了在已编码字符串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志字符串结尾的。当已编码字符串放到这样的语言中处理,一个嵌入的空字符将把字符串一刀两断;
第二,个不同点是基本多文种平面之外字符的编码的方法。在标准UTF-8中,这些字符使用4字节形式编码,而在改正的UTF-8中,这些字符和UTF-16一样首先表示为代理对(surrogate pairs),然后再像CESU-8那样按照代理对分别编码。这样改正的原因更是微妙:Java中的字符为16位长,因此一些Unicode字符需要两个Java字符来表示。语言的这个性质盖过了Unicode的增补平面的要求。尽管如此,为了要保持良好的向后兼容、要改变也不容易了。这个改正的编码系统保证了一个已编码字符串可以一次编为一个UTF-16码,而不是一次一个Unicode码点。不幸的是,这也意味着UTF-8中需要4字节的字符在变种UTF-8中变成需要6字节;
经过和同事的讨论和思考,其实有很多解决方案可以规避这个问题,但是我在想是否将变种的utf-8转换为标准的utf-8或者只针对emoji的utf-8处理为原始的utf-8,这样就将真正解决这个问题,于是开始上手解决。
使用php将变种utf-8编码变化为原始utf-8编码
从java获取的变种utf-8对应的数据是:0xED, 0xA0, 0xBE, 0xED, 0xB4, 0x93,这是6个字节,其中对于为什么变种utf-8会变成这种可以参考:这篇博客,在网上找了一圈,发现一些碎片代码,于是组合起来用php将emoji表情的变种utf-8数据还原utf-8,代码如下:
function VerifyValidUtf8($s)
{
$s = preg_replace_callback('@(?:\xED[\xA0-\xBF][\x80-\xBF]){2}@', function ($m)
{
$bytes = unpack("C*", $m[0]); # always 6 bytes
# create UCS-4 character from CESU-8 encoded surrogate pair in $bytes
# 3 bytes CESU-8 to UNICODE high surrogate:
$high = (($bytes[1] & 0x0F) << 12) + (($bytes[2] & 0x3F) << 6) + ($bytes[3] & 0x3F);
# 3 bytes CESU-8 to UNICODE low surrogate:
$low = (($bytes[4] & 0x0F) << 12) + (($bytes[5] & 0x3F) << 6) + ($bytes[6] & 0x3F);
$codepoint = ($high & 0x03FF) << 10 | ($low & 0x03FF);
$codepoint += 0x10000;
return mb_convert_encoding(pack("N", $codepoint), "UTF-8", "UTF-32");
}, $s);
# replace unmatched surrogate pairs with U+FFFD REPLACEMENT CHARACTER
return preg_replace('@\xED[\xA0-\xBF][\x80-\xBF]@', "\xEF\xBF\xBD", $s);
}
$str1 = chr(0xED).chr(0xA0).chr(0xBE).chr(0xED).chr(0xB4).chr(0x93);
echo "modified utf-8 : $str1 \n";
echo "orgin utf-8 : ".VerifyValidUtf8($str1)." \n";
打印的还原的utf8的数据:0xF0,0x9F,0xA4,0x93
在mac上输出的结果:
用c++重写
既然php可以实现变种utf-8编码还原为原始utf-8编码,那么其他语言肯定能实现,但是google了一圈没有发现c++类似代码,于是决定重写:
size_t Utf32_to_Utf8(uint32_t src, unsigned char* des)
{
if (src == 0) return 0;
static const char PREFIX[] = { 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
static const uint32_t CODE_UP[] =
{
0x80, // U+00000000 - U+0000007F
0x800, // U+00000080 - U+000007FF
0x10000, // U+00000800 - U+0000FFFF
0x200000, // U+00010000 - U+001FFFFF
0x4000000, // U+00200000 - U+03FFFFFF
0x80000000 // U+04000000 - U+7FFFFFFF
};
size_t i, len = sizeof(CODE_UP) / sizeof(uint32_t);
for(i = 0; i < len; ++i) {
if (src < CODE_UP[i]) break;
}
if (i == len) return 0; // the src is invalid
len = i + 1;
if (des)
{
for(; i > 0; --i)
{
des[i] = static_cast<char>((src & 0x3F) | 0x80);
src >>= 6;
}
des[0] = static_cast<char>(src | PREFIX[len - 1]);
}
return len;
}
int VerifyValidUtf8(unsigned char *in, uint32_t in_len, unsigned char *out, uint32_t &out_len)
{
#define EMOJI_SIZE 6
#define TO_UINT32(d) (uint32_t)(*(d))
if (in_len <= 0 || *in == '\0')
{
return in_len == 0 ? 0 : -1;
}
size_t step = 0;
if (in_len >= EMOJI_SIZE) {
uint32_t offset = 0;
if (
(
(TO_UINT32(in+offset) & 0xED) == 0xED &&
(TO_UINT32(in+offset+1) >= 0xA0 && TO_UINT32(in+offset+1) <= 0xBF) &&
(TO_UINT32(in+offset+2) >= 0xA0 && TO_UINT32(in+offset+2) <= 0xBF)
)
&&
(
(TO_UINT32(in+offset+3) & 0xED) == 0xED &&
(TO_UINT32(in+offset+4) >= 0x80 && TO_UINT32(in+offset+5) <= 0xBF) &&
(TO_UINT32(in+offset+4) >= 0x80 && TO_UINT32(in+offset+5) <= 0xBF)
)
)
{
uint32_t high = ((TO_UINT32(in+offset) & 0x0F) << 12) +
((TO_UINT32(in+offset+1) & 0x3F) << 6) +
(TO_UINT32(in+offset+2) & 0x3F);
uint32_t low = ((TO_UINT32(in+offset+3) & 0x0F) << 12) +
((TO_UINT32(in+offset+4) & 0x3F) << 6) +
(TO_UINT32(in+offset+5) & 0x3F);
uint32_t codepoint = (high & 0x03FF) << 10 | (low & 0x03FF);
codepoint += 0x10000;
step = Utf32_to_Utf8(codepoint, out);
in += EMOJI_SIZE;
in_len -= EMOJI_SIZE;
out += step;
out_len += step;
}
}
// 判断是否有跳跃,如果没有则自动+1
if (step == 0)
{
*out++ = *in++;
in_len--;
out_len++;
}
return VerifyValidUtf8(in, in_len, out, out_len);
}
// 使用方法
int main()
{
unsigned char ch1[] = {0xAB, 0xCD, '\n', 0xED, 0xA0, 0xBE, 0xED, 0xB4, 0x93, 0x00};
unsigned char ch2[128] = {0x00};
uint32_t len = 0; // 一定要初始化为0
VerifyValidUtf8(ch1, sizeof(ch1)/sizeof(ch1[0]), ch2, len);
fprintf(stdout, "modified utf-8 : %s\n", ch1);
fprintf(stdout, "orgin utf-8 : %s, len:%d\n", ch2, len);
return 0;
}
打印的还原的utf8的数据:0xF0,0x9F,0xA4,0x93
在mac上输出的结果:
上述代码大家可以借鉴,不过未经过全面的变种utf8编码的测试,可能存在bug,如果发现,请留言给我!