Node-webkit v0.10.x Buffer.write的奇怪表现

本来昨天就可以放出XiamiThief v0.6.2 Beta版的, 结果因为Node-webkit v0.10.x的很多坑折腾了很久, 这篇文章讲的是我在使用Node-webkit v0.10.x时遇到的最奇怪的一个坑, 直到写这篇文章的时候我还是没有搞明白为什么会出现这个坑.

一切问题的开始是Node.js没有ID3信息的写入模块, 所以我自己写了一个, 代码中使用到了Buffer.write方法.

我用这个模块挺长时间了, 在Node-webkit v0.8.x和v0.9.x中运行起来非常正常, 直到我把项目的Node-webkit的版本升级到v0.10.x.

在v0.10.x中我的模块出了些问题, 本来应该正常写入的ID3信息变成了一串乱码:

图中的”佐⁐呓剁”本应该是”POP STAR”才对.

经过多次测试, 我发现只有使用纯ASCII码就可以表示的信息会在写入的时候变成乱码.

于是我用16进制编辑器查看了文件ID3的数据.

在ID3数据里, 我们可以发现”POP STAR”字样的内容, 这就是对应ID3帧, 由于ID3使用的是UTF16编码, 正确的内容以十六进制显示时应该是每个字符占2个字节, 而图中却一个字符只占1个字节.

如果输入的内容为”50 00 4F 00 50 00 20 00 53 00 54 00 41 00 52 00″而不是”50 4F 50 20 53 54 41 52″的话, ID3信息就会正常显示了.

对于这种现象, 我轻率的下了判断——在Node-webkit中由于某种变化导致纯ASCII码可表示的字符在使用Buffer.write(str, offset, str.length * 2, ‘ucs2′)输入时会变成ascii码的1字节输入而不是自动扩充为2字节(注: ‘ucs2’是’utf16le’的别名).

之后我在Ndoe.js的Changelog上看到在Node.js v0.10.29中有这样一条修改:

utf8: Prevent Node from sending invalid UTF-8 (Felix Geisendörfer)
NOTE this introduces a breaking change, previously you could construct invalid UTF-8 and invoke an error in a client that was expecting valid UTF-8, now unmatched surrogate pairs are replaced with the unknown UTF-8 character. To restore the old functionality simply have NODEINVALIDUTF8 environment variable set.

加上Node-webkit v0.9.x和v0.8.x都没有使用Node.js v0.10.29及更高的版本, 我觉得自己之前下的判断是正确的, 只要做出对应的修改就能解决这个兼容问题.

我原以为一切都会很自然的得到解决, 直到我在Node-webkit的开发人员工具Console中输入了这段代码:

var str = 'test test', f = new Buffer(str.length * 2) f.write(str, 0, str.length * 2, 'ucs2') console.log(new Buffer(str), f)

我想我看到了什么不该看到的东西:

如果我之前的判断是正确的, 那么图右侧的前9个数据应该与图左侧一致, 而这里却进行了扩充.

也就是说Buffer.write是会对ucs2编码时遇到的ascii字符进行自动扩充的, 我之前下的判断是完全错误的.

文章前面一直在说的ascii字符, 实际上是utf8编码中的ascii字符兼容部分, 所以我才觉得changelog与这个问题有关, 而现在这段代码及输出结果却告诉我, 它们是无关的.

那么就回到了原来的问题, 为什么输入成ucs2编码时没有进行1字节到2字节的自动扩充?

我测试了很久, 不认为自己的ID3模块有问题, 事实证明它在旧版本中的运行是正常的, 新版本到底发生了什么变化, 难道是Node-webkit暴露在控制台的引擎不是Node-webkit v0.10.x所使用的Node.js v0.11.13-pre? 可process.versions中显示的结果是Node.js v0.11.3-pre没错.

可我还是心存侥幸, 我下载了Node.js v0.10.13装在我的虚拟机上, 并运行了一次上面的代码, 结果与控制台输出的一样, 这说明应该是我的代码有什么问题, 可我看来看去, 还是没发现问题.

这之后我进行了更多的测试, 我发现了奇怪, 或者说灵异的一幕, 在我某一次运行ID3模块时, 我在控制台中输入相同的代码进行重现, 结果是输出的Buffer中并不包含00数据, 这说明编码转换时的自动扩充没有正常运行, 这与之前在控制台中运行的结果是相悖的.

反复的测试后, 出现了更加奇怪的现象:

‘GO MY WAY!’与’GO MY WAY!!’, 这两个字符串只有一个’!’的差异, 但为什么在两次的结果不同?

‘GO MY WAY!’输入后的数据是十进制的’71 0 79 0′, 以ucs2编码表示是’GO’, 它是正常的, 预料的结果.

‘GO MY WAY!!’输入后的数据是十进制的’71 79 32 77′, 以ascii码表示是’GO M’, 它是错误的结果, 是整篇文章在表述的这种怪异现象.

是什么造成了这种差异, ‘!’吗? 我测试后证明不是’!’这个字符的问题, 而是’GO MY WAY!!’在我的ID3模块中运行过一次.

难道在ID3模块中运行过一次Buffer.write就会让控制台中运行相同内容的结果产生差异吗? 这解释不通.

我关闭当前的Node-webkit实例, 然后重新打开, 在Console中执行同样的代码, 结果是:

怎么回事? 问题到了这个地步, 我真的很难相信我的眼睛, 为什么在重启一遍Node-webkit后, 结果又恢复原样了? 可运行ID3模块后, 输出的ID3信息仍然显示为乱码, 而且字符从1字节到2字节的自动扩充并没有被正确处理.

我的代码没有一处有对Buffer对象或类进行原型扩充甚至修改, 但为什么在模块运行后, 运行参数中包含的一个字符串会变成不能自动扩充的? 是存在缓存吗? 如果是存在缓存的话, 为什么会出现错误的缓存? 而且按理说这不应该出现缓存才对的…

我真的找不到原因在哪, 我宁愿相信是我犯了什么低级错误, 对于这个问题, 如果有人发现了原因所在, 请务必告诉我.

2014.8.17更新

虽然原因仍旧不明, 但比较轻松的解决方法还是有的, 经过下面的代码处理过的字符串可以在buffer.write中以预期的形式执行:

function strFix(str){ var validator = require('validator'), punycode = require('punycode') if(validator.isAscii(str)){ return punycode.ucs2.encode(punycode.ucs2.decode(str)) }else{ return str } }

validator是一个验证用的模块, 你可以用自己的isAscii函数实现.