base85-full
Version:
Base85 (Ascii85/btoa/ZMP) encoder and decoder
268 lines (211 loc) • 6.71 kB
JavaScript
;
const { Buffer } = require('buffer');
const alphabets = require('./alphabets');
const Address6 = require('ip-address').Address6;
const NUM_MAXVALUE = Math.pow(2, 32) - 1;
const QUAD85 = 85 * 85 * 85 * 85;
const TRIO85 = 85 * 85 * 85;
const DUO85 = 85 * 85;
const SING85 = 85;
const DEFAULT_ENCODING = 'z85';
const LBigInt = typeof(window) !== 'undefined' ? window.BigInt : global.BigInt;
/* Characters to allow (and ignore) in an encoded buffer */
const IGNORE_CHARS = [
0x09, /* horizontal tab */
0x0a, /* line feed, new line */
0x0b, /* vertical tab */
0x0c, /* form feed, new page */
0x0d, /* carriage return */
0x20 /* space */
];
const ASCII85_ENC_START = '<~';
const ASCII85_ENC_END = '~>';
function bufferToBigInt(buffer) {
return LBigInt('0x'+Buffer.from(buffer).toString('hex'));
}
/* Function borrowed from noseglid/canumb (github) */
function pad(width, number)
{
return new Array(1 + width - number.length).join('0') + number;
}
function encodeBignumIPv6(num)
{
const enctable = alphabets.ipv6.enc;
const enc = [];
for (let i = 1; i < 20; ++i) {
enc.push(enctable[Number(num % 85n)]); /* Ranges between 0 - 84 */
num = num / 85n;
}
enc.push(enctable[Number(num)]); /* What's left is also in range 0 - 84 */
return enc.reverse().join('');
}
function encodeBufferIPv6(buffer)
{
if (16 !== buffer.length) {
/* An IPv6 address must be exactly 16 bytes, 128 bits long */
return false;
}
return encodeBignumIPv6(bufferToBigInt(buffer));
}
function encodeStringIPv6(string)
{
const addr = new Address6(string);
if (!addr.isValid()) {
return false;
}
const hex = addr.parsedAddress.map(function(el) {
return pad(4, el);
}).join('');
const num = LBigInt(`0x${hex}`);
return encodeBignumIPv6(num);
}
function decodeStringIPv6(string)
{
if (20 !== string.length) {
/* An encoded IPv6 is always (5/4) * 16 = 20 bytes */
return false;
}
const dectable = alphabets.ipv6.dec;
let i = 0;
/* bignum throws an exception if invalid data is passed */
try {
const binary = string.split('').reduceRight(function(memo, el) {
const num = LBigInt(dectable[el.charCodeAt(0)]);
const fact = LBigInt(85) ** LBigInt(i++);
const contrib = num * fact;
return memo + (contrib);
}, LBigInt(0));
return Address6.fromBigInteger(binary).correctForm();
} catch(e) {
return false;
}
}
function decodeBufferIPv6(buffer)
{
return decodeStringIPv6(buffer.toString());
}
function encodeBuffer(buffer, encoding)
{
if ('z85' === encoding && buffer.length % 4 !== 0) {
return false;
}
const enctable = alphabets[encoding].enc;
const padding = (buffer.length % 4 === 0) ? 0 : 4 - buffer.length % 4;
let result = '';
for (let i = 0; i < buffer.length; i += 4) {
/* 32 bit number of the current 4 bytes (padded with 0 as necessary) */
let num = ((buffer[i] << 24) >>> 0) + // Shift right to force unsigned number
(((i + 1 > buffer.length ? 0 : buffer[i + 1]) << 16) >>> 0) +
(((i + 2 > buffer.length ? 0 : buffer[i + 2]) << 8) >>> 0) +
(((i + 3 > buffer.length ? 0 : buffer[i + 3]) << 0) >>> 0);
/* Create 5 characters from '!' to 'u' alphabet */
let block = [];
for (let j = 0; j < 5; ++j) {
block.unshift(enctable[num % 85]);
num = Math.floor(num / 85);
}
block = block.join('');
if (block === '!!!!!' && 'ascii85' === encoding) {
block = 'z';
}
/* And append them to the result */
result += block;
}
return (('ascii85' === encoding) ? ASCII85_ENC_START : '') +
result.substring(0, result.length - padding) +
(('ascii85' === encoding) ? ASCII85_ENC_END : '');
}
function encodeString(string, encoding)
{
const buffer = Buffer.from(string, 'utf8'); // utf8 at all times?
return encodeBuffer(buffer, encoding);
}
function decodeBuffer(buffer, encoding)
{
const dectable = alphabets[encoding].dec;
let dataLength = buffer.length;
if ('ascii85' === encoding) {
dataLength -= (ASCII85_ENC_START.length + ASCII85_ENC_END.length);
}
if ('z85' === encoding && dataLength % 5 !== 0) {
return false;
}
let padding = (dataLength % 5 === 0) ? 0 : 5 - dataLength % 5;
const bufferStart = ('ascii85' === encoding) ? ASCII85_ENC_START.length : 0;
const bufferEnd = bufferStart + dataLength;
const result = Buffer.alloc(4 * Math.ceil((bufferEnd - bufferStart) / 5));
const nextValidByte = function(index) {
if (index < bufferEnd) {
while (-1 !== IGNORE_CHARS.indexOf(buffer[index])) {
padding = (padding + 1) % 5;
index++; // skip newline character
}
}
return index;
};
let writeIndex = 0;
for (let i = bufferStart; i < bufferEnd;) {
let num = 0;
const starti = i;
i = nextValidByte(i);
num = (dectable[buffer[i]]) * QUAD85;
i = nextValidByte(i + 1);
num += (i >= bufferEnd ? 84 : dectable[buffer[i]]) * TRIO85;
i = nextValidByte(i + 1);
num += (i >= bufferEnd ? 84 : dectable[buffer[i]]) * DUO85;
i = nextValidByte(i + 1);
num += (i >= bufferEnd ? 84 : dectable[buffer[i]]) * SING85;
i = nextValidByte(i + 1);
num += (i >= bufferEnd ? 84 : dectable[buffer[i]]);
i = nextValidByte(i + 1);
if ('z85' === encoding && starti + 5 !== i) {
return false;
}
if (num > NUM_MAXVALUE || num < 0) {
/* Bogus data */
return false;
}
result.writeUInt32BE(num, writeIndex);
writeIndex += 4;
}
return result.slice(0, writeIndex - padding);
}
function decodeString(string, encoding)
{
if ('ascii85' === encoding) {
string = string.replace('z', '!!!!!');
}
let buffer = Buffer.from(string, 'utf8'); // utf8 at all times?
return decodeBuffer(buffer, encoding);
}
function encode(data, encoding) {
encoding = encoding || DEFAULT_ENCODING;
if (! alphabets.hasOwnProperty(encoding)) {
return false;
}
if (Buffer.isBuffer(data)) {
return ('ipv6' === encoding) ? encodeBufferIPv6(data) : encodeBuffer(data, encoding);
}
if (typeof data === 'string') {
return ('ipv6' === encoding) ? encodeStringIPv6(data) : encodeString(data, encoding);
}
return false;
}
function decode(data, encoding) {
encoding = encoding || DEFAULT_ENCODING;
if (! alphabets.hasOwnProperty(encoding)) {
return false;
}
if (Buffer.isBuffer(data)) {
return ('ipv6' === encoding) ? decodeBufferIPv6(data) : decodeBuffer(data, encoding);
}
if (typeof data === 'string') {
return ('ipv6' === encoding) ? decodeStringIPv6(data) : decodeString(data, encoding);
}
return false;
}
module.exports = {
alphabets,
encode,
decode
};