diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 9d531f8da..ab84bc989 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -44,6 +44,8 @@ const Categories = [ "From Base32", "To Base58", "From Base58", + "To Base85", + "From Base85", "To Base", "From Base", "To BCD", diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 500a88036..f6c72b83e 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -2,6 +2,7 @@ import Arithmetic from "../operations/Arithmetic.js"; import Base from "../operations/Base.js"; import Base58 from "../operations/Base58.js"; import Base64 from "../operations/Base64.js"; +import Base85 from "../operations/Base85.js"; import BCD from "../operations/BCD.js"; import BitwiseOp from "../operations/BitwiseOp.js"; import ByteRepr from "../operations/ByteRepr.js"; @@ -320,6 +321,37 @@ const OperationConfig = { } ] }, + "To Base85": { + module: "Default", + description: "Base85 (similar to Base64) is a notation for encoding arbitrary byte data. It is usually more efficient that Base64.

This operation encodes data in an ASCII string (with an alphabet of your choosing, presets included).

e.g. hello world becomes BOu!rD]j7BEbo7

Base85 is commonly used in Adobe's PostScript and PDF file formats.

Options
AlphabetInclude delimiter
Adds a '<~' and '~>' delimiter to the start and end of the data. This is standard for Adobe's implementation of Base85.", + inputType: "byteArray", + outputType: "string", + args: [ + { + name: "Alphabet", + type: "editableOption", + value: Base85.ALPHABET_OPTIONS + }, + { + name: "Include delimiter", + type: "boolean", + value: Base85.INCLUDE_DELIMITER + }, + ] + }, + "From Base85": { + module: "Default", + description: "Base85 (similar to Base64) is a notation for encoding arbitrary byte data. It is usually more efficient that Base64.

This operation decodes data from an ASCII string (with an alphabet of your choosing, presets included).

e.g. BOu!rD]j7BEbo7 becomes hello world

Base85 is commonly used in Adobe's PostScript and PDF file formats.", + inputType: "string", + outputType: "byteArray", + args: [ + { + name: "Alphabet", + type: "editableOption", + value: Base85.ALPHABET_OPTIONS + }, + ] + }, "Show Base64 offsets": { module: "Default", description: "When a string is within a block of data and the whole block is Base64'd, the string itself could be represented in Base64 in three distinct ways depending on its offset within the block.

