diff --git a/package.json b/package.json index 0357e3f4..67965f4f 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,9 @@ "repository": "github:multiformats/js-multihash", "dependencies": { "buffer": "^5.6.0", - "multibase": "^1.0.1", - "varint": "^5.0.0" + "multibase": "^2.0.0", + "varint": "^5.0.0", + "web-encoding": "^1.0.2" }, "devDependencies": { "aegir": "^24.0.0", @@ -67,4 +68,4 @@ "node": ">=10.0.0", "npm": ">=6.0.0" } -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 8accc704..a095597c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +// @ts-check /* eslint-disable guard-for-in */ /** * Multihash implementation in JavaScript. @@ -10,7 +11,9 @@ const { Buffer } = require('buffer') const multibase = require('multibase') const varint = require('varint') const { names } = require('./constants') +const { TextDecoder } = require('web-encoding') +const textDecoder = new TextDecoder() const codes = {} for (const key in names) { @@ -22,15 +25,19 @@ exports.codes = Object.freeze(codes) /** * Convert the given multihash to a hex encoded string. * - * @param {Buffer} hash + * @param {Uint8Array} hash * @returns {string} */ exports.toHexString = function toHexString (hash) { - if (!Buffer.isBuffer(hash)) { - throw new Error('must be passed a buffer') + if (!(hash instanceof Uint8Array)) { + throw new Error('must be passed a Uint8Array') } - return hash.toString('hex') + const buffer = Buffer.isBuffer(hash) + ? hash + : Buffer.from(hash.buffer, hash.byteOffset, hash.byteLength) + + return buffer.toString('hex') } /** @@ -46,28 +53,27 @@ exports.fromHexString = function fromHexString (hash) { /** * Convert the given multihash to a base58 encoded string. * - * @param {Buffer} hash + * @param {Uint8Array} hash * @returns {string} */ exports.toB58String = function toB58String (hash) { - if (!Buffer.isBuffer(hash)) { - throw new Error('must be passed a buffer') + if (!(hash instanceof Uint8Array)) { + throw new Error('must be passed a Uint8Array') } - return multibase.encode('base58btc', hash).toString().slice(1) + return textDecoder.decode(multibase.encode('base58btc', hash)).slice(1) } /** * Convert the given base58 encoded string to a multihash. * - * @param {string|Buffer} hash + * @param {string|Uint8Array} hash * @returns {Buffer} */ exports.fromB58String = function fromB58String (hash) { - let encoded = hash - if (Buffer.isBuffer(hash)) { - encoded = hash.toString() - } + const encoded = hash instanceof Uint8Array + ? textDecoder.decode(hash) + : hash return multibase.decode('z' + encoded) } @@ -75,13 +81,16 @@ exports.fromB58String = function fromB58String (hash) { /** * Decode a hash from the given multihash. * - * @param {Buffer} buf + * @param {Uint8Array} bytes * @returns {{code: number, name: string, length: number, digest: Buffer}} result */ -exports.decode = function decode (buf) { - if (!(Buffer.isBuffer(buf))) { - throw new Error('multihash must be a Buffer') +exports.decode = function decode (bytes) { + if (!(bytes instanceof Uint8Array)) { + throw new Error('multihash must be a Uint8Array') } + let buf = Buffer.isBuffer(bytes) + ? bytes + : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) if (buf.length < 2) { throw new Error('multihash too short. must be > 2 bytes.') @@ -116,7 +125,7 @@ exports.decode = function decode (buf) { * * > **Note:** the length is derived from the length of the digest itself. * - * @param {Buffer} digest + * @param {Uint8Array} digest * @param {string|number} code * @param {number} [length] * @returns {Buffer} @@ -129,8 +138,8 @@ exports.encode = function encode (digest, code, length) { // ensure it's a hashfunction code. const hashfn = exports.coerceCode(code) - if (!(Buffer.isBuffer(digest))) { - throw new Error('digest should be a Buffer') + if (!(digest instanceof Uint8Array)) { + throw new Error('digest should be a Uint8Array') } if (length == null) { @@ -141,11 +150,13 @@ exports.encode = function encode (digest, code, length) { throw new Error('digest length should be equal to specified length.') } - return Buffer.concat([ - Buffer.from(varint.encode(hashfn)), - Buffer.from(varint.encode(length)), - digest - ]) + const hash = varint.encode(hashfn) + const len = varint.encode(length) + const buffer = Buffer.alloc(hash.length + len.length + digest.length) + buffer.set(hash, 0) + buffer.set(len, hash.length) + buffer.set(digest, hash.length + len.length) + return buffer } /** @@ -206,8 +217,8 @@ exports.isValidCode = function validCode (code) { /** * Check if the given buffer is a valid multihash. Throws an error if it is not valid. * - * @param {Buffer} multihash - * @returns {undefined} + * @param {Uint8Array} multihash + * @returns {void} * @throws {Error} */ function validate (multihash) { @@ -218,12 +229,12 @@ exports.validate = validate /** * Returns a prefix from a valid multihash. Throws an error if it is not valid. * - * @param {Buffer} multihash - * @returns {undefined} + * @param {Uint8Array} multihash + * @returns {Buffer} * @throws {Error} */ exports.prefix = function prefix (multihash) { validate(multihash) - return multihash.slice(0, 2) + return Buffer.from(multihash.buffer, multihash.byteOffset, 2) } diff --git a/test/index.spec.js b/test/index.spec.js index f7e11e5e..32bc7f22 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -12,6 +12,7 @@ const mh = require('../src') const constants = require('../src/constants') const validCases = require('./fixtures/valid') const invalidCases = require('./fixtures/invalid') +const { TextEncoder } = require('web-encoding') function sample (code, size, hex) { const toHex = (i) => { @@ -24,12 +25,28 @@ function sample (code, size, hex) { return Buffer.from(`${toHex(code)}${toHex(size)}${hex}`, 'hex') } +const they = (description, test) => { + it(`${description} (Buffer)`, () => test({ + encodeText: Buffer.from, + encodeHex: (text) => Buffer.from(text, 'hex') + })) + + const textEncoder = new TextEncoder() + it(`${description} (Uint8Array)`, () => test({ + encodeText: (text) => textEncoder.encode(text), + encodeHex: (text) => { + const { buffer, byteOffset, byteLength } = Buffer.from(text, 'hex') + return new Uint8Array(buffer, byteOffset, byteLength) + } + })) +} + describe('multihash', () => { describe('toHexString', () => { - it('valid', () => { + they('valid', ({ encodeHex }) => { validCases.forEach((test) => { const code = test.encoding.code - const buf = mh.encode(Buffer.from(test.hex, 'hex'), code) + const buf = mh.encode(encodeHex(test.hex), code) expect( mh.toHexString(buf) ).to.be.eql( @@ -42,16 +59,16 @@ describe('multihash', () => { expect( () => mh.toHexString('hello world') ).to.throw( - /must be passed a buffer/ + /must be passed a Uint8Array/ ) }) }) describe('fromHexString', () => { - it('valid', () => { + they('valid', ({ encodeHex }) => { validCases.forEach((test) => { const code = test.encoding.code - const buf = mh.encode(Buffer.from(test.hex, 'hex'), code) + const buf = mh.encode(encodeHex(test.hex), code) expect( mh.fromHexString(buf.toString('hex')).toString('hex') ).to.be.eql( @@ -62,10 +79,10 @@ describe('multihash', () => { }) describe('toB58String', () => { - it('valid', () => { + they('valid', ({ encodeHex }) => { validCases.forEach((test) => { const code = test.encoding.code - const buf = mh.encode(Buffer.from(test.hex, 'hex'), code) + const buf = mh.encode(encodeHex(test.hex), code) expect( mh.toB58String(buf) ).to.be.eql( @@ -78,15 +95,15 @@ describe('multihash', () => { expect( () => mh.toB58String('hello world') ).to.throw( - /must be passed a buffer/ + /must be passed a Uint8Array/ ) }) }) describe('fromB58String', () => { - it('valid', () => { + they('valid', ({ encodeHex, encodeText }) => { const src = 'QmPfjpVaf593UQJ9a5ECvdh2x17XuJYG5Yanv5UFnH3jPE' - const expected = Buffer.from('122013bf801597d74a660453412635edd8c34271e5998f801fac5d700c6ce8d8e461', 'hex') + const expected = encodeHex('122013bf801597d74a660453412635edd8c34271e5998f801fac5d700c6ce8d8e461') expect( mh.fromB58String(src) @@ -95,7 +112,7 @@ describe('multihash', () => { ) expect( - mh.fromB58String(Buffer.from(src)) + mh.fromB58String(encodeText(src)) ).to.be.eql( expected ) @@ -125,20 +142,20 @@ describe('multihash', () => { expect( () => mh.decode('hello') ).to.throw( - /multihash must be a Buffer/ + /multihash must be a Uint8Array/ ) }) }) describe('encode', () => { - it('valid', () => { + they('valid', ({ encodeHex }) => { validCases.forEach((test) => { const code = test.encoding.code const name = test.encoding.name const buf = sample(test.encoding.varint || code, test.size, test.hex) const results = [ - mh.encode(Buffer.from(test.hex, 'hex'), code), - mh.encode(Buffer.from(test.hex, 'hex'), name) + mh.encode(encodeHex(test.hex), code), + mh.encode(encodeHex(test.hex), name) ] results.forEach((res) => { @@ -151,7 +168,7 @@ describe('multihash', () => { }) }) - it('invalid', () => { + they('invalid', ({ encodeText }) => { expect( () => mh.encode() ).to.throw( @@ -161,11 +178,11 @@ describe('multihash', () => { expect( () => mh.encode('hello', 0x11) ).to.throw( - /digest should be a Buffer/ + /digest should be a Uint8Array/ ) expect( - () => mh.encode(Buffer.from('hello'), 0x11, 2) + () => mh.encode(encodeText('hello'), 0x11, 2) ).to.throw( /length should be equal/ ) @@ -188,7 +205,7 @@ describe('multihash', () => { ).to.throw() }) - const longBuffer = Buffer.alloc(150, 'a') + const longBuffer = Uint8Array.from(Buffer.alloc(150, 'a')) expect( () => mh.validate(longBuffer) ).to.throw() @@ -277,7 +294,7 @@ describe('multihash', () => { }) }) - it('invalid', () => { + they('invalid', ({ encodeText }) => { const invalidNames = [ 'sha256', 'sha9', @@ -293,7 +310,7 @@ describe('multihash', () => { }) expect( - () => mh.coerceCode(Buffer.from('hello')) + () => mh.coerceCode(encodeText('hello')) ).to.throw( /should be a number/ ) @@ -306,14 +323,14 @@ describe('multihash', () => { }) }) - it('prefix', () => { - const multihash = mh.encode(Buffer.from('hey'), 0x11, 3) + they('prefix', ({ encodeText }) => { + const multihash = mh.encode(encodeText('hey'), 0x11, 3) const prefix = mh.prefix(multihash) expect(prefix.toString('hex')).to.eql('1103') }) - it('prefix throws on invalid multihash', () => { - const multihash = Buffer.from('definitely not valid') + they('prefix throws on invalid multihash', ({ encodeText }) => { + const multihash = encodeText('definitely not valid') expect(() => mh.prefix(multihash)).to.throw() })