Last active
April 27, 2023 16:22
-
-
Save bellbind/871b145110c458e83077a718aef9fa0e to your computer and use it in GitHub Desktop.
[ECMAScript][es6]base64 and base32 encode/decode Uint8Array <=> string
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// base32 elements | |
//RFC4648: why include 2? Z and 2 looks similar than 8 and O | |
const b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
console.assert(b32.length === 32, b32.length); | |
const b32r = new Map(Array.from(b32, (ch, i) => [ch, i])).set("=", 0); | |
//[constants derived from character table size] | |
//cbit = 5 (as 32 == 2 ** 5), ubit = 8 (as byte) | |
//ccount = 8 (= cbit / gcd(cbit, ubit)), ucount = 5 (= ubit / gcd(cbit, ubit)) | |
//cmask = 0x1f (= 2 ** cbit - 1), umask = 0xff (= 2 ** ubit - 1) | |
//const b32pad = [0, 6, 4, 3, 1]; | |
const b32pad = Array.from(Array(5), (_, i) => (8 - i * 8 / 5 | 0) % 8); | |
function b32e5(u1, u2 = 0, u3 = 0, u4 = 0, u5 = 0) { | |
const u40 = u1 * 2 ** 32 + u2 * 2 ** 24 + u3 * 2 ** 16 + u4 * 2 ** 8 + u5; | |
return [b32[u40 / 2 ** 35 & 0x1f], b32[u40 / 2 ** 30 & 0x1f], | |
b32[u40 / 2 ** 25 & 0x1f], b32[u40 / 2 ** 20 & 0x1f], | |
b32[u40 / 2 ** 15 & 0x1f], b32[u40 / 2 ** 10 & 0x1f], | |
b32[u40 / 2 ** 5 & 0x1f], b32[u40 & 0x1f]]; | |
} | |
function b32d8(b1, b2, b3, b4, b5, b6, b7, b8) { | |
const u40 = b32r.get(b1) * 2 ** 35 + b32r.get(b2) * 2 ** 30 + | |
b32r.get(b3) * 2 ** 25 + b32r.get(b4) * 2 ** 20 + | |
b32r.get(b5) * 2 ** 15 + b32r.get(b6) * 2 ** 10 + | |
b32r.get(b7) * 2 ** 5 + b32r.get(b8); | |
return [u40 / 2 ** 32 & 0xff, u40 / 2 ** 24 & 0xff, u40 / 2 ** 16 & 0xff, | |
u40 / 2 ** 8 & 0xff, u40 & 0xff]; | |
} | |
// base32 encode/decode: Uint8Array <=> string | |
function b32e(u8a) { | |
console.assert(u8a instanceof Uint8Array, u8a.constructor); | |
const len = u8a.length, rem = len % 5; | |
const u5s = Array.from(Array((len - rem) / 5), | |
(_, i) => u8a.subarray(i * 5, i * 5 + 5)); | |
const pad = b32pad[rem]; | |
const br = rem === 0 ? [] : b32e5(...u8a.subarray(-rem)).slice(0, 8 - pad); | |
return [].concat(...u5s.map(u5 => b32e5(...u5)), | |
br, ["=".repeat(pad)]).join(""); | |
} | |
function b32d(bs) { | |
const len = bs.length; | |
if (len === 0) return new Uint8Array([]); | |
console.assert(len % 8 === 0, len); | |
const pad = len - bs.indexOf("="), rem = b32pad.indexOf(pad); | |
console.assert(rem >= 0, pad); | |
console.assert(/^[A-Z2-7+\/]*$/.test(bs.slice(0, len - pad)), bs); | |
const u8s = [].concat(...bs.match(/.{8}/g).map(b8 => b32d8(...b8))); | |
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - 5) : u8s); | |
} | |
if (typeof require === "function" && require.main === module) { | |
//Check encode <=> decode | |
const buf = require("crypto").randomBytes(32); | |
//NOTE: basic test case | |
//const buf = Buffer.from([0xf0, 0xff, 0x0f, 0xf0, 0xff]); //`6D7Q74H7` | |
//const buf = Buffer.from([0xf0, 0xff, 0x0f, 0xf0]); //`6D7Q74A=` | |
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`6D7Q6===` | |
//const buf = Buffer.from([0x5b, 0xb5]); // `LO2Q====` | |
//const buf = Buffer.from([0xb5]); // `WU======` | |
//const buf = Buffer.alloc(0); | |
const u8a = new Uint8Array(buf); | |
// compare base32 encode | |
console.log(b32e(u8a)); | |
// compare base32 decode | |
console.log(Array.from(b32d(b32e(u8a))).every((u, i) => u === u8a[i])); | |
//console.log(b32d(b32e(u8a))); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// base64 elements | |
const b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
console.assert(b64.length === 64, b64.length); | |
const b64r = new Map(Array.from(b64, (ch, i) => [ch, i])).set("=", 0); | |
//[constants derived from character table size] | |
//cbit = 6 (as 64 == 2 ** 6), ubit = 8 (as byte) | |
//ccount = 3 (= cbit / gcd(cbit, ubit)), ucount = 4 (= ubit / gcd(cbit, ubit)) | |
//cmask = 0x3f (= 2 ** cbit - 1), umask = 0xff (= 2 ** ubit - 1) | |
//const b64pad = [0, 2, 1]; | |
const b64pad = Array.from(Array(3), (_, i) => (4 - i * 8 / 6 | 0) % 4); | |
function b64e3(u1, u2 = 0, u3 = 0) { | |
const u24 = u1 << 16 | u2 << 8 | u3; | |
return [b64[u24 >>> 18], b64[u24 >>> 12 & 0x3f], | |
b64[u24 >>> 6 & 0x3f], b64[u24 & 0x3f]]; | |
} | |
function b64d4(b1, b2, b3, b4) { | |
const u24 = b64r.get(b1) << 18 | b64r.get(b2) << 12 | | |
b64r.get(b3) << 6 | b64r.get(b4); | |
return [u24 >>> 16, u24 >>> 8 & 0xff, u24 & 0xff]; | |
} | |
// base64 encode/decode: Uint8Array <=> string | |
function b64e(u8a) { | |
console.assert(u8a instanceof Uint8Array, u8a.constructor); | |
const len = u8a.length, rem = len % 3; | |
const u3s = Array.from(Array((len - rem) / 3), | |
(_, i) => u8a.subarray(i * 3, i * 3 + 3)); | |
const pad = b64pad[rem]; | |
const br = rem === 0 ? [] : b64e3(...u8a.subarray(-rem)).slice(0, 4 - pad); | |
return [].concat(...u3s.map(u3 => b64e3(...u3)), | |
br, ["=".repeat(pad)]).join(""); | |
} | |
function b64d(bs) { | |
const len = bs.length; | |
if (len === 0) return new Uint8Array([]); | |
//NOTE: allow strict base64 string: no ignore invalids and no supplements | |
console.assert(len % 4 === 0, len); | |
const pad = len - bs.indexOf("="), rem = b64pad.indexOf(pad); | |
console.assert(rem >= 0, pad); | |
console.assert(/^[A-Za-z0-9+\/]*$/.test(bs.slice(0, len - rem)), bs); | |
const u8s = [].concat(...bs.match(/.{4}/g).map(b4 => b64d4(...b4))); | |
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - 3) : u8s); | |
} | |
if (typeof require === "function" && require.main === module) { | |
//Check with comparing nodejs Buffer.toString("base64") | |
const buf = require("crypto").randomBytes(32); | |
//NOTE: basic test case | |
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`8P8P` | |
//const buf = Buffer.from([0x5b, 0xb5]); // `W5U=` | |
//const buf = Buffer.from([0xb5]); // `tQ==` | |
//const buf = Buffer.alloc(0); | |
const u8a = new Uint8Array(buf); | |
// compare base64 encode | |
console.log(b64e(u8a)); | |
console.log(buf.toString("base64")); | |
console.log(b64e(u8a) === buf.toString("base64")); | |
// compare base64 decode | |
console.log(Array.from(b64d(b64e(u8a))).every((u, i) => u === u8a[i])); | |
//console.log(b64d(b64e(u8a))); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// generic BASE 2**N encoder/decoder | |
function BasePow2(ctable, cpad = "=") { | |
const clen = ctable.length, cbit = Math.log2(clen); | |
console.assert(cpad.length === 1, `cpad must be 1 char, but: ${cpad}`); | |
console.assert(!ctable.includes(cpad), | |
`ctable should not include pad char: ${cpad} in ${ctable}`); | |
console.assert(clen > 0 && Number.isInteger(cbit), | |
`length of ctable must be power of 2, but: ${clen}`); | |
const ulen = 256, ubit = 8; | |
const unitbit = lcm(cbit, ubit); | |
console.assert(unitbit < Math.log2(Number.MAX_SAFE_INTEGER), unitbit); | |
const ccount = unitbit / cbit, ucount = unitbit / ubit; | |
const pads = Array.from( | |
Array(ucount), (_, i) => (ccount - i * ubit / cbit | 0) % ccount); | |
const crev = new Map(Array.from(ctable, (ch, i) => [ch, i])).set(cpad, 0); | |
const regBody = new RegExp(`^[${ctable}]*$`); | |
const regPad = new RegExp(`^${cpad}*$`); | |
return {encode, decode}; | |
function lcm(a, b) {return a * b / gcd(a, b);} | |
function gcd(a, b) { | |
if (a > b) a %= b; | |
while (a > 0) [a, b] = [b % a, a]; | |
return b; | |
} | |
function unitEncode(u8s) { | |
//console.assert(u8s.length === ucount, u8s.length, u8s); | |
let unit = u8s.reduce((s, u8 = 0) => s * ulen + u8, 0); | |
const result = []; | |
for (let i = 0; i < ccount; i++) { | |
const part = unit % clen; | |
result.unshift(ctable[part]); | |
unit = (unit - part) / clen; | |
} | |
return result; | |
} | |
function unitDecode(chs) { | |
//console.assert(chs.length === ccount, chs.length, chs); | |
let unit = [...chs].reduce( | |
(s, ch = cpad) => s * clen + crev.get(ch), 0); | |
const result = []; | |
for (let i = 0; i < ucount; i++) { | |
const part = unit % ulen; | |
result.unshift(part); | |
unit = (unit - part) / ulen; | |
} | |
return result; | |
} | |
function encode(u8s) { | |
if (!(u8s instanceof Uint8Array)) throw new TypeError( | |
`args must be a Uint8Array: ${u8s.constructor}`); | |
const len = u8s.length, rem = len % ucount; | |
const u8ss = Array.from( | |
Array((len - rem) / ucount), | |
(_, i) => u8s.subarray(i * ucount, i * ucount + ucount)); | |
const pad = pads[rem]; | |
let br = []; | |
if (rem > 0) { | |
const units = new Uint8Array(ucount); | |
units.set(u8s.subarray(-rem)); | |
br = unitEncode(units).slice(0, ccount - pad); | |
} | |
return [].concat( | |
...u8ss.map(unitEncode), br, [cpad.repeat(pad)]).join(""); | |
} | |
function decode(bs) { | |
if (typeof bs !== "string") throw new TypeError( | |
`args must be a string: ${typeof bs}`); | |
const len = bs.length; | |
if (len === 0) return new Uint8Array([]); | |
if (len % ccount !== 0) throw new TypeError( | |
`length of the arg must me multiple of ${ccount}, but: ${len}`); | |
const pad = len - bs.indexOf(cpad), rem = pads.indexOf(pad); | |
if (rem < 0) throw new TypeError( | |
`valid pad length is ${pads}, but: ${pad}`); | |
if (!regBody.test(bs.slice(0, len - pad))) throw new TypeError( | |
`invalid chars included in: "${bs.slice(0, len - pad)}"`); | |
if (!regPad.test(bs.slice(-pad))) throw new TypeError( | |
`pad must consist only "${cpad}", but "${bs.slice(-pad)}"`); | |
const u8s = [].concat(...Array.from( | |
Array(len / ccount), | |
(_, i) => unitDecode(bs.slice(i * ccount, i * ccount + ccount)))); | |
return new Uint8Array(rem > 0 ? u8s.slice(0, rem - ucount) : u8s); | |
} | |
} | |
if (typeof require === "function" && require.main === module) { | |
const b64 = BasePow2( | |
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); | |
//const b32 = BasePow2("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"); | |
//Check with comparing nodejs Buffer.toString("base64") | |
const buf = require("crypto").randomBytes(32); | |
//NOTE: basic test case | |
//const buf = Buffer.from([0xf0, 0xff, 0x0f]); //`8P8P` | |
//const buf = Buffer.from([0x5b, 0xb5]); // `W5U=` | |
//const buf = Buffer.from([0xb5]); // `tQ==` | |
//const buf = Buffer.alloc(0); | |
const u8a = new Uint8Array(buf); | |
// compare base64 encode | |
console.log(b64.encode(u8a)); | |
console.log(buf.toString("base64")); | |
console.log(b64.encode(u8a) === buf.toString("base64")); | |
// compare base64 decode | |
console.log(Array.from(b64.decode(b64.encode(u8a))).every( | |
(u, i) => u === u8a[i])); | |
//console.log(b64.decode(b64.encode(u8a))); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment