From d6fb200fb2dc574788c7cb8a45fa798e9941e918 Mon Sep 17 00:00:00 2001 From: George J Date: Sun, 6 May 2018 14:41:35 +0100 Subject: [PATCH 1/3] Adds Base85 operation to resolve #286 --- src/core/config/Categories.js | 2 + src/core/config/OperationConfig.js | 32 ++++++ src/core/config/modules/Default.js | 3 + src/core/operations/Base85.js | 163 +++++++++++++++++++++++++++++ test/index.js | 1 + test/tests/operations/Base85.js | 89 ++++++++++++++++ 6 files changed, 290 insertions(+) create mode 100644 src/core/operations/Base85.js create mode 100644 test/tests/operations/Base85.js diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 9d531f8d..ab84bc98 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 500a8803..4ca00f48 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -1,6 +1,7 @@ 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"; @@ -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 delimeter
Adds a '<~' and '~>' delimeter 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 delimeter", + type: "boolean", + value: Base85.INCLUDE_DELIMETER + }, + ] + }, + "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 6b9f60f9..9c7c28bc 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 00000000..2be772c6 --- /dev/null +++ b/src/core/operations/Base85.js @@ -0,0 +1,163 @@ +import Utils from "../Utils.js"; + + +/** + * 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_DELIMETER: 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 = ""; + + alphabet = Utils.expandAlphRange(alphabet).join(""); + + 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 (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 { + if (encoding === "Standard") result += "z"; + } + } + + if (args[1] || Base85.INCLUDE_DELIMETER) 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 = []; + + let matches = input.match(/<~(.+?)~>/); + if (matches !== null) input = matches[1]; + + alphabet = Utils.expandAlphRange(alphabet).join(""); + + let i = 0; + let digits, block, blockBytes; + while (i < input.length) { + if (encoding === "Standard" && input[i] === "z") { + result.push(0, 0, 0, 0); + i++; + } else { + 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 b602fe8a..c075eb8a 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 00000000..d03af226 --- /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], + }, + ], + }, +]); From 9cabb1126d0e6bd6ac0267e5da3d7e871df6cd2b Mon Sep 17 00:00:00 2001 From: George J Date: Sun, 6 May 2018 15:42:30 +0100 Subject: [PATCH 2/3] Adds Base85 operation to resolve #286 --- src/core/config/OperationConfig.js | 2 +- src/core/operations/Base85.js | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index 4ca00f48..cd6cd1b8 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -1,8 +1,8 @@ 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 Base85 from "../operations/Base85.js"; import BCD from "../operations/BCD.js"; import BitwiseOp from "../operations/BitwiseOp.js"; import ByteRepr from "../operations/ByteRepr.js"; diff --git a/src/core/operations/Base85.js b/src/core/operations/Base85.js index 2be772c6..d8a4288b 100644 --- a/src/core/operations/Base85.js +++ b/src/core/operations/Base85.js @@ -1,6 +1,3 @@ -import Utils from "../Utils.js"; - - /** * Base85 operations. * @@ -50,7 +47,9 @@ const Base85 = { encoding = Base85._alphabetName(alphabet), result = ""; - alphabet = Utils.expandAlphRange(alphabet).join(""); + 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) { @@ -61,7 +60,7 @@ const Base85 = { ((input[i + 3] || 0)) ) >>> 0; - if (block > 0) { + if (encoding !== "Standard" || block > 0) { let digits = []; for (let j = 0; j < 5; j++) { digits.push(block % 85); @@ -73,10 +72,10 @@ const Base85 = { if (input.length < i + 4) { digits.splice(input.length - (i + 4), 4); } - + result += digits.map(digit => alphabet[digit]).join(""); } else { - if (encoding === "Standard") result += "z"; + result += (encoding === "Standard") ? "z" : null; } } @@ -98,18 +97,21 @@ const Base85 = { 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]; - alphabet = Utils.expandAlphRange(alphabet).join(""); - let i = 0; - let digits, block, blockBytes; + 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("") From 30840f86e9140ad2a4ec3109cbac913e5362c468 Mon Sep 17 00:00:00 2001 From: George J Date: Sun, 6 May 2018 15:59:36 +0100 Subject: [PATCH 3/3] Fixes spelling - Adds Base85 operation to resolve #286 --- src/core/config/OperationConfig.js | 6 +++--- src/core/operations/Base85.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index cd6cd1b8..f6c72b83 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -323,7 +323,7 @@ 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
Alphabet
  • Standard - The standard alphabet, referred to as Ascii85
  • Z85 (ZeroMQ) - A string-safe variant of Base85, which avoids quote marks and backslash characters
  • IPv6 - A variant of Base85 suitable for encoding IPv6 addresses (RFC 1924)
Include delimeter
Adds a '<~' and '~>' delimeter to the start and end of the data. This is standard for Adobe's implementation of Base85.", + 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
Alphabet
  • Standard - The standard alphabet, referred to as Ascii85
  • Z85 (ZeroMQ) - A string-safe variant of Base85, which avoids quote marks and backslash characters
  • IPv6 - A variant of Base85 suitable for encoding IPv6 addresses (RFC 1924)
Include 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: [ @@ -333,9 +333,9 @@ const OperationConfig = { value: Base85.ALPHABET_OPTIONS }, { - name: "Include delimeter", + name: "Include delimiter", type: "boolean", - value: Base85.INCLUDE_DELIMETER + value: Base85.INCLUDE_DELIMITER }, ] }, diff --git a/src/core/operations/Base85.js b/src/core/operations/Base85.js index d8a4288b..ec0445b6 100644 --- a/src/core/operations/Base85.js +++ b/src/core/operations/Base85.js @@ -33,7 +33,7 @@ const Base85 = { * @constant * @default */ - INCLUDE_DELIMETER: false, + INCLUDE_DELIMITER: false, /** * To Base85 operation. @@ -72,14 +72,14 @@ const Base85 = { 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_DELIMETER) result = "<~" + result + "~>"; + if (args[1] || Base85.INCLUDE_DELIMITER) result = "<~" + result + "~>"; return result; },