diff --git a/package.json b/package.json index 8b37399..4adb1da 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "iso-url": "^0.4.7", "it-glob": "0.0.8", "merge-options": "^2.0.0", + "multibase": "^2.0.0", "nanoid": "^3.1.3", "node-fetch": "^2.6.0", "stream-to-it": "^0.2.0" diff --git a/src/uint8arrays/compare.js b/src/uint8arrays/compare.js new file mode 100644 index 0000000..f0be7b1 --- /dev/null +++ b/src/uint8arrays/compare.js @@ -0,0 +1,32 @@ +'use strict' + +/** + * Can be used with Array.sort to sort and array with Uint8Array entries + * + * @param {Uint8Array} a + * @param {Uint8Array} b + * @returns {Number} + */ +function compare (a, b) { + for (let i = 0; i < a.byteLength; i++) { + if (a[i] < b[i]) { + return -1 + } + + if (a[i] > b[i]) { + return 1 + } + } + + if (a.byteLength > b.byteLength) { + return 1 + } + + if (a.byteLength < b.byteLength) { + return -1 + } + + return 0 +} + +module.exports = compare diff --git a/src/uint8arrays/concat.js b/src/uint8arrays/concat.js new file mode 100644 index 0000000..8762b76 --- /dev/null +++ b/src/uint8arrays/concat.js @@ -0,0 +1,45 @@ +'use strict' + +function byteLength (arrs) { + return arrs.reduce((acc, curr) => { + if (ArrayBuffer.isView(curr)) { + return acc + curr.byteLength + } else if (Array.isArray(curr)) { + return acc + curr.length + } + + throw new Error('Invalid input passed to concat, should be an Array or ArrayBuffer view') + }, 0) +} + +/** + * Returns a new Uint8Array created by concatenating the passed Arrays + * + * @param {Array} arrs + * @param {Number} [length] + * @returns {Uint8Array} + */ +function concat (arrs, length) { + if (!length) { + length = byteLength(arrs) + } + + const output = new Uint8Array(length) + let offset = 0 + + for (const arr of arrs) { + output.set(arr, offset) + + if (ArrayBuffer.isView(arr)) { + offset += arr.byteLength + } else if (Array.isArray(arr)) { + offset += arr.length + } else { + throw new Error('Invalid input passed to concat, should be an Array or ArrayBuffer view') + } + } + + return output +} + +module.exports = concat diff --git a/src/uint8arrays/equals.js b/src/uint8arrays/equals.js new file mode 100644 index 0000000..efe82f5 --- /dev/null +++ b/src/uint8arrays/equals.js @@ -0,0 +1,28 @@ +'use strict' + +/** + * Returns true if the two passed Uint8Arrays have the same content + * + * @param {Uint8Array} a + * @param {Uint8Array} b + * @returns {boolean} + */ +function equals (a, b) { + if (a === b) { + return true + } + + if (a.byteLength !== b.byteLength) { + return false + } + + for (let i = 0; i < a.byteLength; i++) { + if (a[i] !== b[i]) { + return false + } + } + + return true +} + +module.exports = equals diff --git a/src/uint8arrays/from-string.js b/src/uint8arrays/from-string.js new file mode 100644 index 0000000..148f195 --- /dev/null +++ b/src/uint8arrays/from-string.js @@ -0,0 +1,32 @@ +'use strict' + +const multibase = require('multibase') +const { names } = require('multibase/src/constants') +const TextEncoder = require('../text-encoder') +const utf8Encoder = new TextEncoder() + +/** + * Create a `Uint8Array` from the passed string + * + * @param {String} string + * @param {String} [encoding=utf8] utf8, base16, base64, base64urlpad, etc + * @returns {Uint8Array} + * @see {@link https://www.npmjs.com/package/multibase|multibase} for supported encodings other than `utf8` + */ +function fromString (string, encoding = 'utf8') { + if (encoding === 'utf8' || encoding === 'utf-8') { + return utf8Encoder.encode(string) + } + + const base = names[encoding] + + if (!base) { + throw new Error('Unknown base') + } + + string = `${base.code}${string}` + + return Uint8Array.from(multibase.decode(string)) +} + +module.exports = fromString diff --git a/src/uint8arrays/to-string.js b/src/uint8arrays/to-string.js new file mode 100644 index 0000000..0105ea5 --- /dev/null +++ b/src/uint8arrays/to-string.js @@ -0,0 +1,25 @@ +'use strict' + +const multibase = require('multibase') +const TextDecoder = require('../text-decoder') +const utf8Decoder = new TextDecoder('utf8') + +/** + * Turns a `Uint8Array` into a string. + * + * Supports `utf8` and any encoding supported by the multibase module + * + * @param {Uint8Array} buf The array to turn into a string + * @param {String} [encoding=utf8] The encoding to use + * @returns {String} + * @see {@link https://www.npmjs.com/package/multibase|multibase} for supported encodings other than `utf8` + */ +function toString (buf, encoding = 'utf8') { + if (encoding !== 'utf8' && encoding !== 'utf-8') { + buf = multibase.encode(encoding, buf).subarray(1) + } + + return utf8Decoder.decode(buf) +} + +module.exports = toString diff --git a/test/uint8array/compare.spec.js b/test/uint8array/compare.spec.js new file mode 100644 index 0000000..4155a0e --- /dev/null +++ b/test/uint8array/compare.spec.js @@ -0,0 +1,49 @@ +'use strict' + +/* eslint-env mocha */ +const { expect } = require('aegir/utils/chai') +const compare = require('../../src/uint8arrays/compare') + +describe('Uint8Array compare', () => { + it('is stable', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([0, 1, 2, 3]) + + expect([a, b].sort(compare)).to.deep.equal([ + a, + b + ]) + expect([b, a].sort(compare)).to.deep.equal([ + b, + a + ]) + }) + + it('compares two Uint8Arrays', () => { + const a = Uint8Array.from([0, 1, 2, 4]) + const b = Uint8Array.from([0, 1, 2, 3]) + + expect([a, b].sort(compare)).to.deep.equal([ + b, + a + ]) + expect([b, a].sort(compare)).to.deep.equal([ + b, + a + ]) + }) + + it('compares two Uint8Arrays with different lengths', () => { + const a = Uint8Array.from([0, 1, 2, 3, 4]) + const b = Uint8Array.from([0, 1, 2, 3]) + + expect([a, b].sort(compare)).to.deep.equal([ + b, + a + ]) + expect([b, a].sort(compare)).to.deep.equal([ + b, + a + ]) + }) +}) diff --git a/test/uint8array/concat.spec.js b/test/uint8array/concat.spec.js new file mode 100644 index 0000000..302dc28 --- /dev/null +++ b/test/uint8array/concat.spec.js @@ -0,0 +1,53 @@ +'use strict' + +/* eslint-env mocha */ +const { expect } = require('aegir/utils/chai') +const concat = require('../../src/uint8arrays/concat') + +describe('Uint8Array concat', () => { + it('concats two Uint8Arrays', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([4, 5, 6, 7]) + const c = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) + + expect(concat([a, b])).to.deep.equal(c) + }) + + it('concats two Uint8Arrays with a length', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([4, 5, 6, 7]) + const c = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) + + expect(concat([a, b], 8)).to.deep.equal(c) + }) + + it('concats mixed Uint8Arrays and Arrays', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = [4, 5, 6, 7] + const c = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) + + expect(concat([a, b])).to.deep.equal(c) + }) + + it('concats mixed Uint8Arrays and Arrays with a length', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = [4, 5, 6, 7] + const c = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) + + expect(concat([a, b], 8)).to.deep.equal(c) + }) + + it('throws when passed non ArrayBuffer views/Arrays', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = 'Hello world' + + expect(() => concat([a, b])).to.throw(/Invalid input/) + }) + + it('throws when passed non ArrayBuffer views/Arrays with a length', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = 'Hello world' + + expect(() => concat([a, b], 100)).to.throw(/Invalid input/) + }) +}) diff --git a/test/uint8array/equals.spec.js b/test/uint8array/equals.spec.js new file mode 100644 index 0000000..1466de4 --- /dev/null +++ b/test/uint8array/equals.spec.js @@ -0,0 +1,28 @@ +'use strict' + +/* eslint-env mocha */ +const { expect } = require('aegir/utils/chai') +const equals = require('../../src/uint8arrays/equals') + +describe('Uint8Array equals', () => { + it('finds two Uint8Arrays equal', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([0, 1, 2, 3]) + + expect(equals(a, b)).to.be.true() + }) + + it('finds two Uint8Arrays not equal', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([0, 1, 2, 4]) + + expect(equals(a, b)).to.be.false() + }) + + it('finds two Uint8Arrays with different lengths not equal', () => { + const a = Uint8Array.from([0, 1, 2, 3]) + const b = Uint8Array.from([0, 1, 2, 3, 4]) + + expect(equals(a, b)).to.be.false() + }) +}) diff --git a/test/uint8array/from-string.spec.js b/test/uint8array/from-string.spec.js new file mode 100644 index 0000000..ca833a5 --- /dev/null +++ b/test/uint8array/from-string.spec.js @@ -0,0 +1,29 @@ +'use strict' + +/* eslint-env mocha */ +const { expect } = require('aegir/utils/chai') +const fromString = require('../../src/uint8arrays/from-string') +const TextEncoder = require('../../src/text-encoder') + +describe('Uint8Array fromString', () => { + it('creates a Uint8Array from a string', () => { + const str = 'hello world' + const arr = new TextEncoder('utf8').encode(str) + + expect(fromString(str)).to.deep.equal(arr) + }) + + it('creates a Uint8Array from a base16 string', () => { + const str = '00010203aabbcc' + const arr = Uint8Array.from([0, 1, 2, 3, 170, 187, 204]) + + expect(fromString(str, 'base16')).to.deep.equal(arr) + }) + + it('creates a Uint8Array from a base64 string', () => { + const str = 'AAECA6q7zA' + const arr = Uint8Array.from([0, 1, 2, 3, 170, 187, 204]) + + expect(fromString(str, 'base64')).to.deep.equal(arr) + }) +}) diff --git a/test/uint8array/to-string.spec.js b/test/uint8array/to-string.spec.js new file mode 100644 index 0000000..7b87ab7 --- /dev/null +++ b/test/uint8array/to-string.spec.js @@ -0,0 +1,29 @@ +'use strict' + +/* eslint-env mocha */ +const { expect } = require('aegir/utils/chai') +const toString = require('../../src/uint8arrays/to-string') +const TextEncoder = require('../../src/text-encoder') + +describe('Uint8Array toString', () => { + it('creates a String from a Uint8Array', () => { + const str = 'hello world' + const arr = new TextEncoder('utf8').encode(str) + + expect(toString(arr)).to.deep.equal(str) + }) + + it('creates a hex string from a Uint8Array', () => { + const str = '00010203aabbcc' + const arr = Uint8Array.from([0, 1, 2, 3, 170, 187, 204]) + + expect(toString(arr, 'base16')).to.deep.equal(str) + }) + + it('creates a base64 string from a Uint8Array', () => { + const str = 'AAECA6q7zA' + const arr = Uint8Array.from([0, 1, 2, 3, 170, 187, 204]) + + expect(toString(arr, 'base64')).to.deep.equal(str) + }) +})