作为一个软件开发人员,在程序编写数据处理过程中,经常会遇到各种编解码和格式转换的需求。 本文讨论的核心内容就是希望能够规划、设计和实现一种编解码程序,可以对信息在各种格式和表示方式之间进行处理和转换。之所以有那么多种格式、编码和表示形式,是因为它们会适用于不同的应用场景,所以开发人员必须要深入理解其原理和形式,并熟悉和掌握相关的编解码和转换方法。
需求和基本构想
既然是一个通用的编码程序,它应当能够处理很多常见的编码和信息格式,它们包括:
- Uint8Array: 无符号整数数组,其实就是字节数组,可以作为所有信息表示的基本格式,这个格式不是给用户使用的,但却是各种密码学处理的内容的基本参数格式,通常需要先转为这个格式
- UTF8: 可支持中文的通用字符集格式的字符串,如JSON内容的编码和字符串化后都是这个格式
- JSON: 基于UTF8编码的结构化信息格式,可以序列化(和UTF8字符串相互转换),通常是业务程序中操作数据的格式
- HEX : 其实是字节数组的Hex字符(带补位)的简单表示形式,本质上是一个字节数组,通常用于表示摘要和签名结果(固定长度)
- BASE64 : 其实是字节数组的限定字符集的表示形式,本质上是一个字节数组,但比HEX精简,通常用于密钥存储、加密的结果、中间传输数据的转换等等
- BASE64URL: 基于BASE64,但进行简单的处理,由 '-_' 符号分别来替换 "+/",并且去除'='号,可以安全的使用在URL地址中(比如不会影响到访问路径的解析);通常用于需要在URL地址中表示数据的场景
基于以上需求,实现这个编码器的基本思路是:
既然所有类型的信息,都可以转换为Uint8Array(简称为uinta,字节数组,在nodejs中类似Buffer),那就可以以它为基础和核心,比如任意两种其他格式,都可以先转为uinta,然后再转为另一种格式。这个思路其实是基于sjcl的codec实现的启发,但sjcl的实现和使用比较繁复,所以我们就稍微简化了一下方便使用。
基于这个思路,所设计的编码器程序模块应该实现三个函数:
- from(data,coding): 这个函数用于将数据转换为uinta,coding表示指明此数据的原始格式是什么,默认为json
- to(data, coding, from): 将uinta数据,转换为其他数据(coding格式),默认也是json;改进后的版本可以选择指定第三个参数from,可以指定来源编码格式,这时候输入就可以不是固定的uinta
- convert(data, from, to): 可以直接进行两种格式间的转换,from是data的原始格式,to是目标格式(这个方法其实是上面两个操作的组合)。
其他的一些考量和约束应当包括:
- JS实现,考虑主要在Nodejs(服务器)和浏览器(客户端)环境中运行
- 不依赖第三方库
- 原理简单清晰,可以方便的移植和扩展
代码实现
基于上述设计思路,我们实现了一个编码器对象,具体如下:
// genaral codec support uint8, utf8(string) json, base64/url, hex
const codec = {
// uint8 array to format
to: (data, encoding = "json", fromcoding =null)=>{
if (fromcoding) { // data is from some other encoding
data = codec.from(data, fromcoding);
};
let r1, result = null;
switch(encoding) {
case "utf8":
result = new TextDecoder().decode(data);
break;
case "json":
result = new TextDecoder().decode(data);
try {
result = JSON.parse(result);
} catch (error) {
}
break;
case "base64": // 将 Uint8Array 转换为字符串
case "base64url":
r1= Array.from(data).reduce((c, v) => c + String.fromCharCode(v), '');
result = btoa(r1);// 将字符串转换为 Base64
if (encoding === "base64url") result = result.replace(/\+/g,"-").replace(/\//g,"_");
break;
case "hex":
result = Array.from(data).map(v=>v.toString(16).padStart(2,"0")).join("");
break;
}
return result;
},
// convert data to uint8 array
from: (data, encoding = "json")=>{
let r1, rArray = null;
switch(encoding) {
case "json" : // to String
data = JSON.stringify(data);
case "utf8" : // encode
rArray = new TextEncoder().encode(data);
break;
case "base64url":
data = data.replace(/\-/g,"+").replace(/\_/g,"/");
case "base64":
r1 = atob(data);
rArray = new Uint8Array(r1.length).map((v,i)=>r1.charCodeAt(i));
break;
case "hex":
rArray = Uint8Array.from(data.match(/.{1,2}/g).map(v => parseInt(v, 16)));
break;
}
return rArray;
},
// convert simplize
convert: (data, from, to)=>{
return codec.to(codec.from(data,from), to);
}
};
简单而言就是先区分不同的编码参数,然后按照不同的参数进行相关的操作。现在这个版本的程序可以处理 json,utf8,base64,hex等几种类型的编码。
这里使用到的相关基本函数包括:
- TextEncoder和TextDecode: 用于编解码UTF字符串
- JSON.stringify和JSON.parse: 用户序列化JSON对象和解析JSON字符串
- Uint8Array:构造字节数组
- atob,btoa: Base64编解码,注意此方法不直接支持utf8,所以需要TextCoder进行处理
- convert其实就是from和to方法的组合使用
应用和使用分析
下面是一些典型的应用场景和代码,便于我们理解和应用:
// 将一个base64格式的key,转换为uinta
let skey = "a4rFNKvjz/AxSd0CYof7UjuQbhn768sDQW4vPL6+Bj8=";
const svkey = codec.from(skey,"base64");
// 导出key为hex
let hexkey = codec.to(svkey,"hex");
// 直接转为hex
hexkey = codec.convert(skey, "base64","hex");
hexkey = codec.to(skey,"hex", "base64");
// 将JSON对象转成base64url,再转回来
let o = { hello: "世界" };
let o64 = codec.convert(o,"json","base64url");
let o2 = codec.convert(o64,"base64url","json");