From f26d175cad1ddb60a7899ab999e8c41799cf3295 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 16 May 2018 16:25:05 +0000 Subject: [PATCH] ESM: Ported Base58, Base and BCD operations --- src/core/lib/BCD.mjs | 48 ++++++++++ src/core/lib/Base58.mjs | 22 +++++ src/core/operations/FromBCD.mjs | 115 +++++++++++++++++++++++ src/core/operations/FromBase.mjs | 63 +++++++++++++ src/core/operations/FromBase58.mjs | 93 +++++++++++++++++++ src/core/operations/ToBCD.mjs | 141 +++++++++++++++++++++++++++++ src/core/operations/ToBase.mjs | 53 +++++++++++ src/core/operations/ToBase58.mjs | 85 +++++++++++++++++ 8 files changed, 620 insertions(+) create mode 100755 src/core/lib/BCD.mjs create mode 100755 src/core/lib/Base58.mjs create mode 100644 src/core/operations/FromBCD.mjs create mode 100644 src/core/operations/FromBase.mjs create mode 100644 src/core/operations/FromBase58.mjs create mode 100644 src/core/operations/ToBCD.mjs create mode 100644 src/core/operations/ToBase.mjs create mode 100644 src/core/operations/ToBase58.mjs diff --git a/src/core/lib/BCD.mjs b/src/core/lib/BCD.mjs new file mode 100755 index 00000000..623a90c7 --- /dev/null +++ b/src/core/lib/BCD.mjs @@ -0,0 +1,48 @@ +/** + * Binary Code Decimal resources. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +/** + * BCD encoding schemes. + */ +export const ENCODING_SCHEME = [ + "8 4 2 1", + "7 4 2 1", + "4 2 2 1", + "2 4 2 1", + "8 4 -2 -1", + "Excess-3", + "IBM 8 4 2 1", +]; + +/** + * Lookup table for the binary value of each digit representation. + * + * I wrote a very nice algorithm to generate 8 4 2 1 encoding programatically, + * but unfortunately it's much easier (if less elegant) to use lookup tables + * when supporting multiple encoding schemes. + * + * "Practicality beats purity" - PEP 20 + * + * In some schemes it is possible to represent the same value in multiple ways. + * For instance, in 4 2 2 1 encoding, 0100 and 0010 both represent 2. Support + * has not yet been added for this. + */ +export const ENCODING_LOOKUP = { + "8 4 2 1": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "7 4 2 1": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10], + "4 2 2 1": [0, 1, 4, 5, 8, 9, 12, 13, 14, 15], + "2 4 2 1": [0, 1, 2, 3, 4, 11, 12, 13, 14, 15], + "8 4 -2 -1": [0, 7, 6, 5, 4, 11, 10, 9, 8, 15], + "Excess-3": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "IBM 8 4 2 1": [10, 1, 2, 3, 4, 5, 6, 7, 8, 9], +}; + +/** + * BCD formats. + */ +export const FORMAT = ["Nibbles", "Bytes", "Raw"]; diff --git a/src/core/lib/Base58.mjs b/src/core/lib/Base58.mjs new file mode 100755 index 00000000..b4eb4b41 --- /dev/null +++ b/src/core/lib/Base58.mjs @@ -0,0 +1,22 @@ +/** + * Base58 resources. + * + * @author tlwr [toby@toby.codes] + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +/** + * Base58 alphabet options. + */ +export const ALPHABET_OPTIONS = [ + { + name: "Bitcoin", + value: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", + }, + { + name: "Ripple", + value: "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz", + }, +]; diff --git a/src/core/operations/FromBCD.mjs b/src/core/operations/FromBCD.mjs new file mode 100644 index 00000000..078b7519 --- /dev/null +++ b/src/core/operations/FromBCD.mjs @@ -0,0 +1,115 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import Utils from "../Utils"; +import OperationError from "../errors/OperationError"; +import {ENCODING_SCHEME, ENCODING_LOOKUP, FORMAT} from "../lib/BCD"; +import BigNumber from "bignumber.js"; + +/** + * From BCD operation + */ +class FromBCD extends Operation { + + /** + * FromBCD constructor + */ + constructor() { + super(); + + this.name = "From BCD"; + this.module = "Default"; + this.description = "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign."; + this.inputType = "string"; + this.outputType = "BigNumber"; + this.args = [ + { + "name": "Scheme", + "type": "option", + "value": ENCODING_SCHEME + }, + { + "name": "Packed", + "type": "boolean", + "value": true + }, + { + "name": "Signed", + "type": "boolean", + "value": false + }, + { + "name": "Input format", + "type": "option", + "value": FORMAT + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {BigNumber} + */ + run(input, args) { + const encoding = ENCODING_LOOKUP[args[0]], + packed = args[1], + signed = args[2], + inputFormat = args[3], + nibbles = []; + + let output = "", + byteArray; + + // Normalise the input + switch (inputFormat) { + case "Nibbles": + case "Bytes": + input = input.replace(/\s/g, ""); + for (let i = 0; i < input.length; i += 4) { + nibbles.push(parseInt(input.substr(i, 4), 2)); + } + break; + case "Raw": + default: + byteArray = Utils.strToByteArray(input); + byteArray.forEach(b => { + nibbles.push(b >>> 4); + nibbles.push(b & 15); + }); + break; + } + + if (!packed) { + // Discard each high nibble + for (let i = 0; i < nibbles.length; i++) { + nibbles.splice(i, 1); + } + } + + if (signed) { + const sign = nibbles.pop(); + if (sign === 13 || + sign === 11) { + // Negative + output += "-"; + } + } + + nibbles.forEach(n => { + if (isNaN(n)) throw new OperationError("Invalid input"); + const val = encoding.indexOf(n); + if (val < 0) throw new OperationError(`Value ${Utils.bin(n, 4)} is not in the encoding scheme`); + output += val.toString(); + }); + + return new BigNumber(output); + } + +} + +export default FromBCD; diff --git a/src/core/operations/FromBase.mjs b/src/core/operations/FromBase.mjs new file mode 100644 index 00000000..4d4ca9ff --- /dev/null +++ b/src/core/operations/FromBase.mjs @@ -0,0 +1,63 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import BigNumber from "bignumber.js"; +import OperationError from "../errors/OperationError"; + +/** + * From Base operation + */ +class FromBase extends Operation { + + /** + * FromBase constructor + */ + constructor() { + super(); + + this.name = "From Base"; + this.module = "Default"; + this.description = "Converts a number to decimal from a given numerical base."; + this.inputType = "string"; + this.outputType = "BigNumber"; + this.args = [ + { + "name": "Radix", + "type": "number", + "value": 36 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {BigNumber} + */ + run(input, args) { + const radix = args[0]; + if (radix < 2 || radix > 36) { + throw new OperationError("Error: Radix argument must be between 2 and 36"); + } + + const number = input.replace(/\s/g, "").split("."); + let result = new BigNumber(number[0], radix) || 0; + + if (number.length === 1) return result; + + // Fractional part + for (let i = 0; i < number[1].length; i++) { + const digit = new BigNumber(number[1][i], radix); + result += digit.div(Math.pow(radix, i+1)); + } + + return result; + } + +} + +export default FromBase; diff --git a/src/core/operations/FromBase58.mjs b/src/core/operations/FromBase58.mjs new file mode 100644 index 00000000..893a66a8 --- /dev/null +++ b/src/core/operations/FromBase58.mjs @@ -0,0 +1,93 @@ +/** + * @author tlwr [toby@toby.codes] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import Utils from "../Utils"; +import OperationError from "../errors/OperationError"; +import {ALPHABET_OPTIONS} from "../lib/Base58"; + +/** + * From Base58 operation + */ +class FromBase58 extends Operation { + + /** + * FromBase58 constructor + */ + constructor() { + super(); + + this.name = "From Base58"; + this.module = "Default"; + this.description = "Base58 (similar to Base64) is a notation for encoding arbitrary byte data. It differs from Base64 by removing easily misread characters (i.e. l, I, 0 and O) to improve human readability.

This operation decodes data from an ASCII string (with an alphabet of your choosing, presets included) back into its raw form.

e.g. StV1DL6CwTryKyV becomes hello world

Base58 is commonly used in cryptocurrencies (Bitcoin, Ripple, etc)."; + this.inputType = "string"; + this.outputType = "byteArray"; + this.args = [ + { + "name": "Alphabet", + "type": "editableOption", + "value": ALPHABET_OPTIONS + }, + { + "name": "Remove non-alphabet chars", + "type": "boolean", + "value": true + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + let alphabet = args[0] || ALPHABET_OPTIONS[0].value; + const removeNonAlphaChars = args[1] === undefined ? true : args[1], + result = [0]; + + alphabet = Utils.expandAlphRange(alphabet).join(""); + + if (alphabet.length !== 58 || + [].unique.call(alphabet).length !== 58) { + throw new OperationError("Alphabet must be of length 58"); + } + + if (input.length === 0) return []; + + [].forEach.call(input, function(c, charIndex) { + const index = alphabet.indexOf(c); + + if (index === -1) { + if (removeNonAlphaChars) { + return; + } else { + throw new OperationError(`Char '${c}' at position ${charIndex} not in alphabet`); + } + } + + let carry = result[0] * 58 + index; + result[0] = carry & 0xFF; + carry = carry >> 8; + + for (let i = 1; i < result.length; i++) { + carry += result[i] * 58; + result[i] = carry & 0xFF; + carry = carry >> 8; + } + + while (carry > 0) { + result.push(carry & 0xFF); + carry = carry >> 8; + } + }); + + return result.reverse(); + } + +} + +export default FromBase58; diff --git a/src/core/operations/ToBCD.mjs b/src/core/operations/ToBCD.mjs new file mode 100644 index 00000000..43de14b6 --- /dev/null +++ b/src/core/operations/ToBCD.mjs @@ -0,0 +1,141 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import Utils from "../Utils"; +import OperationError from "../errors/OperationError"; +import {ENCODING_SCHEME, ENCODING_LOOKUP, FORMAT} from "../lib/BCD"; +import BigNumber from "bignumber.js"; + +/** + * To BCD operation + */ +class ToBCD extends Operation { + + /** + * ToBCD constructor + */ + constructor() { + super(); + + this.name = "To BCD"; + this.module = "Default"; + this.description = "Binary-Coded Decimal (BCD) is a class of binary encodings of decimal numbers where each decimal digit is represented by a fixed number of bits, usually four or eight. Special bit patterns are sometimes used for a sign"; + this.inputType = "BigNumber"; + this.outputType = "string"; + this.args = [ + { + "name": "Scheme", + "type": "option", + "value": ENCODING_SCHEME + }, + { + "name": "Packed", + "type": "boolean", + "value": true + }, + { + "name": "Signed", + "type": "boolean", + "value": false + }, + { + "name": "Output format", + "type": "option", + "value": FORMAT + } + ]; + } + + /** + * @param {BigNumber} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + if (input.isNaN()) + throw new OperationError("Invalid input"); + if (!input.integerValue(BigNumber.ROUND_DOWN).isEqualTo(input)) + throw new OperationError("Fractional values are not supported by BCD"); + + const encoding = ENCODING_LOOKUP[args[0]], + packed = args[1], + signed = args[2], + outputFormat = args[3]; + + // Split input number up into separate digits + const digits = input.toFixed().split(""); + + if (digits[0] === "-" || digits[0] === "+") { + digits.shift(); + } + + let nibbles = []; + + digits.forEach(d => { + const n = parseInt(d, 10); + nibbles.push(encoding[n]); + }); + + if (signed) { + if (packed && digits.length % 2 === 0) { + // If there are an even number of digits, we add a leading 0 so + // that the sign nibble doesn't sit in its own byte, leading to + // ambiguity around whether the number ends with a 0 or not. + nibbles.unshift(encoding[0]); + } + + nibbles.push(input > 0 ? 12 : 13); + // 12 ("C") for + (credit) + // 13 ("D") for - (debit) + } + + let bytes = []; + + if (packed) { + let encoded = 0, + little = false; + + nibbles.forEach(n => { + encoded ^= little ? n : (n << 4); + if (little) { + bytes.push(encoded); + encoded = 0; + } + little = !little; + }); + + if (little) bytes.push(encoded); + } else { + bytes = nibbles; + + // Add null high nibbles + nibbles = nibbles.map(n => { + return [0, n]; + }).reduce((a, b) => { + return a.concat(b); + }); + } + + // Output + switch (outputFormat) { + case "Nibbles": + return nibbles.map(n => { + return n.toString(2).padStart(4, "0"); + }).join(" "); + case "Bytes": + return bytes.map(b => { + return b.toString(2).padStart(8, "0"); + }).join(" "); + case "Raw": + default: + return Utils.byteArrayToChars(bytes); + } + } + +} + +export default ToBCD; diff --git a/src/core/operations/ToBase.mjs b/src/core/operations/ToBase.mjs new file mode 100644 index 00000000..e37431e2 --- /dev/null +++ b/src/core/operations/ToBase.mjs @@ -0,0 +1,53 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import OperationError from "../errors/OperationError"; + +/** + * To Base operation + */ +class ToBase extends Operation { + + /** + * ToBase constructor + */ + constructor() { + super(); + + this.name = "To Base"; + this.module = "Default"; + this.description = "Converts a decimal number to a given numerical base."; + this.inputType = "BigNumber"; + this.outputType = "string"; + this.args = [ + { + "name": "Radix", + "type": "number", + "value": 36 + } + ]; + } + + /** + * @param {BigNumber} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + if (!input) { + throw new OperationError("Error: Input must be a number"); + } + const radix = args[0]; + if (radix < 2 || radix > 36) { + throw new OperationError("Error: Radix argument must be between 2 and 36"); + } + return input.toString(radix); + } + +} + +export default ToBase; diff --git a/src/core/operations/ToBase58.mjs b/src/core/operations/ToBase58.mjs new file mode 100644 index 00000000..47e0096f --- /dev/null +++ b/src/core/operations/ToBase58.mjs @@ -0,0 +1,85 @@ +/** + * @author tlwr [toby@toby.codes] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import Utils from "../Utils"; +import OperationError from "../errors/OperationError"; +import {ALPHABET_OPTIONS} from "../lib/Base58"; + +/** + * To Base58 operation + */ +class ToBase58 extends Operation { + + /** + * ToBase58 constructor + */ + constructor() { + super(); + + this.name = "To Base58"; + this.module = "Default"; + this.description = "Base58 (similar to Base64) is a notation for encoding arbitrary byte data. It differs from Base64 by removing easily misread characters (i.e. l, I, 0 and O) to improve human readability.

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

e.g. hello world becomes StV1DL6CwTryKyV

Base58 is commonly used in cryptocurrencies (Bitcoin, Ripple, etc)."; + this.inputType = "byteArray"; + this.outputType = "string"; + this.args = [ + { + "name": "Alphabet", + "type": "editableOption", + "value": ALPHABET_OPTIONS + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let alphabet = args[0] || ALPHABET_OPTIONS[0].value, + result = [0]; + + alphabet = Utils.expandAlphRange(alphabet).join(""); + + if (alphabet.length !== 58 || + [].unique.call(alphabet).length !== 58) { + throw new OperationError("Error: alphabet must be of length 58"); + } + + if (input.length === 0) return ""; + + input.forEach(function(b) { + let carry = (result[0] << 8) + b; + result[0] = carry % 58; + carry = (carry / 58) | 0; + + for (let i = 1; i < result.length; i++) { + carry += result[i] << 8; + result[i] = carry % 58; + carry = (carry / 58) | 0; + } + + while (carry > 0) { + result.push(carry % 58); + carry = (carry / 58) | 0; + } + }); + + result = result.map(function(b) { + return alphabet[b]; + }).reverse().join(""); + + while (result.length < input.length) { + result = alphabet[0] + result; + } + + return result; + } + +} + +export default ToBase58;