From 4b235e116675512c3f6c18147f8cf1a668b37dee Mon Sep 17 00:00:00 2001 From: IChenDEV Date: Wed, 5 Aug 2020 23:58:53 +0800 Subject: [PATCH] add base91 support --- src/core/config/Categories.json | 2 + src/core/lib/Base91.mjs | 126 +++++++++++++++++++++++++++++ src/core/operations/FromBase91.mjs | 46 +++++++++++ src/core/operations/ToBase91.mjs | 46 +++++++++++ tests/operations/tests/Base91.mjs | 22 +++++ 5 files changed, 242 insertions(+) create mode 100644 src/core/lib/Base91.mjs create mode 100644 src/core/operations/FromBase91.mjs create mode 100644 src/core/operations/ToBase91.mjs create mode 100644 tests/operations/tests/Base91.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 77e3d319..6bef49d4 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -29,6 +29,8 @@ "From Base62", "To Base85", "From Base85", + "To Base91", + "From Base91", "To Base", "From Base", "To BCD", diff --git a/src/core/lib/Base91.mjs b/src/core/lib/Base91.mjs new file mode 100644 index 00000000..3ee4c13a --- /dev/null +++ b/src/core/lib/Base91.mjs @@ -0,0 +1,126 @@ +/** + * Base91 resources. + * + * @author idevlab [idevlab@outlook.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Base91 alphabet options. + */ +export const ALPHABET_OPTIONS = [ + { + name: "Standard", + value: "A-Za-z0-9!#$%&()*+,./:;<=>?@[]^_`{|}~\"", + } +]; + +/** + * Base91's the input byte array using the given alphabet, returning a string. + * + * @param {byteArray|Uint8Array|ArrayBuffer|string} data + * @param {string} alphabet + * @returns {string} + * + */ +export function toBase91(data, alphabet) { + if (!data) return ""; + if (data instanceof ArrayBuffer) { + data = new Uint8Array(data); + } + if (typeof data == "string") { + data = Utils.strToByteArray(data); + } + + alphabet = Utils.expandAlphRange(alphabet).join(""); + if (alphabet.length !== 91 && alphabet.length !== 92) { // Allow for padding + throw new OperationError(`Invalid Base91 alphabet length (${alphabet.length}): ${alphabet}`); + } + + if (data == null) { + throw new OperationError("base91: Missing data to encode."); + } + const raw = Buffer.isBuffer(data) ? data : typeof data === "number" ? Buffer.from(data.toString()) : Buffer.from(data); + let ret = ""; + + let n = 0; + let b = 0; + + for (let i = 0; i < raw.length; i++) { + b |= raw[i] << n; + n += 8; + + if (n > 13) { + let v = b & 8191; + if (v > 88) { + b >>= 13; + n -= 13; + } else { + v = b & 16383; + b >>= 14; + n -= 14; + } + ret += alphabet[v % 91] + alphabet[v / 91 | 0]; + } + } + + if (n) { + ret += alphabet[b % 91]; + if (n > 7 || b > 90) ret += alphabet[b / 91 | 0]; + } + return ret; +} + + +/** + * UnBase91's the input string using the given alphabet, returning a byte array. + * + * @param {string} data + * @param {string} alphabet + * @param {string} [returnType="string"] - Either "string" or "byteArray" + * @returns {byteArray} + */ +export function fromBase91(data, alphabet, returnType) { + + if (!data) { + return returnType === "string" ? "" : []; + } + + alphabet = alphabet || "A-Za-z0-9+/="; + alphabet = Utils.expandAlphRange(alphabet).join(""); + if (alphabet.length !== 91 && alphabet.length !== 92) // Allow for padding + throw new OperationError(`Invalid Base91 alphabet length (${alphabet.length}): ${alphabet}`); + + const raw = "" + (data || ""); + + let b = 0; + let n = 0; + let v = -1; + const output = []; + for (let i = 0; i < raw.length; i++) { + const p = alphabet.indexOf(raw[i]); + if (p === -1) continue; + if (v < 0) { + v = p; + } else { + v += p * 91; + b |= v << n; + n += (v & 8191) > 88 ? 13 : 14; + do { + output.push(b & 0xff); + b >>= 8; + n -= 8; + } while (n > 7); + v = -1; + } + } + + if (v > -1) + output.push((b | v << n) & 0xff); + + return returnType === "string" ? Utils.byteArrayToUtf8(output) : output; +} diff --git a/src/core/operations/FromBase91.mjs b/src/core/operations/FromBase91.mjs new file mode 100644 index 00000000..fe83c696 --- /dev/null +++ b/src/core/operations/FromBase91.mjs @@ -0,0 +1,46 @@ +/** + * @author idevlab [idevlab@outlook.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { ALPHABET_OPTIONS, fromBase91 } from "../lib/Base91.mjs"; +/** + * From Base91 operation + */ +class FromBase91 extends Operation { + + /** + * FromBase91 constructor + */ + constructor() { + super(); + + this.name = "From Base91"; + this.module = "Default"; + this.description = "BasE91 is an encoding method that uses ASCII characters. Similar to base64, it has the advantage of limiting the size of the encoded data and to be used as a cipher."; + this.infoURL = "http://base91.sourceforge.net/"; + this.inputType = "string"; + this.outputType = "byteArray"; + this.args = [ + { + name: "Alphabet", + type: "editableOption", + value: ALPHABET_OPTIONS + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [alphabet] = args; + return fromBase91(input, alphabet, "byteArray"); + } +} + +export default FromBase91; diff --git a/src/core/operations/ToBase91.mjs b/src/core/operations/ToBase91.mjs new file mode 100644 index 00000000..162093af --- /dev/null +++ b/src/core/operations/ToBase91.mjs @@ -0,0 +1,46 @@ +/** + * @author idevlab [idevlab@outlook.com] + * @copyright Crown Copyright 2020 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { ALPHABET_OPTIONS, toBase91 } from "../lib/Base91.mjs"; +/** + * To Base91 operation + */ +class ToBase91 extends Operation { + + /** + * ToBase91 constructor + */ + constructor() { + super(); + + this.name = "To Base91"; + this.module = "Default"; + this.description = "BasE91 is an encoding method that uses ASCII characters. Similar to base64, it has the advantage of limiting the size of the encoded data and to be used as a cipher."; + this.infoURL = "http://base91.sourceforge.net/"; + this.inputType = "ArrayBuffer"; + this.outputType = "string"; + this.args = [ + { + name: "Alphabet", + type: "editableOption", + value: ALPHABET_OPTIONS + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const alphabet = args[0]; + return toBase91(input, alphabet); + } +} + +export default ToBase91; diff --git a/tests/operations/tests/Base91.mjs b/tests/operations/tests/Base91.mjs new file mode 100644 index 00000000..e52c5391 --- /dev/null +++ b/tests/operations/tests/Base91.mjs @@ -0,0 +1,22 @@ +/** + * Base64 tests. + * + * @author n1474335 [n1474335@gmail.com] + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "From Base91: First", + input: "lM_1Z