octify
Version:
Base8 Encoder
207 lines (161 loc) • 5.39 kB
JavaScript
const deflate = require("pako");
function error(msg) { throw new Error(`[octify] Error: ${msg}`); }
const NUMS = [ "0", "1", "2", "3", "4", "5", "6", "7" ];
const NUMS_VALUES = [
0b00000000,
0b00000001,
0b00000010,
0b00000011,
0b00000100,
0b00000101,
0b00000110,
0b00000111,
];
const TYPES = [ "string", "object", "number", "bigint", "boolean" ];
const TYPES_VALUES = [ NUMS[0], NUMS[1], NUMS[2], NUMS[3], NUMS[4] ];
const COMPRESSION = [ true, false ];
const COMPRESSION_VALUES = [ NUMS[0], NUMS[1] ];
const text_encoder = new TextEncoder();
const text_decoder = new TextDecoder();
function encode_bytes(bytes, type, compression) {
const data = compression ? deflate.deflate(bytes) : bytes;
const encoded = [];
const compression_index = COMPRESSION.indexOf(compression);
const type_index = TYPES.indexOf(type);
encoded.push(COMPRESSION_VALUES[compression_index], TYPES_VALUES[type_index]);
let equals_next = false;
let current_byte, next_byte;
const dataLength = data.length;
for (let i = 0; i < dataLength; i++) {
current_byte = data[i];
next_byte = i < dataLength - 1 ? data[i + 1] : null;
if (!equals_next) {
encoded.push(NUMS[current_byte >> 5]);
} else {
equals_next = false;
}
encoded.push(NUMS[(current_byte >> 2) & 0b00000111]);
equals_next = next_byte !== null &&
next_byte >> 5 === (current_byte & 0b00000011) << 1;
if (equals_next) {
encoded.push(NUMS[((current_byte & 0b00000011) << 1) | 0b00000001]);
} else {
encoded.push(NUMS[(current_byte & 0b00000011) << 1]);
}
}
return encoded.join('');
}
function decode_bytes(str) {
const decoded = [];
const compression = COMPRESSION[COMPRESSION_VALUES.indexOf(str[0])];
const type = TYPES[TYPES_VALUES.indexOf(str[1])];
let equals_next = false;
let byte;
let i = 2;
const str_length = str.length;
const num_values = NUMS_VALUES;
while (i < str_length) {
if (equals_next) {
byte = (num_values[str[i - 1]] & 0b00000110) << 5 |
(num_values[str[i++]] << 2) | (num_values[str[i++]] >> 1);
} else {
byte = (num_values[str[i++]] << 5) | (num_values[str[i++]] << 2) |
(num_values[str[i++]] >> 1);
}
decoded.push(byte);
equals_next = !!(num_values[str[i - 1]] & 0b00000001);
}
return {
bytes : new Uint8Array(compression ? deflate.inflate(decoded) : decoded),
type
};
}
function encode_str(str) { return text_encoder.encode(str); }
function decode_str(bytes) { return text_decoder.decode(bytes); }
function encode_obj(obj) { return encode_str(JSON.stringify(obj)); }
function decode_obj(bytes) { return JSON.parse(decode_str(bytes)); }
function encode_num(num) { return encode_str(num.toString()); }
function decode_num(bytes) { return Number(decode_str(bytes)); }
function encode_bigint(num) { return encode_num(num); }
function decode_bigint(bytes) { return BigInt(decode_str(bytes)); }
function encode_bool(bool) { return new Uint8Array([ bool ? 1 : 0 ]); }
function decode_bool(bytes) { return !!bytes[0]; }
/**
* Encodes a string or object into a custom compact string format.
* Optionally compresses the data using deflate.
*
* @param {string|object|number|boolean} data - The data to encode.
* @param {boolean} [compression=true] - Whether to apply compression.
* @returns {string} - The encoded string.
*
* @example
* // Encoding an object with compression (default)
* const data = { name: "John", age: 30 };
* const encodedData = encode(data);
*
* Note: Compressing small-sized data might result in a larger output size than
* when uncompressed.
*
* @example
* // Encoding a string with no compression
* const data = "Hello World";
* const encodedData = encode(data, false);
*/
function encode(data, compression = true) {
if (data == null || data == undefined) {
return data;
}
const type = typeof data;
if (type === "string") {
return encode_bytes(encode_str(data), type, compression);
}
if (type === "object") {
return encode_bytes(encode_obj(data), type, compression);
}
if (type === "number") {
return encode_bytes(encode_num(data), type, false);
}
if (type === "bigint") {
return encode_bytes(encode_bigint(data), type, false);
}
if (type === "boolean") {
return encode_bytes(encode_bool(data), type, false);
}
error(`cannot encode unsupported type '${type}'`);
}
/**
* Decodes a string produced by the encode function back into its original
* value. Handles decompression if the original encoding used it.
*
* @param {string} data - The encoded string.
* @returns {string|object|number|boolean} - The decoded original data.
*/
function decode(data) {
if (data == null || data == undefined) {
return data;
}
if (typeof data !== "string") {
error(`only string data can be decoded. Encode it first using encode()`);
}
const {bytes, type} = decode_bytes(data);
if (type === "string") {
return decode_str(bytes);
}
if (type === "object") {
return decode_obj(bytes);
}
if (type === "number") {
return decode_num(bytes);
}
if (type === "bigint") {
return decode_bigint(bytes);
}
if (type === "boolean") {
return decode_bool(bytes);
}
error(`decoding failed due to unsupported encoded type '${type}'`);
}
module.exports = {
encode,
decode,
};