“一”夫当关:boost::property_tree 的 i18n Bug
i18n 测试常要选用特殊字符。这些字符之所以特殊,全是拜这些字符的编码所赐,比如简单的“一”字。
问题
在做 i18n 测试时,发现程序无法正常解析含有中文的 XML 宽字节流( std::wistream )。分析后发现问题出在 boost::property_tree 的 read_xml 函数上。它在解析宽字节流时如果其中含有中文字符“一”,函数会抛出异常,提示在“一”字符处 XML 解析出错。下面的单元测试可以重现这个问题:
#define BOOST_TEST_MODULE unit_test_for_i18n_case
#include <boost/test/unit_test.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <sstream>
using namespace std;
using namespace boost;
BOOST_AUTO_TEST_CASE(test_i18n_xml_tag_value)
{
wistringstream in(L"<\u4E00>abc</\u4E00>"); // 一的 Unicode 值
property_tree::wptree pt;
BOOST_REQUIRE_NO_THROW(property_tree::read_xml(in, pt));
BOOST_CHECK(L"abc" == pt.get<wstring>(L"\u4E00"));
}
那么,这个“一”到底有何魔力呢?
原因
我们来看看“一”的 Unicode 编码: L'\\u4E00' ,注意到那个扎眼的 00 了吗, 会不会程序将“一”当成了字符串的 '\\x00' 终结符呢?翻开 Boost 源代码,确实就如猜测的那样, boost::property_tree 在这有一个 bug。
property_tree 中包含一个简单的 XML 解析器,其中用查表法判断特殊字符,范围包括 255 个 ASCII 扩展字符。对值超过 255 的 Unicode 编码,代码错误地将字符 static_cast 成 unsigned char 再查表判断。在 rapidxml.hpp 中:
// Detect attribute name character
struct attribute_name_pred
{
static unsigned char test(Ch ch)
{
return internal::lookup_tables<0>::lookup_attribute_name[static_cast<unsigned char>(ch)];
}
};
如果被测试的是“一”字,那么经过 static_cast , L'\\u4E00' 就变成了 '\\x00' ,一个合法的字符变成了非法的字符串结束符,不经意的类型转换造成了“一”夫当关的现象。
解
直接的解是修正 property_tree 中 rapidxml 的 bug。对 Unicode 做特殊处理,对值超过 255 的 Unicode 把它当成普通的 ASCII 字母 'z'
template<class Ch>
inline size_t get_index(const Ch c)
{
// If not ASCII char, its semantic is same as plain 'z'
if (c > 255)
{
return 'z';
}
return c;
}
再将原来使用 static_cast 进行转换的地方都换成使用上面的 get_index 函数:
// Detect attribute name character
struct attribute_name_pred
{
static unsigned char test(Ch ch)
{
return internal::lookup_tables<0>::lookup_attribute_name[internal::get_index(ch)];
}
};
我已经给 Boost 提了这个 问题 ,并且包含了这个 Patch 。新版本的 Boost 已经采纳,修复了该问题。
如果不能修改 Boost 库,还有个解可以绕过这个问题:不使用宽字符版本的 read_xml ,而是先将数据用 UTF-8 编码成窄字节,再直接使用窄字节版本的 read_xml 。这个解依赖于 UTF-8 与 ASCII 编码兼容这个特性。
启发
- 所有代码都有可能出问题,哪怕质量很高的 Boost 库
- 隐式类型转换不好,显式类型转换同样要小心
- i18n 测试要特别注意编码中含 '\\x00' 的宽字节字符,例如 ĀȀ̀Ѐ一帀开怀笀묀밀 等,测试用例一定要包含这些特殊字符
- UTF-8 编码与 ASCII 兼容,这个特性让程序更容易迁移到 Unicode。也许正因此,wxWidget 3.0 将会使用 UTF-8 来同时支持 Unicode 和窄字符接口,统一原来的宽窄两套 API