分享一个自认为不错的 base64 基础库
发布于 5 年前 作者 cnwhy 6376 次浏览 来自 分享

最近项目需要用到 Base64, 想到npm中找个好用的, 很可惜没一个符合我的预期, 于自己捣鼓一个, 感觉还不错所以分享出来;

其实我的需求其实很简单:

  1. 浏览器可用; (利用 Buffer 的方法出局);
  2. 支持字符串 (现有的库都支持, 只有 btoa , atob 只支持 Latin1)
  3. javascript 字符串无损转换 (因为这一点, 现有库全军覆没), 稍后做说明;
  4. 能用上 Tree-shaking , 因为项目一般只用了Base64的一半功能(encodedecode), 我可不想copy代码;

为什么不能做到javascript字符串无损转换

看一个例子:

 var s = '\ud800'; 
 var b64 = Buffer.from(s).toString('base64'); 
 var _s = Buffer.from(b64, 'base64').toString();
 console.log(s == _s); //false

为什么最后是 false?
首先要知道, 字符串编码为base64之前要先转成字节数组, 字节数组再进行 base64 编码, 解码反之, 我们一般都用’utf8’编码字符串; 再看 U+d800 是一个空码, 从 utf16 来看, 是一个4字节字符的一半, 然而javascript是ucs2编码, 所以它在javascript中是一个正常的字符串; Buffer 默认也是以’utf8’编码字符串 , 所以过程是这样 字符串>Unicode>utf8, 字符串 到 Unicode 过和程对于空码, 替换成了一个占位符 ‘�’, 之后的转换过程就跟着错了, 还原回来当然就错了;

反正就认准一点,我一个字符串 出了个门回来就变了 这是不可以的.

怎么解决:

方案1:
直接用ucs2编码; 省去 字符串 到 Unicode 的过程, 自然不会出问题了;

 var s = '\ud800'; 
 var b64 = Buffer.from(s,'ucs2').toString('base64'); 
 var _s = Buffer.from(b64, 'base64').toString('ucs2');
 console.log(s == _s); //true

注意: 上面代码里的 ‘ucs2’ 其实是 ‘utf16le’ 的别名. 但有一个问题, 单字符串中 英文居多时 , 编码后的Base64会比用 “utf8” 的情况长很多;

方案2:
字符串>Unicode>utf8 的过程不把空码替换, 解码也一样, 这样也可以保证字符串的一至性; 我写的这个Base64库用的也是这个方案;

其它

除了上面说的5点, 我还做了一些顺手的功能;

  1. 抽象出 Base64 算法, 支持自定义的 编码表 和 字符串编码方式, 适应更多特殊场景;
  2. 支持字节数组 (既然在都有了 ArrayBuffer/Uint8Array 类型, 为什么不顺便支持一下, 其实比支持字符串更简单吧) , 其实我的项目也没用到;

GitHub: cnwhy/Base64.js
详细的使用方法可参看这篇

18 回复

javascript是ucs2编码是什么意思

@strugglexiang ucs2 可以看成是 utf16 的子集

浏览器不支持 buffer ?

@waitingsong 浏览器是 ArrayBuffer 我说的是 Nodejs的 Buffer 对象 , 它虽然继承自Uint8Array ,但是并不适合在浏览器中实现

其它的解决方案: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 不知道满足 3.javascript 字符串无损转换 不?

@waitingsong Solution #2 – JavaScript's UTF-16 => UTF-8 => base64 这不失为一个很好的解决方案, 不考虑4字节字符的事全部, 我的方案1是 ucs2 > base64 它是 ucs2 > UTF-8 > base64 , 解决英文字符会占两位题, 不过, 当遇到 4 字节安符时, 会看成两个字符进行UTF8编码,所以会占到6字节, 如果不拆转的话 一般是4个字节;

@DerekYeung 他这个方法是利用 Bufferbtoa'; 实测编码"\ud800"` 在node环境中肯定被替换, 在浏览器环境报错了, 没深究;

@cnwhy utf8.ts 转换方法似乎可以用 str2Uint8Array() 这个函数来简化。 浏览器和 Node.js 通用。

@cnwhy utf8.ts isArray 这个没必要还用 polyfill 吧, 对于不支持 Array.isArray() 的浏览器可以忽略了。

反正就认准一点,我一个字符串 出了个门回来就变了 这是不可以的.

我认为所谓完全保持原样转换的思路是不正确的: \ud800 单独这个码点不是个合法的字符(需要和其它码点配套使用)。各个开发语言在处理这种不合法的 UTF-8 码点时一会都会用 EF BF BD 这个备胎来替换,表示为异常值

console.log(Buffer.from('\ud800')) // <Buffer ef bf bd>
console.log(Buffer.from('\ud801')) // <Buffer ef bf bd>
console.log(Buffer.from('\ud802')) // <Buffer ef bf bd>

如果在转换、传输中对于非法字符(码点)不处理为 EF BF BD 而是“原样”传输,那么就可能会产生多字节字符(拼接)攻击漏洞。这是我们所不希望发生的。

对于我来说:

反正就认准一点,首先一切非法的字符统统干掉,不管是外面进来的还是内部产出的。

@waitingsong 如果认定你认定 '\u800'是非法字符 那就别管我瞎叨叨了; 用你喜欢的方式就好.

@cnwhy 那看来我们的应用场景不同。不同需求自然有不同实现。

  • 我的应用这对于 单个\uD800 之类代理码点,或者无对应 Unicode 字符的代理组合码点,统统认为是非法的、无效的。
  • 你的场景需要原样转换、传输。

基于他人成果包装出一个轮子,适用于通用场景:

  • Base64 编码、解码
  • URL-safe base64 编码、解码
  • 支持长青浏览器以及 Node.js
  • cjs, esm, umd 多种格式编译
  • 丰富的测试用例
  • TypeScript 开发,静态类型提高工作效率,减少错误

项目: https://github.com/waitingsong/base64 文档: https://waitingsong.github.io/base64/index.html NPM: https://www.npmjs.com/package/@waiting/base64

欢迎各位食用 (=・ω・=),

最近优化了UTF8转码函数, 效率有所提高 , 但在 node 环境下 和 TestEncode 还是有不小的差距, node下的 TestEncode 是C写的吧. @waitingsong

@cnwhy 应该是C++吧。 我这儿测试 nodejs 通过 Buffer 转换 base64 的方式比 textEncoder 方式性能更高(粗略估计快一倍以上) 。

确实好用 请问 如果自定义base64的编码表, 这样是不是相当于加密了 容易破解么?

@csc860 只是比普通base64好一点, 因为算法是一样的, 只要别人只要拿到编码表 分分钟破解. 但很多系统还是喜欢这样用.

@csc860 广义上说 base64 也算加密算法,只是算法公开。你自定义码表只是些许提高了点破解难度。

回到顶部