This operation shows all possible offsets for a given string so that each possible encoding can be considered.", diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js index 6b9f60f99..9c7c28bce 100644 --- a/src/core/config/modules/Default.js +++ b/src/core/config/modules/Default.js @@ -2,6 +2,7 @@ import FlowControl from "../../FlowControl.js"; import Arithmetic from "../../operations/Arithmetic.js"; import Base from "../../operations/Base.js"; import Base58 from "../../operations/Base58.js"; +import Base85 from "../../operations/Base85.js"; import Base64 from "../../operations/Base64.js"; import BCD from "../../operations/BCD.js"; import BitwiseOp from "../../operations/BitwiseOp.js"; @@ -74,6 +75,8 @@ OpModules.Default = { "From Base32": Base64.runFrom32, "To Base58": Base58.runTo, "From Base58": Base58.runFrom, + "To Base85": Base85.runTo, + "From Base85": Base85.runFrom, "To Base": Base.runTo, "From Base": Base.runFrom, "To BCD": BCD.runToBCD, diff --git a/src/core/operations/Base85.js b/src/core/operations/Base85.js new file mode 100644 index 000000000..ec0445b60 --- /dev/null +++ b/src/core/operations/Base85.js @@ -0,0 +1,165 @@ +/** + * Base85 operations. + * + * @author George J [george@penguingeorge.com] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + * + * @namespace + */ +const Base85 = { + + /** + * @constant + * @default + */ + ALPHABET_OPTIONS: [ + { + name: "Standard", + value: "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstu", + }, + { + name: "Z85 (ZeroMQ)", + value: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#", + }, + { + name: "IPv6", + value: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|~}", + }, + ], + + /** + * Includes a '<~' and '~>' at the beginning and end of the Base85 data. + * @constant + * @default + */ + INCLUDE_DELIMITER: false, + + /** + * To Base85 operation. + * + * @param {byteArray} input + * @param {Object[]} args + * @returns {string} + */ + runTo: function(input, args) { + let alphabet = args[0] || Base85.ALPHABET_OPTIONS[0].value, + encoding = Base85._alphabetName(alphabet), + result = ""; + + if (alphabet.length !== 85 || [].unique.call(alphabet).length !== 85) { + throw ("Alphabet must be of length 85"); + } + + let block; + for (let i = 0; i < input.length; i += 4) { + block = ( + ((input[i]) << 24) + + ((input[i + 1] || 0) << 16) + + ((input[i + 2] || 0) << 8) + + ((input[i + 3] || 0)) + ) >>> 0; + + if (encoding !== "Standard" || block > 0) { + let digits = []; + for (let j = 0; j < 5; j++) { + digits.push(block % 85); + block = Math.floor(block / 85); + } + + digits = digits.reverse(); + + if (input.length < i + 4) { + digits.splice(input.length - (i + 4), 4); + } + + result += digits.map(digit => alphabet[digit]).join(""); + } else { + result += (encoding === "Standard") ? "z" : null; + } + } + + if (args[1] || Base85.INCLUDE_DELIMITER) result = "<~" + result + "~>"; + + return result; + }, + + + /** + * From Base85 operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + runFrom: function(input, args) { + let alphabet = args[0] || Base85.ALPHABET_OPTIONS[0].value, + encoding = Base85._alphabetName(alphabet), + result = []; + + if (alphabet.length !== 85 || [].unique.call(alphabet).length !== 85) { + throw ("Alphabet must be of length 85"); + } + + let matches = input.match(/<~(.+?)~>/); + if (matches !== null) input = matches[1]; + + let i = 0; + let block, blockBytes; + while (i < input.length) { + if (encoding === "Standard" && input[i] === "z") { + result.push(0, 0, 0, 0); + i++; + } else { + let digits = []; + digits = input + .substr(i, 5) + .split("") + .map((chr, idx) => { + let digit = alphabet.indexOf(chr); + if (digit < 0 || digit > 84) { + throw "Invalid character '" + chr + "' at index " + idx; + } + return digit; + }); + + block = + digits[0] * 52200625 + + digits[1] * 614125 + + (i + 2 < input.length ? digits[2] : 84) * 7225 + + (i + 3 < input.length ? digits[3] : 84) * 85 + + (i + 4 < input.length ? digits[4] : 84); + + blockBytes = [ + (block >> 24) & 0xff, + (block >> 16) & 0xff, + (block >> 8) & 0xff, + block & 0xff + ]; + + if (input.length < i + 5) { + blockBytes.splice(input.length - (i + 5), 5); + } + + result.push.apply(result, blockBytes); + i += 5; + } + } + return result; + }, + + /** + * Returns the name of the alphabet, when given the alphabet. + */ + _alphabetName: function(alphabet) { + alphabet = alphabet.replace("'", "'"); + let name; + Base85.ALPHABET_OPTIONS.forEach(function(a) { + if (escape(alphabet) === escape(a.value)) name = a.name; + }); + return name; + } + +}; + +export default Base85; diff --git a/test/index.js b/test/index.js index b602fe8aa..c075eb8a6 100644 --- a/test/index.js +++ b/test/index.js @@ -15,6 +15,7 @@ import "babel-polyfill"; import TestRegister from "./TestRegister.js"; import "./tests/operations/Base58.js"; import "./tests/operations/Base64.js"; +import "./tests/operations/Base85.js"; import "./tests/operations/BCD.js"; import "./tests/operations/BitwiseOp.js"; import "./tests/operations/BSON.js"; diff --git a/test/tests/operations/Base85.js b/test/tests/operations/Base85.js new file mode 100644 index 000000000..d03af2265 --- /dev/null +++ b/test/tests/operations/Base85.js @@ -0,0 +1,89 @@ +/** + * Base85 tests. + * + * @author George J [george@penguingeorge.com] + * + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ +import TestRegister from "../../TestRegister.js"; + +TestRegister.addTests([ + { + name: "To Base85 (Standard): nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "To Base85", + args: [null, false], + }, + ], + }, + { + name: "To Base85 (Standard (Ascii85)): Hello, World!", + input: "Hello, World!", + expectedOutput: "87cURD_*#4DfTZ)+T", + recipeConfig: [ + { + op: "To Base85", + args: [null, false], + }, + ], + }, + { + name: "To Base85 (Standard (Ascii85)): UTF-8", + input: "ნუ პანიკას", + expectedOutput: "iIdZZK;0RJK:_%SOPth^iIdNVK:1\\NOPthc", + recipeConfig: [ + { + op: "To Base85", + args: [null, false], + }, + ], + }, + { + name: "To Base85 (Z85 (ZeroMQ)): Hello, World!", + input: "Hello, World!", + expectedOutput: "nm2QNz.92jz5PV8aP", + recipeConfig: [ + { + op: "To Base85", + args: ["0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#", false], + }, + ], + }, + { + name: "From Base85 (Standard): nothing", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "From Base85", + args: [null, false], + }, + ], + }, + { + name: "From Base85 (Standard): Hello, World!", + input: "87cURD_*#4DfTZ)+T", + expectedOutput: "Hello, World!", + recipeConfig: [ + { + op: "From Base85", + args: [null, false], + }, + ], + }, + { + name: "From Base85 (Standard): UTF-8", + input: "iIdZZK;0RJK:_%SOPth^iIdNVK:1\\NOPthc", + expectedOutput: "ნუ პანიკას", + recipeConfig: [ + { + op: "From Base85", + args: [null, false], + }, + ], + }, +]);