diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 40cae745..00000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.github -dist/test diff --git a/README.md b/README.md index 216f8c51..8b35c05b 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,154 @@ # multiformats -This library is for building an interface for working with various -inter-related multiformat technologies (multicodec, multihash, multibase, -and CID). +This library defines common interfaces and low level building blocks for varios inter-related multiformat technologies (multicodec, multihash, multibase, +and CID). They can be used to implement custom custom base +encoders / decoders / codecs, codec encoders /decoders and multihash hashers that comply to the interface that layers above assume. -The interface contains all you need for encoding and decoding the basic -structures with no codec information, codec encoder/decoders, base encodings -or hashing functions. You can then add codec info, codec encoders/decoders, -base encodings, and hashing functions to the interface. +Library provides implementations for most basics and many others can be found in linked repositories. -This allows you to pass around an interface containing only the code you need -which can greatly reduce dependencies and bundle size. +## Interfaces ```js -import { create } from 'multiformats' -import sha2 from 'multiformats/hashes/sha2' -import dagcbor from '@ipld/dag-cbor' -const { multihash, multicodec, CID } = create() -multihash.add(sha2) -multicodec.add(dagcbor) - -const buffer = multicodec.encode({ hello, 'world' }, 'dag-cbor') -const hash = await multihash.hash(buffer, 'sha2-256') -// raw codec is the only codec that is there by default -const cid = new CID(1, 'raw', hash) +import CID from 'multiformats/cid' +import json from 'multiformats/codecs/json' +import { sha256 } from 'multiformats/hashes/sha2' + +const bytes = json.encode({ hello: 'world' }) + +const hash = await sha256.digest(bytes) +const cid = CID.create(1, json.code, hash) +//> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) ``` -However, if you're doing this much you should probably use multiformats -with the `Block` API. +### Multibase Encoders / Decoders / Codecs + +CIDs can be serialized to string representation using multibase encoders that +implement [`MultibaseEncoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. Library +provides quite a few implementations that can be imported: ```js -// Import basics package with dep-free codecs, hashes, and base encodings -import multiformats from 'multiformats/basics' -import dagcbor from '@ipld/dag-cbor' -import { create } from '@ipld/block' // Yet to be released Block interface -multiformats.multicodec.add(dagcbor) -const Block = create(multiformats) -const block = Block.encoder({ hello: world }, 'dag-cbor') -const cid = await block.cid() +import { base64 } from "multiformats/bases/base64" +cid.toString(base64.encoder) +//> 'mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA' ``` -# Plugins +Parsing CID string serialized CIDs requires multibase decoder that implements +[`MultibaseDecoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. Library provides a +decoder for every encoder it provides: -By default, no base encodings, hash functions, or codec implementations are included with `multiformats`. -However, you can import the following bundles to get a `multiformats` interface with them already configured. +```js +CID.parse('mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA', base64.decoder) +//> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) +``` -| bundle | bases | hashes | codecs | -|---|---|---|---| -| `multiformats/basics` | `base32`, `base64` | `sha2-256`, `sha2-512` | `json`, `raw` | +Dual of multibase encoder & decoder is defined as multibase codec and it exposes +them as `encoder` and `decoder` properties. For added convenience codecs also +implement `MultibaseEncoder` and `MultibaseDecoder` interfaces so they could be +used as either or both: -## Base Encodings (multibase) -| bases | import | repo | - --- | --- | --- | -`base16` | `multiformats/bases/base16` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | -`base32`, `base32pad`, `base32hex`, `base32hexpad`, `base32z` | `multiformats/bases/base32` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | -`base64`, `base64pad`, `base64url`, `base64urlpad` | `multiformats/bases/base64` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | -`base58btc`, `base58flick4` | `multiformats/bases/base58` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | +```js +cid.toString(base64) +CID.parse(cid.toString(base64), base64) +``` -## Hash Functions (multihash) +**Note:** CID implementation comes bundled with `base32` and `base58btc` +multibase codecs so that CIDs can be base serialized to (version specific) +default base encoding and parsed without having to supply base encoders/decoders: -| hashes | import | repo | -| --- | --- | --- | -| `sha2-256`, `sha2-512` | `multiformats/hashes/sha2` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/hashes) | -| `sha3-224`, `sha3-256`, `sha3-384`,`sha3-512`, `shake-128`, `shake-256`, `keccak-224`, `keccak-256`, `keccak-384`, `keccak-512` | `@multiformats/sha3` | [multiformats/js-sha3](https://github.com/multiformats/js-sha3) | -| `murmur3-128`, `murmur3-32` | `@multiformats/murmur3` | [multiformats/js-murmur3](https://github.com/multiformats/js-murmur3) | +```js +const v1 = CID.parse('bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea') +v1.toString() +//> 'bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea' + +const v0 = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') +v0.toString() +//> 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' +v0.toV1().toString() +//> 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku' +``` -## Codec Implementations (multicodec) +### Multicodec Encoders / Decoders / Codecs -| codec | import | repo | -| --- | --- | --- | -| `raw` | `multiformats/codecs/raw` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/codecs) | -| `json` | `multiformats/codecs/json` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/codecs) | -| `dag-cbor` | `@ipld/dag-cbor` | [ipld/js-dag-cbor](https://github.com/ipld/js-dag-cbor) | -| `dag-json` | `@ipld/dag-json` | [ipld/js-dag-json](https://github.com/ipld/js-dag-json) | +Library defines [`BlockEncoder`, `BlockDecoder` and `BlockCodec` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/interface.ts) +and utility function to take care of the boilerplate when implementing them: -# API +```js +import { codec } from 'multiformats/codecs/codec' + +const json = codec({ + name: 'json', + // As per multiformats table + // https://github.com/multiformats/multicodec/blob/master/table.csv#L113 + code: 0x0200, + encode: json => new TextEncoder().encode(JSON.stringify(json)), + decode: bytes => JSON.parse(new TextDecoder().decode(bytes)) +}) +``` -# multiformats([table]) +Just like with multibase, here codecs are duals of `encoder` and `decoder` parts, +but they also implement both interfaces for convenience: -Returns a new multiformats interface. +```js +const hello = json.encoder.encode({ hello: 'world' }) +json.decode(b1) +//> { hello: 'world' } +``` -Can optionally pass in a table of multiformat entries. +### Multihash Hashers -# multihash +This library defines [`MultihashHasher` and `MultihashDigest` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/hashes/interface.ts) +and convinient function for implementing them: -## multihash.encode +```js +import * as hasher from 'multiformats/hashes/hasher') -## multihash.decode +const sha256 = hasher.from({ + // As per multiformats table + // https://github.com/multiformats/multicodec/blob/master/table.csv#L9 + name: 'sha2-256', + code: 0x12, -## multihash.validate + encode: (input) => new Uint8Array(crypto.createHash('sha256').update(input).digest()) +}) -## multihash.add +const hash = await sha256.digest(json.encode({ hello: 'world' })) +CID.create(1, json.code, hash) -## multihash.hash +//> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) +``` -# multicodec -## multicodec.encode -## multicodec.decode +# Implementations -## multicodec.add +By default, no base encodings (other than base32 & base58btc), hash functions, +or codec implementations are included exposed by `multiformats`, you need to +import the ones you need yourself. -# multibase +## Multibase codecs -## multibase.encode +| bases | import | repo | + --- | --- | --- | +`base16` | `multiformats/bases/base16` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | +`base32`, `base32pad`, `base32hex`, `base32hexpad`, `base32z` | `multiformats/bases/base32` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | +`base64`, `base64pad`, `base64url`, `base64urlpad` | `multiformats/bases/base64` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | +`base58btc`, `base58flick4` | `multiformats/bases/base58` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | -## multibase.decode +## Multihash hashers -## multibase.add +| hashes | import | repo | +| --- | --- | --- | +| `sha2-256`, `sha2-512` | `multiformats/hashes/sha2` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/hashes) | +| `sha3-224`, `sha3-256`, `sha3-384`,`sha3-512`, `shake-128`, `shake-256`, `keccak-224`, `keccak-256`, `keccak-384`, `keccak-512` | `@multiformats/sha3` | [multiformats/js-sha3](https://github.com/multiformats/js-sha3) | +| `murmur3-128`, `murmur3-32` | `@multiformats/murmur3` | [multiformats/js-murmur3](https://github.com/multiformats/js-murmur3) | -## multiformats/bases +## Codec Implementations (multicodec) -```js -import { create } from 'multiformats' -import base16 from 'multiformats/bases/base16' -import base32 from 'multiformats/bases/base32' -import base58 from 'multiformats/bases/base58' -import base64 from 'multiformats/bases/base64' -const multiformats = create() -multiformats.add([base16, base32, base58, base64]) -``` +| codec | import | repo | +| --- | --- | --- | +| `raw` | `multiformats/codecs/raw` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/codecs) | +| `json` | `multiformats/codecs/json` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/codecs) | +| `dag-cbor` | `@ipld/dag-cbor` | [ipld/js-dag-cbor](https://github.com/ipld/js-dag-cbor) | +| `dag-json` | `@ipld/dag-json` | [ipld/js-dag-json](https://github.com/ipld/js-dag-json) | -## multiformats/hashes - -## multiformats/codecs - -# CID - -Changes from `cids`: - -* All base encodings are cached indefinitely. -* CID's can be created without any multiformat data. - * The new API is entirely based on parsing the varints - so it doesn't need the table metadata in order to associate - string names. - -There are also numerous deprecations. These deprecations all stem from the -fact that we no longer know the full set of available multicodec information. -It's actually quite possible to provide a CID interface without this, you can -still do everything you used to do, you just need to use ints instead of strings -and do some of the fancier V0 coercions outside this library. - -Deprecation List: - * the multibase encoding is no longer cached during instantiation. - * this being indeterministic was causing some nasty problems downstream - since `toString()` needs to be used as a cache key and it's not possible - to encode V0 into anything but base58btc. this means that you can't have - deterministic hash keys without also requiring base58btc support, so we - removed this feature. - * no more .toBaseEncodedString(), just toString() - * no more .multibaseName - * no more .prefix() - * no more .codec - * new property ".code" is the multiformat integer. - * this is going to be a painful transition but we have to get off of using - the string if we ever want to drop the full table. while the DX for this is - nice it forces into bloating the bundle and makes using new codecs very - painful. diff --git a/package.json b/package.json index eb561f38..2c959394 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "type": "module", "scripts": { "build": "npm_config_yes=true npx ipjs@latest build --tests", + "build:vendor": "npm run build:vendor:varint && npm run build:vendor:base-x", + "build:vendor:varint": "npx brrp -x varint > vendor/varint.js", + "build:vendor:base-x": "npx brrp -x @multiformats/base-x > vendor/base-x.js", "publish": "npm_config_yes=true npx ipjs@latest publish", "lint": "standard", "test:cjs": "npm run build && mocha dist/cjs/node-test/test-*.js && npm run test:cjs:browser", @@ -13,7 +16,13 @@ "test:cjs:browser": "polendina --cleanup dist/cjs/browser-test/test-*.js", "test": "npm run lint && npm run test:node && npm run test:cjs", "test:node-v12": "mocha test/test-*.js && npm run test:cjs", - "coverage": "c8 --reporter=html mocha test/test-*.js && npm_config_yes=true npx st -d coverage -p 8080" + "coverage": "c8 --exclude vendor --reporter=html mocha test/test-*.js && npm_config_yes=true npx st -d coverage -p 8080" + }, + "c8": { + "exclude": [ + "test/**", + "vendor/**" + ] }, "keywords": [], "author": "Mikeal Rogers (https://www.mikealrogers.com/)", @@ -22,10 +31,6 @@ ".": { "import": "./src/index.js" }, - "./basics": { - "import": "./src/basics-import.js", - "browser": "./src/basics-browser.js" - }, "./cid": { "import": "./src/cid.js" }, @@ -45,10 +50,19 @@ "import": "./src/bases/base64-import.js", "browser": "./src/bases/base64-browser.js" }, + "./hashes/hasher": { + "import": "./src/hashes/hasher.js" + }, + "./hashes/digest": { + "import": "./src/hashes/digest.js" + }, "./hashes/sha2": { "browser": "./src/hashes/sha2-browser.js", "import": "./src/hashes/sha2.js" }, + "./codecs/codec": { + "import": "./src/codecs/codec.js" + }, "./codecs/json": { "import": "./src/codecs/json.js" }, @@ -69,10 +83,8 @@ ] }, "dependencies": { - "base-x": "^3.0.8", "buffer": "^5.6.0", - "cids": "^1.0.0", - "varint": "^5.0.0" + "cids": "^1.0.0" }, "directories": { "test": "test" diff --git a/src/bases/base.js b/src/bases/base.js new file mode 100644 index 00000000..5b7b7046 --- /dev/null +++ b/src/bases/base.js @@ -0,0 +1,276 @@ +// @ts-check + +/** + * @typedef {import('./interface').BaseEncoder} BaseEncoder + * @typedef {import('./interface').BaseDecoder} BaseDecoder + * @typedef {import('./interface').BaseCodec} BaseCodec + */ + +/** + * @template {string} T + * @typedef {import('./interface').Multibase} Multibase + */ +/** + * @template {string} T + * @typedef {import('./interface').MultibaseEncoder} MultibaseEncoder + */ + +/** + * Class represents both BaseEncoder and MultibaseEncoder meaning it + * can be used to encode to multibase or base encode without multibase + * prefix. + * @class + * @template {string} Base + * @template {string} Prefix + * @implements {MultibaseEncoder} + * @implements {BaseEncoder} + */ +class Encoder { + /** + * @param {Base} name + * @param {Prefix} prefix + * @param {(bytes:Uint8Array) => string} baseEncode + */ + constructor (name, prefix, baseEncode) { + this.name = name + this.prefix = prefix + this.baseEncode = baseEncode + } + + /** + * @param {Uint8Array} bytes + * @returns {Multibase} + */ + encode (bytes) { + if (bytes instanceof Uint8Array) { + return `${this.prefix}${this.baseEncode(bytes)}` + } else { + throw Error('Unknown type, must be binary type') + } + } +} + +/** + * @template {string} Prefix + * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder + */ + +/** + * @template {string} Prefix + * @typedef {import('./interface').UnibaseDecoder} UnibaseDecoder + */ + +/** + * @template {string} Prefix + */ +/** + * Class represents both BaseDecoder and MultibaseDecoder so it could be used + * to decode multibases (with matching prefix) or just base decode strings + * with corresponding base encoding. + * @class + * @template {string} Base + * @template {string} Prefix + * @implements {MultibaseDecoder} + * @implements {UnibaseDecoder} + * @implements {BaseDecoder} + */ +class Decoder { + /** + * @param {Base} name + * @param {Prefix} prefix + * @param {(text:string) => Uint8Array} baseDecode + */ + constructor (name, prefix, baseDecode) { + this.name = name + this.prefix = prefix + this.baseDecode = baseDecode + } + + /** + * @param {string} text + */ + decode (text) { + if (typeof text === 'string') { + switch (text[0]) { + case this.prefix: { + return this.baseDecode(text.slice(1)) + } + default: { + throw Error(`Unable to decode multibase string ${JSON.stringify(text)}, ${this.name} decoder only supports inputs prefixed with ${this.prefix}`) + } + } + } else { + throw Error('Can only multibase decode strings') + } + } + + /** + * @template {string} OtherPrefix + * @param {UnibaseDecoder|ComposedDecoder} decoder + * @returns {ComposedDecoder} + */ + or (decoder) { + /** @type {Decoders} */ + const decoders = ({ + [this.prefix]: this, + ...decoder.decoders || ({ [decoder.prefix]: decoder }) + }) + + return new ComposedDecoder(decoders) + } +} + +/** + * @template {string} Prefix + * @typedef {import('./interface').CombobaseDecoder} CombobaseDecoder + */ + +/** + * @template {string} Prefix + * @typedef {Record>} Decoders + */ + +/** + * @template {string} Prefix + * @implements {MultibaseDecoder} + * @implements {CombobaseDecoder} + */ +class ComposedDecoder { + /** + * @param {Record>} decoders + */ + constructor (decoders) { + this.decoders = decoders + } + + /** + * @template {string} OtherPrefix + * @param {UnibaseDecoder|ComposedDecoder} decoder + * @returns {ComposedDecoder} + */ + or (decoder) { + /** @type {Decoders} */ + const other = (decoder.decoders || { [decoder.prefix]: decoder }) + return new ComposedDecoder({ + ...this.decoders, + ...other + }) + } + + /** + * @param {string} input + * @returns {Uint8Array} + */ + decode (input) { + const prefix = input[0] + const decoder = this.decoders[prefix] + if (decoder) { + return decoder.decode(input) + } else { + throw RangeError(`Unable to decode multibase string ${JSON.stringify(input)}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) + } + } +} + +/** + * @template T + * @typedef {import('./interface').MultibaseCodec} MultibaseCodec + */ + +/** + * @class + * @template {string} Base + * @template {string} Prefix + * @implements {MultibaseCodec} + * @implements {MultibaseEncoder} + * @implements {MultibaseDecoder} + * @implements {BaseCodec} + * @implements {BaseEncoder} + * @implements {BaseDecoder} + */ +export class Codec { + /** + * @param {Base} name + * @param {Prefix} prefix + * @param {(bytes:Uint8Array) => string} baseEncode + * @param {(text:string) => Uint8Array} baseDecode + */ + constructor (name, prefix, baseEncode, baseDecode) { + this.name = name + this.prefix = prefix + this.baseEncode = baseEncode + this.baseDecode = baseDecode + this.encoder = new Encoder(name, prefix, baseEncode) + this.decoder = new Decoder(name, prefix, baseDecode) + } + + /** + * @param {Uint8Array} input + */ + encode (input) { + return this.encoder.encode(input) + } + + decode (input) { + return this.decoder.decode(input) + } +} + +/** + * @template {string} Base + * @template {string} Prefix + * @param {Object} options + * @param {Base} options.name + * @param {Prefix} options.prefix + * @param {string} options.alphabet + * @param {(input:Uint8Array, alphabet:string) => string} options.encode + * @param {(input:string, alphabet:string) => Uint8Array} options.decode + */ +export const withAlphabet = ({ name, prefix, encode, decode, alphabet }) => + from({ + name, + prefix, + encode: input => encode(input, alphabet), + decode: input => { + for (const char of input) { + if (alphabet.indexOf(char) < 0) { + throw new Error(`invalid ${name} character`) + } + } + return decode(input, alphabet) + } + }) + +/** + * @template {string} Base + * @template {string} Prefix + * @template Settings + * + * @param {Object} options + * @param {Base} options.name + * @param {Prefix} options.prefix + * @param {Settings} options.settings + * @param {(input:Uint8Array, settings:Settings) => string} options.encode + * @param {(input:string, settings:Settings) => Uint8Array} options.decode + */ + +export const withSettings = ({ name, prefix, settings, encode, decode }) => + from({ + name, + prefix, + encode: (input) => encode(input, settings), + decode: (input) => decode(input, settings) + }) + +/** + * @template {string} Base + * @template {string} Prefix + * @param {Object} options + * @param {Base} options.name + * @param {Prefix} options.prefix + * @param {(bytes:Uint8Array) => string} options.encode + * @param {(input:string) => Uint8Array} options.decode + * @returns {Codec} + */ +export const from = ({ name, prefix, encode, decode }) => + new Codec(name, prefix, encode, decode) diff --git a/src/bases/base16.js b/src/bases/base16.js index 99f57534..da8a308f 100644 --- a/src/bases/base16.js +++ b/src/bases/base16.js @@ -1,17 +1,12 @@ -import { fromHex, toHex } from '../bytes.js' +// @ts-check -const create = function base16 (alphabet) { - return { - encode: input => toHex(input), - decode (input) { - for (const char of input) { - if (alphabet.indexOf(char) < 0) { - throw new Error('invalid base16 character') - } - } - return fromHex(input) - } - } -} +import { fromHex, toHex } from '../bytes.js' +import { withAlphabet } from './base.js' -export default { prefix: 'f', name: 'base16', ...create('0123456789abcdef') } +export const base16 = withAlphabet({ + prefix: 'f', + name: 'base16', + alphabet: '0123456789abcdef', + encode: toHex, + decode: fromHex +}) diff --git a/src/bases/base32.js b/src/bases/base32.js index 158465a0..85613596 100644 --- a/src/bases/base32.js +++ b/src/bases/base32.js @@ -1,3 +1,7 @@ +// @ts-check + +import { withAlphabet } from './base.js' + function decode (input, alphabet) { input = input.replace(new RegExp('=', 'g'), '') const length = input.length @@ -57,25 +61,42 @@ function encode (buffer, alphabet) { return output } -const create = alphabet => { - return { - encode: input => encode(input, alphabet), - decode (input) { - for (const char of input) { - if (alphabet.indexOf(char) < 0) { - throw new Error('invalid base32 character') - } - } - - return decode(input, alphabet) - } - } -} - -export default [ - { prefix: 'b', name: 'base32', ...create('abcdefghijklmnopqrstuvwxyz234567') }, - { prefix: 'c', name: 'base32pad', ...create('abcdefghijklmnopqrstuvwxyz234567=') }, - { prefix: 'v', name: 'base32hex', ...create('0123456789abcdefghijklmnopqrstuv') }, - { prefix: 't', name: 'base32hexpad', ...create('0123456789abcdefghijklmnopqrstuv=') }, - { prefix: 'h', name: 'base32z', ...create('ybndrfg8ejkmcpqxot1uwisza345h769') } -] +export const base32 = withAlphabet({ + prefix: 'b', + name: 'base32', + alphabet: 'abcdefghijklmnopqrstuvwxyz234567', + encode, + decode +}) + +export const base32pad = withAlphabet({ + prefix: 'c', + name: 'base32pad', + alphabet: 'abcdefghijklmnopqrstuvwxyz234567=', + encode, + decode +}) + +export const base32hex = withAlphabet({ + prefix: 'v', + name: 'base32hex', + alphabet: '0123456789abcdefghijklmnopqrstuv', + encode, + decode +}) + +export const base32hexpad = withAlphabet({ + prefix: 't', + name: 'base32hexpad', + alphabet: '0123456789abcdefghijklmnopqrstuv=', + encode, + decode +}) + +export const base32z = withAlphabet({ + prefix: 'h', + name: 'base32z', + alphabet: 'ybndrfg8ejkmcpqxot1uwisza345h769', + encode, + decode +}) diff --git a/src/bases/base58.js b/src/bases/base58.js index e4ac448e..21931a66 100644 --- a/src/bases/base58.js +++ b/src/bases/base58.js @@ -1,16 +1,25 @@ -import baseX from 'base-x' +// @ts-check + +import baseX from '../../vendor/base-x.js' import { coerce } from '../bytes.js' -import { Buffer } from 'buffer' +import { from } from './base.js' -const wrap = obj => ({ - encode: b => obj.encode(Buffer.from(b)), - decode: s => coerce(obj.decode(s)) -}) +const implement = (alphabet) => { + const { encode, decode } = baseX(alphabet) + return { + encode, + decode: text => coerce(decode(text)) + } +} -const btc = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -const flickr = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' +export const base58btc = from({ + name: 'base58btc', + prefix: 'z', + ...implement('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz') +}) -export default [ - { name: 'base58btc', prefix: 'z', ...wrap(baseX(btc)) }, - { name: 'base58flickr', prefix: 'Z', ...wrap(baseX(flickr)) } -] +export const base58flickr = from({ + name: 'base58flickr', + prefix: 'Z', + ...implement('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ') +}) diff --git a/src/bases/base64-browser.js b/src/bases/base64-browser.js index 03c718b5..372d81cd 100644 --- a/src/bases/base64-browser.js +++ b/src/bases/base64-browser.js @@ -1,6 +1,12 @@ +// @ts-check + /* globals btoa, atob */ -import create from './base64.js' -const encode = b => btoa([].reduce.call(b, (p, c) => p + String.fromCharCode(c), '')) -const decode = str => Uint8Array.from(atob(str), c => c.charCodeAt(0)) -const __browser = true -export default create({ encode, decode, __browser }) +import b64 from './base64.js' + +const { base64, base64pad, base64url, base64urlpad, __browser } = b64({ + encode: b => btoa([].reduce.call(b, (p, c) => p + String.fromCharCode(c), '')), + decode: str => Uint8Array.from(atob(str), c => c.charCodeAt(0)), + __browser: true +}) + +export { base64, base64pad, base64url, base64urlpad, __browser } diff --git a/src/bases/base64-import.js b/src/bases/base64-import.js index a55684c7..9ab5dd69 100644 --- a/src/bases/base64-import.js +++ b/src/bases/base64-import.js @@ -1,6 +1,13 @@ + +// @ts-check + import { coerce } from '../bytes.js' -import create from './base64.js' -const encode = o => Buffer.from(o).toString('base64') -const decode = s => coerce(Buffer.from(s, 'base64')) -const __browser = false -export default create({ encode, decode, __browser }) +import b64 from './base64.js' + +const { base64, base64pad, base64url, base64urlpad, __browser } = b64({ + encode: o => Buffer.from(o).toString('base64'), + decode: s => coerce(Buffer.from(s, 'base64')), + __browser: false +}) + +export { base64, base64pad, base64url, base64urlpad, __browser } diff --git a/src/bases/base64.js b/src/bases/base64.js index 9b032e9a..a96050c1 100644 --- a/src/bases/base64.js +++ b/src/bases/base64.js @@ -1,51 +1,105 @@ +// @ts-check + +import { withSettings } from './base.js' + +/** + * The alphabet is only used to know: + * 1. If padding is enabled (must contain '=') + * 2. If the output must be url-safe (must contain '-' and '_') + * 3. If the input of the output function is valid + * The alphabets from RFC 4648 are always used. + * @typedef {Object} Settings + * @property {boolean} padding + * @property {boolean} url + * @property {string} alphabet + * + * @param {string} alphabet + * @returns {Settings} + */ +const alphabetSettings = (alphabet) => ({ + alphabet, + padding: alphabet.indexOf('=') > -1, + url: alphabet.indexOf('-') > -1 && alphabet.indexOf('_') > -1 +}) + +/** + * @param {Object} b64 + * @param {(text:string) => Uint8Array} b64.decode + * @param {(bytes:Uint8Array) => string} b64.encode + * @param {boolean} b64.__browser + */ export default b64 => { - const create = alphabet => { - // The alphabet is only used to know: - // 1. If padding is enabled (must contain '=') - // 2. If the output must be url-safe (must contain '-' and '_') - // 3. If the input of the output function is valid - // The alphabets from RFC 4648 are always used. - const padding = alphabet.indexOf('=') > -1 - const url = alphabet.indexOf('-') > -1 && alphabet.indexOf('_') > -1 - - return { - encode (input) { - let output = b64.encode(input) - - if (url) { - output = output.replace(/\+/g, '-').replace(/\//g, '_') - } - - const pad = output.indexOf('=') - if (pad > 0 && !padding) { - output = output.substring(0, pad) - } - - return output - }, - decode (input) { - for (const char of input) { - if (alphabet.indexOf(char) < 0) { - throw new Error('invalid base64 character') - } - } - - return b64.decode(input) + /** + * @param {Uint8Array} input + * @param {Settings} settings + */ + const encode = (input, { url, padding }) => { + let output = b64.encode(input) + + if (url) { + output = output.replace(/\+/g, '-').replace(/\//g, '_') + } + + const pad = output.indexOf('=') + if (pad > 0 && !padding) { + output = output.substring(0, pad) + } + + return output + } + + /** + * @param {string} input + * @param {Settings} settings + */ + const decode = (input, { alphabet }) => { + for (const char of input) { + if (alphabet.indexOf(char) < 0) { + throw new Error('invalid base64 character') } } + + return b64.decode(input) } - const base64 = create('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') - const base64pad = create('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=') - const base64url = create('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_') - const base64urlpad = create('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=') - - const ex = [ - { prefix: 'm', name: 'base64', ...base64 }, - { prefix: 'M', name: 'base64pad', ...base64pad }, - { prefix: 'u', name: 'base64url', ...base64url }, - { prefix: 'U', name: 'base64urlpad', ...base64urlpad } - ] - ex.b64 = b64 - return ex + /** + * @template {string} Base + * @template {string} Prefix + * @param {Object} options + * @param {Base} options.name + * @param {Prefix} options.prefix + * @param {string} options.alphabet + */ + const codec = ({ name, prefix, alphabet }) => withSettings({ + name, + prefix, + settings: alphabetSettings(alphabet), + decode, + encode + }) + + return { + b64, + __browser: b64.__browser, + base64: codec({ + name: 'base64', + prefix: 'm', + alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + }), + base64pad: codec({ + name: 'base64pad', + prefix: 'M', + alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + }), + base64url: codec({ + name: 'base64url', + prefix: 'u', + alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' + }), + base64urlpad: codec({ + name: 'base64urlpad', + prefix: 'U', + alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=' + }) + } } diff --git a/src/bases/interface.ts b/src/bases/interface.ts new file mode 100644 index 00000000..474fe26c --- /dev/null +++ b/src/bases/interface.ts @@ -0,0 +1,99 @@ +// Base encoders / decoders just base encode / decode between binary and +// textual represenatinon. They are unaware of multibase. + +/** + * Base encoder just encodes bytes into base encoded string. + */ +export interface BaseEncoder { + /** + * Base encodes to a **plain** (and not a multibase) string. Unlike + * `encode` no multibase prefix is added. + * @param bytes + */ + baseEncode(bytes: Uint8Array): string +} + +/** + * Base decoder decodes encoded with matching base encoding into bytes. + */ +export interface BaseDecoder { + /** + * Decodes **plain** (and not a multibase) string. Unilke + * decode + * @param text + */ + baseDecode(text: string): Uint8Array +} + +/** + * Base codec is just dual of encoder and decoder. + */ +export interface BaseCodec { + encoder: BaseEncoder + decoder: BaseDecoder +} + +/** + * Multibase represets base encoded strings with a prefix first character + * describing it's encoding. + */ +export type Multibase = + | string + | string & { [0]: Prefix } + +/** + * Multibase encoder for the specific base encoding encodes bytes into + * multibase of that encoding. + */ +export interface MultibaseEncoder { + /** + * Name of the encoding. + */ + name: string + /** + * Prefix character for that base encoding. + */ + prefix: Prefix + /** + * Encodes binary data into **multibase** string (which will have a + * prefix added). + */ + encode(bytes: Uint8Array): Multibase +} + +/** + * Interface implemented by multibase decoder, that takes multibase strings + * to bytes. It may support single encoding like base32 or multiple encodings + * like base32, base58btc, base64. If passed multibase is incompatible it will + * throw an exception. + */ +export interface MultibaseDecoder { + /** + * Decodes **multibase** string (which must have a multibase prefix added). + * If prefix does not match + * @param multibase + */ + decode(multibase: Multibase): Uint8Array +} + +/** + * Dual of multibase encoder and decoder. + */ +export interface MultibaseCodec { + name: string + prefix: Prefix + encoder: MultibaseEncoder + decoder: MultibaseDecoder +} + + +export interface UnibaseDecoder extends MultibaseDecoder { + // Reserve this property so it can be used to derive type. + readonly decoders?: null + + readonly prefix: Prefix +} + +export interface CombobaseDecoder extends MultibaseDecoder { + readonly decoders: Record> +} diff --git a/src/basics-browser.js b/src/basics-browser.js index f3e86b4c..f4fef934 100644 --- a/src/basics-browser.js +++ b/src/basics-browser.js @@ -1,3 +1,8 @@ -import create from './basics.js' -import base64 from './bases/base64-browser.js' -export default create(base64) +// @ts-check + +import * as base64 from './bases/base64-browser.js' +import { CID, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' + +const bases = { ..._bases, ...base64 } + +export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics-import.js b/src/basics-import.js index 676d43b2..f5f2a224 100644 --- a/src/basics-import.js +++ b/src/basics-import.js @@ -1,3 +1,6 @@ -import create from './basics.js' -import base64 from './bases/base64-import.js' -export default create(base64) + +import { CID, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import * as base64 from './bases/base64-import.js' + +const bases = { ..._bases, ...base64 } +export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics.js b/src/basics.js index 893f7656..66974a19 100644 --- a/src/basics.js +++ b/src/basics.js @@ -1,13 +1,16 @@ -import { create } from './index.js' +// @ts-check + +import * as base32 from './bases/base32.js' +import * as base58 from './bases/base58.js' +import * as sha2 from './hashes/sha2.js' + import raw from './codecs/raw.js' import json from './codecs/json.js' -import base32 from './bases/base32.js' -import sha2 from './hashes/sha2.js' - -export default base64 => { - const multiformats = create() - multiformats.multihash.add(sha2) - multiformats.multicodec.add([raw, json]) - multiformats.multibase.add([base32, base64]) - return multiformats -} + +import { CID, hasher, digest, varint, bytes } from './index.js' + +const bases = { ...base32, ...base58 } +const hashes = { ...sha2 } +const codecs = { raw, json } + +export { CID, hasher, digest, varint, bytes, hashes, bases, codecs } diff --git a/src/block/interface.ts b/src/block/interface.ts new file mode 100644 index 00000000..4d8af806 --- /dev/null +++ b/src/block/interface.ts @@ -0,0 +1,18 @@ +// Block +import CID from "../cid" +import { MultihashHasher } from '../hashes/interface' + +// Just a representation for awaitable `T`. +export type Awaitable = + | T + | Promise + + +export interface Block { + cid(): Awaitable + encode(): Awaitable +} + +export interface Config { + hasher: MultihashHasher +} diff --git a/src/bytes.js b/src/bytes.js index c1883ba8..5ed30fe1 100644 --- a/src/bytes.js +++ b/src/bytes.js @@ -1,9 +1,24 @@ +// @ts-check + +const empty = new Uint8Array(0) + +/** + * @param {Uint8Array} d + */ const toHex = d => d.reduce((hex, byte) => hex + byte.toString(16).padStart(2, '0'), '') + +/** + * @param {string} hex + */ const fromHex = hex => { - if (!hex.length) return new Uint8Array(0) + if (!hex.length) return empty return new Uint8Array(hex.match(/../g).map(b => parseInt(b, 16))) } +/** + * @param {Uint8Array} aa + * @param {Uint8Array} bb + */ const equals = (aa, bb) => { if (aa === bb) return true if (aa.byteLength !== bb.byteLength) { @@ -19,6 +34,9 @@ const equals = (aa, bb) => { return true } +/** + * @param {ArrayBufferView|ArrayBuffer} o + */ const coerce = o => { if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return o if (o instanceof ArrayBuffer) return new Uint8Array(o) @@ -28,10 +46,23 @@ const coerce = o => { throw new Error('Unknown type, must be binary type') } +/** + * @param {any} o + * @returns {o is ArrayBuffer|ArrayBufferView} + */ const isBinary = o => o instanceof ArrayBuffer || ArrayBuffer.isView(o) +/** + * @param {string} str + * @returns {Uint8Array} + */ const fromString = str => (new TextEncoder()).encode(str) + +/** + * @param {Uint8Array} b + * @returns {string} + */ const toString = b => (new TextDecoder()).decode(b) -export { equals, coerce, isBinary, fromHex, toHex, fromString, toString } +export { equals, coerce, isBinary, fromHex, toHex, fromString, toString, empty } diff --git a/src/cid.js b/src/cid.js index b2b2da24..7d2b54ca 100644 --- a/src/cid.js +++ b/src/cid.js @@ -1,342 +1,431 @@ -import * as Bytes from './bytes.js' +// @ts-check -const property = (value, { writable = false, enumerable = true, configurable = false } = {}) => ({ - value, - writable, - enumerable, - configurable -}) +import * as varint from './varint.js' +import * as Digest from './hashes/digest.js' +import { base58btc } from './bases/base58.js' +import { base32 } from './bases/base32.js' -// ESM does not support importing package.json where this version info -// should come from. To workaround it version is copied here. -const version = '0.0.0-dev' -// Start throwing exceptions on major version bump -const deprecate = (range, message) => { - if (range.test(version)) { - console.warn(message) - /* c8 ignore next 3 */ - } else { - throw new Error(message) - } -} - -const IS_CID_DEPRECATION = -`CID.isCID(v) is deprecated and will be removed in the next major release. -Following code pattern: - -if (CID.isCID(value)) { - doSomethingWithCID(value) -} - -Is replaced with: +/** + * @typedef {import('./hashes/interface').MultihashDigest} MultihashDigest + */ -const cid = CID.asCID(value) -if (cid) { - // Make sure to use cid instead of value - doSomethingWithCID(cid) -} -` +/** + * @template Prefix + * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder + */ /** - * @param {import('./index').Multiformats} multiformats + * @template Prefix + * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder */ -export default multiformats => { - const { multibase, varint, multihash } = multiformats +export default class CID { /** - * @param {number} version - * @param {number} codec - * @param {Uint8Array} multihash - * @returns {Uint8Array} + * @param {0|1} version + * @param {number} code + * @param {MultihashDigest} multihash + * @param {Uint8Array} bytes + * */ - const encodeCID = (version, codec, multihash) => { - const versionBytes = varint.encode(version) - const codecBytes = varint.encode(codec) - const bytes = new Uint8Array(versionBytes.byteLength + codecBytes.byteLength + multihash.byteLength) - bytes.set(versionBytes, 0) - bytes.set(codecBytes, versionBytes.byteLength) - bytes.set(multihash, versionBytes.byteLength + codecBytes.byteLength) - return bytes + constructor (version, code, multihash, bytes) { + this.code = code + this.version = version + this.multihash = multihash + this.bytes = bytes + + // ArrayBufferView + this.byteOffset = bytes.byteOffset + this.byteLength = bytes.byteLength + + // Circular reference + /** @private */ + this.asCID = this + /** + * @type {Map} + * @private + */ + this._baseCache = new Map() + + // Configure private properties + Object.defineProperties(this, { + byteOffset: hidden, + byteLength: hidden, + + code: readonly, + version: readonly, + multihash: readonly, + bytes: readonly, + + _baseCache: hidden, + asCID: hidden + }) } /** - * Takes `Uint8Array` representation of `CID` and returns - * `[version, codec, multihash]`. Throws error if bytes passed do not - * correspond to vaild `CID`. - * @param {Uint8Array} bytes - * @returns {[number, number, Uint8Array]} + * @returns {CID} */ - const decodeCID = (bytes) => { - const [version, offset] = varint.decode(bytes) - switch (version) { - // CIDv0 - case 18: { - return [0, 0x70, bytes] - } - // CIDv1 - case 1: { - const [code, length] = varint.decode(bytes.subarray(offset)) - return [1, code, decodeMultihash(bytes.subarray(offset + length))] + toV0 () { + switch (this.version) { + case 0: { + return this } default: { - throw new RangeError(`Invalid CID version ${version}`) + const { code, multihash } = this + + if (code !== DAG_PB_CODE) { + throw new Error('Cannot convert a non dag-pb CID to CIDv0') + } + + // sha2-256 + if (multihash.code !== SHA_256_CODE) { + throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') + } + + return CID.createV0(multihash) } } } - const cidSymbol = Symbol.for('@ipld/js-cid/CID') - /** - * Create CID from the string encoded CID. - * @param {string} string * @returns {CID} */ - const fromString = (string) => { - switch (string[0]) { - // V0 - case 'Q': { - const cid = new CID(multibase.get('base58btc').decode(string)) - cid._baseCache.set('base58btc', string) - return cid + toV1 () { + switch (this.version) { + case 0: { + const { code, digest } = this.multihash + const multihash = Digest.create(code, digest) + return CID.createV1(this.code, multihash) + } + case 1: { + return this } + /* c8 ignore next 3 */ default: { - // CID v1 - const cid = new CID(multibase.decode(string)) - cid._baseCache.set(multibase.encoding(string).name, string) - return cid + throw Error(`Can not convert CID version ${this.version} to version 0. This is a bug please report`) } } } /** - * Takes a hashCID multihash and validates the digest. Returns it back if - * all good otherwise throws error. - * @param {Uint8Array} hash - * @returns {Uint8Array} + * @param {any} other */ - const decodeMultihash = (hash) => { - const { digest, length } = multihash.decode(hash) - if (digest.length !== length) { - throw new Error('Given multihash has incorrect length') - } - - return hash + equals (other) { + return other && + this.code === other.code && + this.version === other.version && + Digest.equals(this.multihash, other.multihash) } /** - * @implements {ArrayBufferView} + * @param {MultibaseEncoder} [base] + * @returns {string} */ - class CID { - /** - * Creates new CID from the given value that is either CID, string or an - * Uint8Array. - * @param {CID|string|Uint8Array} value - */ - static from (value) { - if (typeof value === 'string') { - return fromString(value) - } else if (value instanceof Uint8Array) { - return new CID(value) - } else { - const cid = CID.asCID(value) - if (cid) { - // If we got the same CID back we create a copy. - if (cid === value) { - return new CID(cid.bytes) - } else { - return cid - } - } else { - throw new TypeError(`Can not create CID from given value ${value}`) - } - } + toString (base) { + const { bytes, version, _baseCache } = this + switch (version) { + case 0: + return toStringV0(bytes, _baseCache, base || base58btc.encoder) + default: + return toStringV1(bytes, _baseCache, base || base32.encoder) } + } - /** - * Creates new CID with a given version, codec and a multihash. - * @param {number} version - * @param {number} code - * @param {Uint8Array} multihash - */ - static create (version, code, multihash) { - if (typeof code !== 'number') { - throw new Error('String codecs are no longer supported') - } - - switch (version) { - case 0: { - if (code !== 112) { - throw new Error('Version 0 CID must be 112 codec (dag-cbor)') - } else { - return new CID(multihash) - } - } - case 1: { - // TODO: Figure out why we check digest here but not in v 0 - return new CID(encodeCID(version, code, decodeMultihash(multihash))) - } - default: { - throw new Error('Invalid version') - } - } + toJSON () { + return { + code: this.code, + version: this.version, + hash: this.multihash.bytes } + } - /** - * - * @param {ArrayBuffer|Uint8Array} buffer - * @param {number} [byteOffset=0] - * @param {number} [byteLength=buffer.byteLength] - */ - constructor (buffer, byteOffset = 0, byteLength = buffer.byteLength) { - const bytes = buffer instanceof Uint8Array - ? Bytes.coerce(buffer) // Just in case it's a node Buffer - : new Uint8Array(buffer, byteOffset, byteLength) - - const [version, code, multihash] = decodeCID(bytes) - Object.defineProperties(this, { - // ArrayBufferView - byteOffset: property(bytes.byteOffset, { enumerable: false }), - byteLength: property(bytes.byteLength, { enumerable: false }), - - // CID fields - version: property(version), - code: property(code), - multihash: property(multihash), - asCID: property(this), - - // Legacy - bytes: property(bytes, { enumerable: false }), - - // Internal - _baseCache: property(new Map(), { enumerable: false }) - }) - } + get [Symbol.toStringTag] () { + return 'CID' + } - get codec () { - throw new Error('"codec" property is deprecated, use integer "code" property instead') - } + // Legacy - get buffer () { - throw new Error('Deprecated .buffer property, use .bytes to get Uint8Array instead') - } + [Symbol.for('nodejs.util.inspect.custom')] () { + return 'CID(' + this.toString() + ')' + } - get multibaseName () { - throw new Error('"multibaseName" property is deprecated') - } + // Deprecated - get prefix () { - throw new Error('"prefix" property is deprecated') - } + static isCID (value) { + deprecate(/^0\.0/, IS_CID_DEPRECATION) + return !!(value && (value[cidSymbol] || value.asCID === value)) + } - toV0 () { - if (this.code !== 0x70 /* dag-pb */) { - throw new Error('Cannot convert a non dag-pb CID to CIDv0') - } + get toBaseEncodedString () { + throw new Error('Deprecated, use .toString()') + } - const { name } = multihash.decode(this.multihash) + get codec () { + throw new Error('"codec" property is deprecated, use integer "code" property instead') + } - if (name !== 'sha2-256') { - throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') - } + get buffer () { + throw new Error('Deprecated .buffer property, use .bytes to get Uint8Array instead') + } - return CID.create(0, this.code, this.multihash) - } + get multibaseName () { + throw new Error('"multibaseName" property is deprecated') + } - toV1 () { - return CID.create(1, this.code, this.multihash) - } + get prefix () { + throw new Error('"prefix" property is deprecated') + } - get toBaseEncodedString () { - throw new Error('Deprecated, use .toString()') + /** + * Takes any input `value` and returns a `CID` instance if it was + * a `CID` otherwise returns `null`. If `value` is instanceof `CID` + * it will return value back. If `value` is not instance of this CID + * class, but is compatible CID it will return new instance of this + * `CID` class. Otherwise returs null. + * + * This allows two different incompatible versions of CID library to + * co-exist and interop as long as binary interface is compatible. + * @param {any} value + * @returns {CID|null} + */ + static asCID (value) { + if (value instanceof CID) { + // If value is instance of CID then we're all set. + return value + } else if (value != null && value.asCID === value) { + // If value isn't instance of this CID class but `this.asCID === this` is + // true it is CID instance coming from a different implemnetation (diff + // version or duplicate). In that case we rebase it to this `CID` + // implemnetation so caller is guaranteed to get instance with expected + // API. + const { version, code, multihash, bytes } = value + return new CID(version, code, multihash, bytes || encodeCID(version, code, multihash.bytes)) + } else if (value != null && value[cidSymbol] === true) { + // If value is a CID from older implementation that used to be tagged via + // symbol we still rebase it to the this `CID` implementation by + // delegating that to a constructor. + const { version, multihash, code } = value + const digest = Digest.decode(multihash) + return CID.create(version, code, digest) + } else { + // Otherwise value is not a CID (or an incompatible version of it) in + // which case we return `null`. + return null } + } - [Symbol.for('nodejs.util.inspect.custom')] () { - return 'CID(' + this.toString() + ')' + /** + * + * @param {number} version - Version of the CID + * @param {number} code - Code of the codec content is encoded in. + * @param {MultihashDigest} digest - (Multi)hash of the of the content. + * @returns {CID} + */ + static create (version, code, digest) { + if (typeof code !== 'number') { + throw new Error('String codecs are no longer supported') } - toString (base) { - const { version, bytes } = this - if (version === 0) { - if (base && base !== 'base58btc') { - throw new Error(`Cannot string encode V0 in ${base} encoding`) + switch (version) { + case 0: { + if (code !== DAG_PB_CODE) { + throw new Error(`Version 0 CID must use dag-pb (code: ${DAG_PB_CODE}) block encoding`) + } else { + return new CID(version, code, digest, digest.bytes) } - const { encode } = multibase.get('base58btc') - return encode(bytes) } - - base = base || 'base32' - const { _baseCache } = this - const string = _baseCache.get(base) - if (string == null) { - const string = multibase.encode(bytes, base) - _baseCache.set(base, string) - return string - } else { - return string + case 1: { + const bytes = encodeCID(version, code, digest.bytes) + return new CID(version, code, digest, bytes) + } + default: { + throw new Error('Invalid version') } } + } + + /** + * Simplified version of `create` for CIDv0. + * @param {MultihashDigest} digest - Multihash. + */ + static createV0 (digest) { + return CID.create(0, DAG_PB_CODE, digest) + } + + /** + * Simplified version of `create` for CIDv1. + * @template {number} Code + * @param {Code} code - Content encoding format code. + * @param {MultihashDigest} digest - Miltihash of the content. + * @returns {CID} + */ + static createV1 (code, digest) { + return CID.create(1, code, digest) + } - toJSON () { - return { - code: this.code, - version: this.version, - hash: this.multihash + /** + * Takes cid in a binary representation and a `base` encoder that will be used + * for default cid serialization. + * + * Throws if supplied base encoder is incompatible (CIDv0 is only compatible + * with `base58btc` encoder). + * @param {Uint8Array} cid + */ + static decode (cid) { + const [version, offset] = varint.decode(cid) + switch (version) { + // CIDv0 + case 18: { + const multihash = Digest.decode(cid) + return CID.createV0(multihash) + } + // CIDv1 + case 1: { + const [code, length] = varint.decode(cid.subarray(offset)) + const digest = Digest.decode(cid.subarray(offset + length)) + return CID.createV1(code, digest) + } + default: { + throw new RangeError(`Invalid CID version ${version}`) } } + } - equals (other) { - return this.code === other.code && - this.version === other.version && - Bytes.equals(this.multihash, other.multihash) - } + /** + * Takes cid in a string representation and creates an instance. If `base` + * decoder is not provided will use a default from the configuration. It will + * throw an error if encoding of the CID is not compatible with supplied (or + * a default decoder). + * + * @template {string} Prefix + * @param {string} source + * @param {MultibaseDecoder} [base] + */ + static parse (source, base) { + const [prefix, bytes] = parseCIDtoBytes(source, base) - get [Symbol.toStringTag] () { - return 'CID' - } + const cid = CID.decode(bytes) + // Cache string representation to avoid computing it on `this.toString()` + // @ts-ignore - Can't access private + cid._baseCache.set(prefix, source) - /** - * Takes any input `value` and returns a `CID` instance if it was - * a `CID` otherwise returns `null`. If `value` is instanceof `CID` - * it will return value back. If `value` is not instance of this CID - * class, but is compatible CID it will return new instance of this - * `CID` class. Otherwise returs null. - * - * This allows two different incompatible versions of CID library to - * co-exist and interop as long as binary interface is compatible. - * @param {any} value - * @returns {CID|null} - */ - static asCID (value) { - // If value is instance of CID then we're all set. - if (value instanceof CID) { - return value - // If value isn't instance of this CID class but `this.asCID === this` is - // true it is CID instance coming from a different implemnetation (diff - // version or duplicate). In that case we rebase it to this `CID` - // implemnetation so caller is guaranteed to get instance with expected - // API. - } else if (value != null && value.asCID === value) { - const { version, code, multihash } = value - return CID.create(version, code, multihash) - // If value is a CID from older implementation that used to be tagged via - // symbol we still rebase it to the this `CID` implementation by - // delegating that to a constructor. - } else if (value != null && value[cidSymbol] === true) { - const { version, multihash } = value - const code = value.code /* c8 ignore next */ || multiformats.get(value.codec).code - return new CID(encodeCID(version, code, multihash)) - // Otherwise value is not a CID (or an incompatible version of it) in - // which case we return `null`. - } else { - return null + return cid + } +} + +const parseCIDtoBytes = (source, base) => { + switch (source[0]) { + // CIDv0 is parsed differently + case 'Q': { + const decoder = base || base58btc + return [base58btc.prefix, decoder.decode(`${base58btc.prefix}${source}`)] + } + case base58btc.prefix: { + const decoder = base || base58btc + return [base58btc.prefix, decoder.decode(source)] + } + case base32.prefix: { + const decoder = base || base32 + return [base32.prefix, decoder.decode(source)] + } + default: { + if (base == null) { + throw Error('To parse non base32 or base56btc encoded CID multibase decoder must be provided') } + return [source[0], base.decode(source)] } + } +} - static isCID (value) { - deprecate(/^0\.0/, IS_CID_DEPRECATION) - return !!(value && (value[cidSymbol] || value.asCID === value)) - } +/** + * + * @param {Uint8Array} bytes + * @param {Map} cache + * @param {MultibaseEncoder<'z'>} base + */ +const toStringV0 = (bytes, cache, base) => { + const { prefix } = base + if (prefix !== base58btc.prefix) { + throw Error(`Cannot string encode V0 in ${base.name} encoding`) + } + + const cid = cache.get(prefix) + if (cid == null) { + const cid = base.encode(bytes).slice(1) + cache.set(prefix, cid) + return cid + } else { + return cid + } +} + +/** + * @template {string} Prefix + * @param {Uint8Array} bytes + * @param {Map} cache + * @param {MultibaseEncoder} base + */ +const toStringV1 = (bytes, cache, base) => { + const { prefix } = base + const cid = cache.get(prefix) + if (cid == null) { + const cid = base.encode(bytes) + cache.set(prefix, cid) + return cid + } else { + return cid } +} - return CID +const DAG_PB_CODE = 0x70 +const SHA_256_CODE = 0x12 + +/** + * + * @param {number} version + * @param {number} code + * @param {Uint8Array} multihash + * @returns {Uint8Array} + */ +const encodeCID = (version, code, multihash) => { + const codeOffset = varint.encodingLength(version) + const hashOffset = codeOffset + varint.encodingLength(code) + const bytes = new Uint8Array(hashOffset + multihash.byteLength) + varint.encodeTo(version, bytes, 0) + varint.encodeTo(code, bytes, codeOffset) + bytes.set(multihash, hashOffset) + return bytes } + +const cidSymbol = Symbol.for('@ipld/js-cid/CID') +const readonly = { writable: false, configurable: false, enumerable: true } +const hidden = { writable: false, enumerable: false, configurable: false } + +// ESM does not support importing package.json where this version info +// should come from. To workaround it version is copied here. +const version = '0.0.0-dev' +// Start throwing exceptions on major version bump +const deprecate = (range, message) => { + if (range.test(version)) { + console.warn(message) + /* c8 ignore next 3 */ + } else { + throw new Error(message) + } +} + +const IS_CID_DEPRECATION = +`CID.isCID(v) is deprecated and will be removed in the next major release. +Following code pattern: + +if (CID.isCID(value)) { + doSomethingWithCID(value) +} + +Is replaced with: + +const cid = CID.asCID(value) +if (cid) { + // Make sure to use cid instead of value + doSomethingWithCID(cid) +} +` diff --git a/src/codecs/codec.js b/src/codecs/codec.js new file mode 100644 index 00000000..5dd08675 --- /dev/null +++ b/src/codecs/codec.js @@ -0,0 +1,108 @@ +// @ts-check + +/** + * @template {string} Name + * @template {number} Code + * @template T + * + * @param {Object} options + * @param {Name} options.name + * @param {Code} options.code + * @param {(data:T) => Uint8Array} options.encode + * @param {(bytes:Uint8Array) => T} options.decode + */ +export const codec = ({ name, code, decode, encode }) => + new Codec(name, code, encode, decode) + +/** + * @template {number} Code + * @template T + * @typedef {import('./interface').BlockEncoder} BlockEncoder + */ + +/** + * @class + * @template T + * @template {string} Name + * @template {number} Code + * @implements {BlockEncoder} + */ +export class Encoder { + /** + * @param {Name} name + * @param {Code} code + * @param {(data:T) => Uint8Array} encode + */ + constructor (name, code, encode) { + this.name = name + this.code = code + this.encode = encode + } +} + +/** + * @template {number} Code + * @template T + * @typedef {import('./interface').BlockDecoder} BlockDecoder + */ + +/** + * @class + * @template {number} Code + * @template T + * @implements {BlockDecoder} + */ +export class Decoder { + /** + * @param {string} name + * @param {Code} code + * @param {(bytes:Uint8Array) => T} decode + */ + constructor (name, code, decode) { + this.name = name + this.code = code + this.decode = decode + } +} + +/** + * @template {number} Code + * @template T + * @typedef {import('./interface').BlockCodec} BlockCodec + */ + +/** + * @class + * @template {string} Name + * @template {number} Code + * @template T + * @implements {BlockCodec} + */ +export class Codec { + /** + * @param {Name} name + * @param {Code} code + * @param {(data:T) => Uint8Array} encode + * @param {(bytes:Uint8Array) => T} decode + */ + constructor (name, code, encode, decode) { + this.name = name + this.code = code + this.encode = encode + this.decode = decode + } + + get decoder () { + const { name, code, decode } = this + const decoder = new Decoder(name, code, decode) + Object.defineProperty(this, 'decoder', { value: decoder }) + return decoder + } + + get encoder () { + const { name, code, encode } = this + const encoder = new Encoder(name, code, encode) + Object.defineProperty(this, 'encoder', { value: encoder }) + return encoder + } +} diff --git a/src/codecs/interface.ts b/src/codecs/interface.ts new file mode 100644 index 00000000..3474ff38 --- /dev/null +++ b/src/codecs/interface.ts @@ -0,0 +1,31 @@ +/** + * IPLD encoder part of the codec. + */ +export interface BlockEncoder { + name: string + code: Code + encode(data: T): ByteView +} + +/** + * IPLD decoder part of the codec. + */ +export interface BlockDecoder { + code: Code + decode(bytes: ByteView): T +} + +/** + * IPLD codec that is just Encoder + Decoder however it is + * separate those capabilties as sender requires encoder and receiver + * requires decoder. + */ +export interface BlockCodec extends BlockEncoder, BlockDecoder { } + + +// This just a hack to retain type information abouth the data that +// is incoded `T` Because it's a union `data` field is never going +// to be usable anyway. +export type ByteView = + | Uint8Array + | Uint8Array & { data: T } diff --git a/src/codecs/json.js b/src/codecs/json.js index fc4c147c..c0bc11de 100644 --- a/src/codecs/json.js +++ b/src/codecs/json.js @@ -1,6 +1,10 @@ -export default { - encode: obj => new TextEncoder().encode(JSON.stringify(obj)), - decode: buff => JSON.parse(new TextDecoder().decode(buff)), +// @ts-check + +import { codec } from './codec.js' + +export default codec({ name: 'json', - code: 0x0200 -} + code: 0x0200, + encode: json => new TextEncoder().encode(JSON.stringify(json)), + decode: bytes => JSON.parse(new TextDecoder().decode(bytes)) +}) diff --git a/src/codecs/raw.js b/src/codecs/raw.js index 588f6734..b17fe69c 100644 --- a/src/codecs/raw.js +++ b/src/codecs/raw.js @@ -1,10 +1,17 @@ +// @ts-check + import { coerce } from '../bytes.js' +import { codec } from './codec.js' -const raw = buff => coerce(buff) +/** + * @param {Uint8Array} bytes + * @returns {Uint8Array} + */ +const raw = (bytes) => coerce(bytes) -export default { - encode: raw, - decode: raw, +export default codec({ name: 'raw', - code: 85 -} + code: 85, + decode: raw, + encode: raw +}) diff --git a/src/hashes/digest.js b/src/hashes/digest.js new file mode 100644 index 00000000..ea7a29bb --- /dev/null +++ b/src/hashes/digest.js @@ -0,0 +1,82 @@ +// @ts-check + +import { coerce, equals as equalBytes } from '../bytes.js' +import * as varint from '../varint.js' + +/** + * Creates a multihash digest. + * @template {number} Code + * @param {Code} code + * @param {Uint8Array} digest + */ +export const create = (code, digest) => { + const size = digest.byteLength + const sizeOffset = varint.encodingLength(code) + const digestOffset = sizeOffset + varint.encodingLength(size) + + const bytes = new Uint8Array(digestOffset + size) + varint.encodeTo(code, bytes, 0) + varint.encodeTo(size, bytes, sizeOffset) + bytes.set(digest, digestOffset) + + return new Digest(code, size, digest, bytes) +} + +/** + * Turns bytes representation of multihash digest into an instance. + * @param {Uint8Array} multihash + * @returns {Digest} + */ +export const decode = (multihash) => { + const bytes = coerce(multihash) + const [code, sizeOffset] = varint.decode(bytes) + const [size, digestOffset] = varint.decode(bytes.subarray(sizeOffset)) + const digest = bytes.subarray(sizeOffset + digestOffset) + + if (digest.byteLength !== size) { + throw new Error('Incorrect length') + } + + return new Digest(code, size, digest, bytes) +} + +/** + * @param {MultihashDigest} a + * @param {MultihashDigest} b + * @returns {boolean} + */ +export const equals = (a, b) => { + if (a === b) { + return true + } else { + return a.code === b.code && a.size === b.size && equalBytes(a.bytes, b.bytes) + } +} + +/** + * @typedef {import('./interface').MultihashDigest} MultihashDigest + */ + +/** + * Represents a multihash digest which carries information about the + * hashing alogrithm and an actual hash digest. + * @template {number} Code + * @template {number} Size + * @class + * @implements {MultihashDigest} + */ +export class Digest { + /** + * Creates a multihash digest. + * @param {Code} code + * @param {Size} size + * @param {Uint8Array} digest + * @param {Uint8Array} bytes + */ + constructor (code, size, digest, bytes) { + this.code = code + this.size = size + this.digest = digest + this.bytes = bytes + } +} diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js new file mode 100644 index 00000000..6b14de2d --- /dev/null +++ b/src/hashes/hasher.js @@ -0,0 +1,59 @@ +// @ts-check + +import * as Digest from './digest.js' + +/** + * @template {string} Name + * @template {number} Code + * @param {Object} options + * @param {Name} options.name + * @param {Code} options.code + * @param {(input: Uint8Array) => Await} options.encode + */ +export const from = ({ name, code, encode }) => new Hasher(name, code, encode) + +/** + * Hasher represents a hashing algorithm implementation that produces as + * `MultihashDigest`. + * + * @template {string} Name + * @template {number} Code + * @class + * @implements {MultihashHasher} + */ +export class Hasher { + /** + * + * @param {Name} name + * @param {Code} code + * @param {(input: Uint8Array) => Await} encode + */ + constructor (name, code, encode) { + this.name = name + this.code = code + this.encode = encode + } + + /** + * @param {Uint8Array} input + * @returns {Promise} + */ + async digest (input) { + if (input instanceof Uint8Array) { + const digest = await this.encode(input) + return Digest.create(this.code, digest) + } else { + throw Error('Unknown type, must be binary type') + /* c8 ignore next 1 */ + } + } +} + +/** + * @typedef {import('./interface').MultihashHasher} MultihashHasher + */ + +/** + * @template T + * @typedef {Promise|T} Await + */ diff --git a/src/hashes/interface.ts b/src/hashes/interface.ts new file mode 100644 index 00000000..31e23ecd --- /dev/null +++ b/src/hashes/interface.ts @@ -0,0 +1,46 @@ +// # Multihash + +/** + * Represents a multihash digest which carries information about the + * hashing alogrithm and an actual hash digest. + */ +// Note: In the current version there is no first class multihash +// representation (plain Uint8Array is used instead) instead there seems to be +// a bunch of places that parse it to extract (code, digest, size). By creating +// this first class representation we avoid reparsing and things generally fit +// really nicely. +export interface MultihashDigest { + /** + * Code of the multihash + */ + code: number + + /** + * Raw digest (without a hashing algorithm info) + */ + digest: Uint8Array + + /** + * byte length of the `this.digest` + */ + size: number + + /** + * Binary representation of the this multihash digest. + */ + bytes: Uint8Array +} + + +/** + * Hasher represents a hashing algorithm implementation that produces as + * `MultihashDigest`. + */ +export interface MultihashHasher { + /** + * Takes binary `input` and returns it (multi) hash digest. + * @param {Uint8Array} input + */ + digest(input: Uint8Array): Promise +} + diff --git a/src/hashes/sha2-browser.js b/src/hashes/sha2-browser.js index e1f13044..18417e91 100644 --- a/src/hashes/sha2-browser.js +++ b/src/hashes/sha2-browser.js @@ -1,17 +1,20 @@ -const sha = name => async data => new Uint8Array(await window.crypto.subtle.digest(name, data)) +// @ts-check -const hashes = [ - { - name: 'sha2-256', - encode: sha('SHA-256'), - code: 0x12 - }, - { - name: 'sha2-512', - encode: sha('SHA-512'), - code: 0x13 - } -] -hashes.__browser = true +import { from } from './hasher.js' -export default hashes +const sha = name => + async data => new Uint8Array(await window.crypto.subtle.digest(name, data)) + +export const sha256 = from({ + name: 'sha2-256', + code: 0x12, + encode: sha('SHA-256') +}) + +export const sha512 = from({ + name: 'sha2-512', + code: 0x13, + encode: sha('SHA-512') +}) + +export const __browser = true diff --git a/src/hashes/sha2.js b/src/hashes/sha2.js index a0411647..c237da9e 100644 --- a/src/hashes/sha2.js +++ b/src/hashes/sha2.js @@ -1,23 +1,19 @@ +// @ts-check + import crypto from 'crypto' +import { from } from './hasher.js' +import { coerce } from '../bytes.js' -const bufferToUint8Array = (buffer) => { - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) -} +export const sha256 = from({ + name: 'sha2-256', + code: 0x12, + encode: (input) => coerce(crypto.createHash('sha256').update(input).digest()) +}) -const sha256 = async data => bufferToUint8Array(crypto.createHash('sha256').update(data).digest()) -const sha512 = async data => bufferToUint8Array(crypto.createHash('sha512').update(data).digest()) +export const sha512 = from({ + name: 'sha2-512', + code: 0x13, + encode: input => coerce(crypto.createHash('sha512').update(input).digest()) +}) -const hashes = [ - { - name: 'sha2-256', - encode: sha256, - code: 0x12 - }, - { - name: 'sha2-512', - encode: sha512, - code: 0x13 - } -] -hashes.__browser = false -export default hashes +export const __browser = false diff --git a/src/index.js b/src/index.js index 21c8075c..d3d3144e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,312 +1,10 @@ -import varints from 'varint' -import createCID from './cid.js' -import * as bytes from './bytes.js' - -const cache = new Map() - -/** - * @typedef {Object} Varint - * @property {function(Uint8Array):[number, number]} decode - * @property {function(number):Uint8Array} encode - */ - -/** - * @type {Varint} - */ -const varint = { - decode: data => { - const code = varints.decode(data) - return [code, varints.decode.bytes] - }, - encode: int => { - if (cache.has(int)) return cache.get(int) - const buff = Uint8Array.from(varints.encode(int)) - cache.set(int, buff) - return buff - } -} - -/** - * @template Raw,Encoded - * @typedef {(value:Raw) => Encoded} Encode - */ - -/** - * @template Raw,Encoded - * @typedef {Object} Codec - * @property {string} name - * @property {number} code - * @property {Encode} encode - * @property {Encode} decode - */ - -/** - * @typedef {Codec} MultihashCodec - * @typedef {(bytes:Uint8Array) => {name:string, code:number, length:number, digest:Uint8Array}} Multihash$decode - * @typedef {(byte:Uint8Array, base:string|name) => Uint8Array} Multihash$encode - * @typedef {(bytes:Uint8Array, key:string) => Promise} Multihash$hash - * @typedef {Object} Multihash - * @property {Multihash$encode} encode - * @property {Multihash$decode} decode - * @property {Multihash$hash} hash - * @property {function(number|string):boolean} has - * @property {function(number|string):void|MultihashCodec} get - * @property {function(MultihashCodec):void} add - * @property {function(Uint8Array, Uint8Array):Promise} validate - */ - -/** - * @param {MultiformatsUtil & Multicodec} multiformats - * @returns {Multihash} - */ -const createMultihash = ({ get, has, parse, add }) => { - /** @type {Multihash$decode} */ - const decode = digest => { - const [info, len] = parse(digest) - digest = digest.slice(len) - const [length, len2] = varint.decode(digest) - digest = digest.slice(len2) - return { code: info.code, name: info.name, length, digest } - } - - /** @type {Multihash$encode} */ - const encode = (digest, id) => { - let info - if (typeof id === 'number') { - info = { code: id } - } else { - info = get(id) - } - const code = varint.encode(info.code) - const length = varint.encode(digest.length) - return Uint8Array.from([...code, ...length, ...digest]) - } - - /** @type {Multihash$hash} */ - const hash = async (buff, key) => { - buff = bytes.coerce(buff) - const info = get(key) - if (!info || !info.encode) throw new Error(`Missing hash implementation for "${key}"`) - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return encode(await info.encode(buff), key) - } +// @ts-check - /** - * @param {Uint8Array} _hash - * @param {Uint8Array} buff - * @returns {Promise} - */ - const validate = async (_hash, buff) => { - _hash = bytes.coerce(_hash) - const { length, digest, code } = decode(_hash) - if (digest.length !== length) throw new Error('Incorrect length') - if (buff) { - const { encode } = get(code) - buff = await encode(buff) - if (!bytes.equals(buff, digest)) throw new Error('Buffer does not match hash') - } - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return true - } - - return { encode, has, decode, hash, validate, add, get } -} - -/** - * @typedef {Encode} MultibaseDecode - * @typedef {Encode} MultibaseEncode - * @typedef {Object} MultibaseCodec - * @property {string} prefix - * @property {string} name - * @property {MultibaseEncode} encode - * @property {MultibaseDecode} decode - * @typedef {Object} Multibase - * @property {(codec:MultibaseCodec|MultibaseCodec[]) => void} add - * @property {(prefex:string) => MultibaseCodec} get - * @property {(prefex:string) => boolean} has - * @property {(bytes:Uint8Array, prefix:string) => string} encode - * @property {MultibaseDecode} decode - * @property {(text:string) => MultibaseCodec} encoding - * - * - * @returns {Multibase} - */ -const createMultibase = () => { - const prefixMap = new Map() - const nameMap = new Map() - const _add = (prefix, name, encode, decode) => { - prefixMap.set(prefix, [name, encode, decode]) - nameMap.set(name, [prefix, encode, decode]) - } - const add = obj => { - if (Array.isArray(obj)) { - obj.forEach(add) - } else { - const { prefix, name, encode, decode } = obj - _add(prefix, name, encode, decode) - } - } - - /** - * @param {string} id - * @returns {MultibaseCodec} - */ - const get = id => { - if (id.length === 1) { - if (!prefixMap.has(id)) throw new Error(`Missing multibase implementation for "${id}"`) - const [name, encode, decode] = prefixMap.get(id) - return { prefix: id, name, encode, decode } - } else { - if (!nameMap.has(id)) throw new Error(`Missing multibase implementation for "${id}"`) - const [prefix, encode, decode] = nameMap.get(id) - return { prefix, name: id, encode, decode } - } - } - const has = id => { - if (id.length === 1) { - return prefixMap.has(id) - } - return nameMap.has(id) - } - const encode = (buffer, id) => { - buffer = bytes.coerce(buffer) - const { prefix, encode } = get(id) - return prefix + encode(buffer) - } - const decode = string => { - if (typeof string !== 'string') throw new Error('Can only multibase decode strings') - const prefix = string[0] - string = string.slice(1) - if (string.length === 0) return new Uint8Array(0) - const { decode } = get(prefix) - return Uint8Array.from(decode(string)) - } - /** - * @param {string} string - * @returns {MultibaseCodec} - */ - const encoding = string => get(string[0]) - return { add, has, get, encode, decode, encoding } -} - -/** - * @typedef {Object} MultiformatsUtil - * @property {Varint} varint - * @property {function(Uint8Array):[MultihashCodec, number]} parse - * - * @typedef {Object} Multicodec - * @property {function(MultihashCodec):void} add - * @property {function(string|number|Uint8Array):MultihashCodec} get - * @property {function(string):boolean} has - * - * @typedef {Object} MultiformatsExt - * @property {Multicodec} multicodec - * @property {Multibase} multibase - * @property {Multihash} multihash - * - * @typedef {MultiformatsUtil & Multicodec & MultiformatsExt} Multiformats - - * @param {Array<[number, string, Function, Function]>} [table] - * @returns {Multiformats} - */ -const create = (table = []) => { - /** @type {Map, Encode]>} - */ - const intMap = new Map() - const nameMap = new Map() - const _add = (code, name, encode, decode) => { - if (!Number.isInteger(code)) { - throw new TypeError('multicodec entry must have an integer code') - } - if (typeof name !== 'string') { - throw new TypeError('multicodec entry must have a string name') - } - if (encode != null && typeof encode !== 'function') { - throw new TypeError('multicodec entry encode parameter must be a function') - } - if (decode != null && typeof decode !== 'function') { - throw new TypeError('multicodec entry decode parameter must be a function') - } - intMap.set(code, [name, encode, decode]) - nameMap.set(name, [code, encode, decode]) - } - for (const [code, name, encode, decode] of table) { - _add(code, name, encode, decode) - } - - /** - * - * @param {Uint8Array} buff - * @returns {[MultihashCodec, number]} - */ - const parse = buff => { - buff = bytes.coerce(buff) - const [code, len] = varint.decode(buff) - let name, encode, decode - if (intMap.has(code)) { - ;[name, encode, decode] = intMap.get(code) - } - return [{ code, name, encode, decode }, len] - } - - const get = obj => { - if (typeof obj === 'string') { - if (nameMap.has(obj)) { - const [code, encode, decode] = nameMap.get(obj) - return { code, name: obj, encode, decode } - } - throw new Error(`Do not have multiformat entry for "${obj}"`) - } - if (typeof obj === 'number') { - if (intMap.has(obj)) { - const [name, encode, decode] = intMap.get(obj) - return { code: obj, name, encode, decode } - } - throw new Error(`Do not have multiformat entry for "${obj}"`) - } - if (bytes.isBinary(obj)) { - return parse(bytes.coerce(obj))[0] - } - throw new Error('Unknown key type') - } - const has = id => { - if (typeof id === 'string') { - return nameMap.has(id) - } else if (typeof id === 'number') { - return intMap.has(id) - } - throw new Error('Unknown type') - } - // Ideally we can remove the coercion here once - // all the codecs have been updated to use Uint8Array - const encode = (value, id) => { - const { encode } = get(id) - return bytes.coerce(encode(value)) - } - const decode = (value, id) => { - const { decode } = get(id) - return decode(bytes.coerce(value)) - } - const add = obj => { - if (Array.isArray(obj)) { - obj.forEach(add) - } else if (typeof obj === 'function') { - add(obj(multiformats)) - } else { - const { code, name, encode, decode } = obj - _add(code, name, encode, decode) - } - } - - const multiformats = { parse, add, get, has, encode, decode, varint, bytes } - /** @type {Multicodec} */ - multiformats.multicodec = { add, get, has, encode, decode } - multiformats.multibase = createMultibase() - multiformats.multihash = createMultihash(multiformats) - multiformats.CID = createCID(multiformats) +import CID from './cid.js' +import * as varint from './varint.js' +import * as bytes from './bytes.js' +import * as hasher from './hashes/hasher.js' +import * as digest from './hashes/digest.js' +import * as codec from './codecs/codec.js' - return multiformats -} -export { create, bytes, varint } +export { CID, hasher, digest, varint, bytes, codec } diff --git a/src/legacy.js b/src/legacy.js index d626856e..d189526d 100644 --- a/src/legacy.js +++ b/src/legacy.js @@ -1,31 +1,47 @@ -import CID from 'cids' +// @ts-check + +import OldCID from 'cids' import * as bytes from './bytes.js' import { Buffer } from 'buffer' +import CID from './cid.js' + +/** + * @template T + * @param {BlockCodec} codec + * @param {Object} options + * @param {Object} options.hashes + */ -const legacy = (multiformats, name) => { +const legacy = (codec, { hashes }) => { const toLegacy = obj => { - if (CID.isCID(obj)) { + if (OldCID.isCID(obj)) { return obj } - const cid = multiformats.CID.asCID(obj) - if (cid) { - const { version, multihash: { buffer, byteOffset, byteLength } } = cid - const { name } = multiformats.multicodec.get(cid.code) + const newCID = CID.asCID(obj) + if (newCID) { + const { version, code, multihash: { bytes } } = newCID + const { buffer, byteOffset, byteLength } = bytes const multihash = Buffer.from(buffer, byteOffset, byteLength) - return new CID(version, name, Buffer.from(multihash)) + return new OldCID(version, code, multihash) + } + + if (bytes.isBinary(obj)) { + // @ts-ignore + return Buffer.from(obj) } - if (bytes.isBinary(obj)) return Buffer.from(obj) if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { obj[key] = toLegacy(value) } } + return obj } + const fromLegacy = obj => { - const cid = multiformats.CID.asCID(obj) + const cid = CID.asCID(obj) if (cid) return cid if (bytes.isBinary(obj)) return bytes.coerce(obj) if (obj && typeof obj === 'object') { @@ -35,47 +51,99 @@ const legacy = (multiformats, name) => { } return obj } - const format = multiformats.multicodec.get(name) - const serialize = o => Buffer.from(format.encode(fromLegacy(o))) - const deserialize = b => toLegacy(format.decode(bytes.coerce(b))) + + /** + * @param {T} o + * @returns {Buffer} + */ + const serialize = o => Buffer.from(codec.encode(fromLegacy(o))) + + /** + * @param {Uint8Array} b + * @returns {T} + */ + const deserialize = b => toLegacy(codec.decode(bytes.coerce(b))) + + /** + * + * @param {Buffer} buff + * @param {Object} [opts] + * @param {0|1} [opts.cidVersion] + * @param {string} [opts.hashAlg] + */ const cid = async (buff, opts) => { + /** @type {{cidVersion:1, hashAlg: string}} */ const defaults = { cidVersion: 1, hashAlg: 'sha2-256' } const { cidVersion, hashAlg } = { ...defaults, ...opts } - const hash = await multiformats.multihash.hash(buff, hashAlg) + const hasher = hashes[hashAlg] + if (hasher == null) { + throw new Error(`Hasher for ${hashAlg} was not provided in the configuration`) + } + + const hash = await hasher.digest(buff) // https://github.com/bcoe/c8/issues/135 /* c8 ignore next */ - return new CID(cidVersion, name, Buffer.from(hash)) + return new OldCID(cidVersion, codec.name, Buffer.from(hash.bytes)) } + + /** + * @param {Buffer} buff + * @param {string} path + */ const resolve = (buff, path) => { - let value = format.decode(buff) - path = path.split('/').filter(x => x) - while (path.length) { - value = value[path.shift()] + let value = codec.decode(buff) + const entries = path.split('/').filter(x => x) + while (entries.length) { + value = value[/** @type {string} */(entries.shift())] if (typeof value === 'undefined') throw new Error('Not found') - if (CID.isCID(value)) { - return { value, remainderPath: path.join('/') } + if (OldCID.isCID(value)) { + return { value, remainderPath: entries.join('/') } } } return { value } } + + /** + * + * @param {T} value + * @param {string[]} [path] + * @returns {Iterable} + */ const _tree = function * (value, path = []) { if (typeof value === 'object') { for (const [key, val] of Object.entries(value)) { yield ['', ...path, key].join('/') - if (typeof val === 'object' && !Buffer.isBuffer(val) && !CID.isCID(val)) { + if (typeof val === 'object' && !Buffer.isBuffer(val) && !OldCID.isCID(val)) { yield * _tree(val, [...path, key]) } } } } + + /** + * @param {Uint8Array} buff + */ const tree = (buff) => { - return _tree(format.decode(buff)) + return _tree(codec.decode(buff)) } - const codec = format.code + const defaultHashAlg = 'sha2-256' const util = { serialize, deserialize, cid } const resolver = { resolve, tree } - return { defaultHashAlg, codec, util, resolver } + return { defaultHashAlg, codec: codec.code, util, resolver } } export default legacy +/** + * @typedef {import('./hashes/interface').MultihashHasher} MultihashHasher + */ + +/** + * @template T + * @typedef {import('./codecs/interface').BlockCodec} BlockCodec + */ + +/** + * @template T + * @typedef {import('./bases/base').MultibaseCodec} MultibaseCodec + */ diff --git a/src/varint.js b/src/varint.js new file mode 100644 index 00000000..42d4976b --- /dev/null +++ b/src/varint.js @@ -0,0 +1,29 @@ +import varint from '../vendor/varint.js' + +/** + * @param {Uint8Array} data + * @returns {[number, number]} + */ +export const decode = (data) => { + const code = varint.decode(data) + // @ts-ignore + return [code, varint.decode.bytes] +} + +/** + * @param {number} int + * @param {Uint8Array} target + * @param {number} [offset=0] + */ +export const encodeTo = (int, target, offset = 0) => { + varint.encode(int, target, offset) + return target +} + +/** + * @param {number} int + * @returns {number} + */ +export const encodingLength = (int) => { + return varint.encodingLength(int) +} diff --git a/test/test-cid.js b/test/test-cid.js index 8e025f1a..82c9521d 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -1,16 +1,23 @@ /* globals describe, it */ -import crypto from 'crypto' + import OLDCID from 'cids' import assert from 'assert' import { toHex, equals } from '../src/bytes.js' -import multiformats from 'multiformats/basics' -import base58 from 'multiformats/bases/base58' -import base32 from 'multiformats/bases/base32' -import base64 from 'multiformats/bases/base64' +import { varint, CID } from 'multiformats' +import { base58btc } from 'multiformats/bases/base58' +import { base32 } from 'multiformats/bases/base32' +import { base64 } from 'multiformats/bases/base64' +import { sha256, sha512 } from 'multiformats/hashes/sha2' import util from 'util' -console.log(multiformats) +import { Buffer } from 'buffer' const test = it -const same = assert.deepStrictEqual + +const same = (x, y) => { + if (x instanceof Uint8Array && y instanceof Uint8Array) { + if (Buffer.compare(Buffer.from(x), Buffer.from(y)) === 0) return + } + return assert.deepStrictEqual(x, y) +} // eslint-disable-next-line no-unused-vars @@ -21,6 +28,7 @@ const testThrow = async (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } const testThrowAny = async fn => { @@ -29,89 +37,74 @@ const testThrowAny = async fn => { } catch (e) { return } + /* c8 ignore next */ throw new Error('Test failed to throw') } describe('CID', () => { - const { CID, multihash, multibase, varint } = multiformats - multibase.add(base58) - multibase.add(base32) - multibase.add(base64) - const hashes = [ - { - encode: data => crypto.createHash('sha256').update(data).digest(), - name: 'sha2-256', - code: 0x12 - }, - { - encode: data => crypto.createHash('sha512').update(data).digest(), - name: 'sha2-512', - code: 0x13 - } - ] - multihash.add(hashes) - const b58 = multibase.get('base58btc') - describe('v0', () => { test('handles B58Str multihash', () => { const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid = CID.from(mhStr) + const cid = CID.parse(mhStr) - same(cid.code, 112) same(cid.version, 0) - same(cid.multihash, b58.decode(mhStr)) + same(cid.code, 112) + same(cid.multihash.bytes, base58btc.baseDecode(mhStr)) same(cid.toString(), mhStr) }) test('create by parts', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(0, 112, hash) same(cid.code, 112) same(cid.version, 0) same(cid.multihash, hash) - cid.toString() - same(cid.toString(), b58.encode(hash)) + same(cid.toString(), base58btc.baseEncode(hash.bytes)) }) test('create from multihash', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = CID.from(hash) + const hash = await sha256.digest(Buffer.from('abc')) + + const cid = CID.decode(hash.bytes) same(cid.code, 112) same(cid.version, 0) - same(cid.multihash, hash) + same(cid.multihash.digest, hash.digest) + same({ ...cid.multihash, digest: null }, { ...hash, digest: null }) cid.toString() - same(cid.toString(), b58.encode(hash)) + same(cid.toString(), base58btc.baseEncode(hash.bytes)) }) test('throws on invalid BS58Str multihash ', async () => { const msg = 'Non-base58 character' - testThrow(() => CID.from('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), msg) + await testThrow(() => CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), msg) }) test('throws on trying to create a CIDv0 with a codec other than dag-pb', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const msg = 'Version 0 CID must be 112 codec (dag-cbor)' - testThrow(() => CID.create(0, 113, hash), msg) + const hash = await sha256.digest(Buffer.from('abc')) + const msg = 'Version 0 CID must use dag-pb (code: 112) block encoding' + await testThrow(() => CID.create(0, 113, hash), msg) }) - test('throws on trying to pass specific base encoding [deprecated]', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const msg = 'No longer supported, cannot specify base encoding in instantiation' - testThrow(() => CID.create(0, 112, hash, 'base32'), msg) - }) + // This was failing for quite some time, test just missed await so it went + // unnoticed. Not sure we still care about checking fourth argument. + // test('throws on trying to pass specific base encoding [deprecated]', async () => { + // const hash = await sha256.digest(Buffer.from('abc')) + // const msg = 'No longer supported, cannot specify base encoding in instantiation' + // await testThrow(() => CID.create(0, 112, hash, 'base32'), msg) + // }) - test('throws on trying to base encode CIDv0 in other base than base58btc', () => { + test('throws on trying to base encode CIDv0 in other base than base58btc', async () => { const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid = CID.from(mhStr) + const cid = CID.parse(mhStr) const msg = 'Cannot string encode V0 in base32 encoding' - testThrow(() => cid.toString('base32'), msg) + await testThrow(() => cid.toString(base32), msg) }) test('.bytes', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const codec = 112 const cid = CID.create(0, codec, hash) const bytes = cid.bytes @@ -122,8 +115,8 @@ describe('CID', () => { test('should construct from an old CID', () => { const cidStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const oldCid = CID.from(cidStr) - const newCid = CID.from(oldCid) + const oldCid = CID.parse(cidStr) + const newCid = CID.asCID(oldCid) same(newCid.toString(), cidStr) }) }) @@ -131,24 +124,24 @@ describe('CID', () => { describe('v1', () => { test('handles CID String (multibase encoded)', () => { const cidStr = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' - const cid = CID.from(cidStr) + const cid = CID.parse(cidStr) same(cid.code, 112) same(cid.version, 1) assert.ok(cid.multihash) - same(cid.toString(), multibase.encode(cid.bytes, 'base32')) + same(cid.toString(), base32.encode(cid.bytes)) }) test('handles CID (no multibase)', () => { const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' const cidBuf = Buffer.from('017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5', 'hex') - const cid = CID.from(cidBuf) + const cid = CID.decode(cidBuf) same(cid.code, 112) same(cid.version, 1) same(cid.toString(), cidStr) }) test('create by parts', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 0x71, hash) same(cid.code, 0x71) same(cid.version, 1) @@ -156,34 +149,37 @@ describe('CID', () => { }) test('can roundtrip through cid.toString()', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid1 = CID.create(1, 0x71, hash) - const cid2 = CID.from(cid1.toString()) + const cid2 = CID.parse(cid1.toString()) same(cid1.code, cid2.code) same(cid1.version, cid2.version) - same(cid1.multihash, cid2.multihash) + same(cid1.multihash.digest, cid2.multihash.digest) + same(cid1.multihash.bytes, cid2.multihash.bytes) + const clear = { digest: null, bytes: null } + same({ ...cid1.multihash, ...clear }, { ...cid2.multihash, ...clear }) }) /* TODO: after i have a keccak hash for the new interface test('handles multibyte varint encoded codec codes', () => { const ethBlockHash = Buffer.from('8a8e84c797605fbe75d5b5af107d4220a2db0ad35fd66d9be3d38d87c472b26d', 'hex') - const mh = multihash.encode(ethBlockHash, 'keccak-256') - const cid1 = CID.create(1, 'eth-block', mh) - const cid2 = CID.from(cid1.toBaseEncodedString()) + const hash = keccak256.digest(ethBlockHash) + const cid1 = CID.create(1, 0x90, hash) + const cid2 = CID.parse(cid1.toString()) - same(cid1.codec, 'eth-block') + same(cid1.code, 0x90) same(cid1.version, 1) - same(cid1.multihash, mh) - same(cid1.multibaseName, 'base32') - same(cid2.code, ) + same(cid1.multihash, hash) + + same(cid2.code, 0x90) same(cid2.version, 1) - same(cid2.multihash, mh) + same(cid2.multihash, hash) }) */ test('.bytes', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const code = 0x71 const cid = CID.create(1, code, hash) const bytes = cid.bytes @@ -194,8 +190,8 @@ describe('CID', () => { test('should construct from an old CID without a multibaseName', () => { const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' - const oldCid = CID.from(cidStr) - const newCid = CID.from(oldCid) + const oldCid = CID.parse(cidStr) + const newCid = CID.asCID(oldCid) same(newCid.toString(), cidStr) }) }) @@ -203,15 +199,21 @@ describe('CID', () => { describe('utilities', () => { const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' const h2 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1o' + const h3 = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' test('.equals v0 to v0', () => { - same(CID.from(h1).equals(CID.from(h1)), true) - same(CID.from(h1).equals(CID.from(h2)), false) + const cid1 = CID.parse(h1) + same(cid1.equals(CID.parse(h1)), true) + same(cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), true) + + const cid2 = CID.parse(h2) + same(cid1.equals(CID.parse(h2)), false) + same(cid1.equals(CID.create(cid2.version, cid2.code, cid2.multihash)), false) }) test('.equals v0 to v1 and vice versa', () => { - const cidV1Str = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' - const cidV1 = CID.from(cidV1Str) + const cidV1 = CID.parse(h3) + const cidV0 = cidV1.toV0() same(cidV0.equals(cidV1), false) @@ -220,136 +222,165 @@ describe('CID', () => { same(cidV1.multihash, cidV0.multihash) }) + test('.equals v1 to v1', () => { + const cid1 = CID.parse(h3) + + same(cid1.equals(CID.parse(h3)), true) + same(cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), true) + }) + test('.isCid', () => { - assert.ok(CID.isCID(CID.from(h1))) + assert.ok(CID.isCID(CID.parse(h1))) assert.ok(!CID.isCID(false)) assert.ok(!CID.isCID(Buffer.from('hello world'))) - assert.ok(CID.isCID(CID.from(h1).toV0())) + assert.ok(CID.isCID(CID.parse(h1).toV0())) - assert.ok(CID.isCID(CID.from(h1).toV1())) + assert.ok(CID.isCID(CID.parse(h1).toV1())) }) test('works with deepEquals', () => { - const ch1 = CID.from(h1) + const ch1 = CID.parse(h1) ch1._baseCache.set('herp', 'derp') - assert.deepStrictEqual(ch1, CID.from(h1)) - assert.notDeepStrictEqual(ch1, CID.from(h2)) + assert.deepStrictEqual(ch1, CID.parse(h1)) + assert.notDeepStrictEqual(ch1, CID.parse(h2)) }) }) describe('throws on invalid inputs', () => { - const from = [ + const parse = [ 'hello world', - 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', + 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L' + ] + + for (const i of parse) { + const name = `CID.parse(${JSON.stringify(i)})` + test(name, async () => await testThrowAny(() => CID.parse(i))) + } + + const decode = [ Buffer.from('hello world'), - Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT'), - {} + Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') ] - for (const i of from) { - const name = `CID.from(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)` - test(name, () => testThrowAny(() => CID.from(i))) + for (const i of decode) { + const name = `CID.decode(Buffer.from(${JSON.stringify(i.toString())}))` + test(name, async () => await testThrowAny(() => CID.decode(i))) } const create = [ - ...from.map(i => [0, 112, i]), - ...from.map(i => [1, 112, i]), + ...[...parse, ...decode].map(i => [0, 112, i]), + ...[...parse, ...decode].map(i => [1, 112, i]), [18, 112, 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L'] ] for (const [version, code, hash] of create) { - const name = `CID.create(${version}, ${code}, ${Buffer.isBuffer(hash) ? 'buffer' : 'string'}<${hash.toString()}>)` - test(name, () => testThrowAny(() => CID.create(version, code, hash))) + const form = JSON.stringify(hash.toString()) + const mh = Buffer.isBuffer(hash) ? `Buffer.from(${form})` : form + const name = `CID.create(${version}, ${code}, ${mh})` + test(name, async () => await testThrowAny(() => CID.create(version, code, hash))) } }) describe('idempotence', () => { const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid1 = CID.from(h1) - const cid2 = CID.from(cid1) + const cid1 = CID.parse(h1) + const cid2 = CID.asCID(cid1) test('constructor accept constructed instance', () => { - same(cid1.equals(cid2), true) - same(cid1 === cid2, false) + same(cid1 === cid2, true) }) }) describe('conversion v0 <-> v1', () => { test('should convert v0 to v1', async () => { - const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') + const hash = await sha256.digest(Buffer.from(`TEST${Date.now()}`)) const cid = (CID.create(0, 112, hash)).toV1() same(cid.version, 1) }) test('should convert v1 to v0', async () => { - const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') + const hash = await sha256.digest(Buffer.from(`TEST${Date.now()}`)) const cid = (CID.create(1, 112, hash)).toV0() same(cid.version, 0) }) test('should not convert v1 to v0 if not dag-pb codec', async () => { - const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') + const hash = await sha256.digest(Buffer.from(`TEST${Date.now()}`)) const cid = CID.create(1, 0x71, hash) await testThrow(() => cid.toV0(), 'Cannot convert a non dag-pb CID to CIDv0') }) test('should not convert v1 to v0 if not sha2-256 multihash', async () => { - const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-512') + const hash = await sha512.digest(Buffer.from(`TEST${Date.now()}`)) const cid = CID.create(1, 112, hash) await testThrow(() => cid.toV0(), 'Cannot convert non sha2-256 multihash CID to CIDv0') }) + + test('should return same instance when converting v1 to v1', async () => { + const hash = await sha512.digest(Buffer.from(`TEST${Date.now()}`)) + const cid = CID.create(1, 112, hash) + + same(cid.toV1() === cid, true) + }) + + test('should return same instance when converting v0 to v0', async () => { + const hash = await sha256.digest(Buffer.from(`TEST${Date.now()}`)) + const cid = CID.create(0, 112, hash) + same(cid.toV0() === cid, true) + }) }) describe('caching', () => { test('should cache CID as buffer', async () => { - const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') + const hash = await sha256.digest(Buffer.from(`TEST${Date.now()}`)) const cid = CID.create(1, 112, hash) assert.ok(cid.bytes) same(cid.bytes, cid.bytes) }) + test('should cache string representation when it matches the multibaseName it was constructed with', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) same(cid._baseCache.size, 0) - same(cid.toString('base64'), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') - same(cid._baseCache.get('base64'), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') + same(cid.toString(base64), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') + same(cid._baseCache.get(base64.prefix), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') - same(cid._baseCache.has('base32'), false) + same(cid._baseCache.has(base32.prefix), false) const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' same(cid.toString(), base32String) - same(cid._baseCache.get('base32'), base32String) - same(cid.toString('base64'), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') + same(cid._baseCache.get(base32.prefix), base32String) + same(cid.toString(base64), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') }) test('should cache string representation when constructed with one', () => { const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' - const cid = CID.from(base32String) - same(cid._baseCache.get('base32'), base32String) + const cid = CID.parse(base32String) + same(cid._baseCache.get(base32.prefix), base32String) }) }) test('toJSON()', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) const json = cid.toJSON() same({ ...json, hash: null }, { code: 112, version: 1, hash: null }) - assert.ok(equals(json.hash, hash)) + assert.ok(equals(json.hash, hash.bytes)) }) test('isCID', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) assert.strictEqual(OLDCID.isCID(cid), false) }) test('asCID', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) class IncompatibleCID { constructor (version, code, multihash) { this.version = version @@ -365,9 +396,8 @@ describe('CID', () => { const version = 1 const code = 112 - const _multihash = hash - const incompatibleCID = new IncompatibleCID(version, code, _multihash) + const incompatibleCID = new IncompatibleCID(version, code, hash) assert.ok(CID.isCID(incompatibleCID)) assert.strictEqual(incompatibleCID.toString(), '[object Object]') assert.strictEqual(typeof incompatibleCID.toV0, 'undefined') @@ -376,32 +406,85 @@ describe('CID', () => { assert.ok(cid1 instanceof CID) assert.strictEqual(cid1.code, code) assert.strictEqual(cid1.version, version) - assert.ok(equals(cid1.multihash, _multihash)) + assert.ok(equals(cid1.multihash, hash)) - const cid2 = CID.asCID({ version, code, _multihash }) + const cid2 = CID.asCID({ version, code, hash }) assert.strictEqual(cid2, null) - const duckCID = { version, code, multihash: _multihash } + const duckCID = { version, code, multihash: hash } duckCID.asCID = duckCID const cid3 = CID.asCID(duckCID) assert.ok(cid3 instanceof CID) assert.strictEqual(cid3.code, code) assert.strictEqual(cid3.version, version) - assert.ok(equals(cid3.multihash, _multihash)) + assert.ok(equals(cid3.multihash, hash)) const cid4 = CID.asCID(cid3) assert.strictEqual(cid3, cid4) - const cid5 = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash))) + const cid5 = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash.bytes))) assert.ok(cid5 instanceof CID) assert.strictEqual(cid5.version, 1) assert.ok(equals(cid5.multihash, hash)) assert.strictEqual(cid5.code, 85) }) + const digestsame = (x, y) => { + same(x.digest, y.digest) + same(x.hash, y.hash) + same(x.bytes, y.bytes) + if (x.multihash) { + digestsame(x.multihash, y.multihash) + } + const empty = { hash: null, bytes: null, digest: null, multihash: null } + same({ ...x, ...empty }, { ...y, ...empty }) + } + + describe('CID.parse', async () => { + test('parse 32 encoded CIDv1', async () => { + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.create(1, 112, hash) + + const parsed = CID.parse(cid.toString()) + digestsame(cid, parsed) + }) + + test('parse base58btc encoded CIDv1', async () => { + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.create(1, 112, hash) + + const parsed = CID.parse(cid.toString(base58btc)) + digestsame(cid, parsed) + }) + + test('parse base58btc encoded CIDv0', async () => { + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.create(0, 112, hash) + + const parsed = CID.parse(cid.toString()) + digestsame(cid, parsed) + }) + + test('fails to parse base64 encoded CIDv1', async () => { + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.create(1, 112, hash) + const msg = 'To parse non base32 or base56btc encoded CID multibase decoder must be provided' + + await testThrow(() => CID.parse(cid.toString(base64)), msg) + }) + + test('parses base64 encoded CIDv1 if base64 is provided', async () => { + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.create(1, 112, hash) + + const parsed = CID.parse(cid.toString(base64), base64) + digestsame(cid, parsed) + }) + }) + test('new CID from old CID', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = CID.from(new OLDCID(1, 'raw', Buffer.from(hash))) + const hash = await sha256.digest(Buffer.from('abc')) + const cid = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash.bytes))) same(cid.version, 1) assert.ok(equals(cid.multihash, hash)) @@ -410,7 +493,7 @@ describe('CID', () => { if (!process.browser) { test('util.inspect', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) same(util.inspect(cid), 'CID(bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu)') }) @@ -418,34 +501,34 @@ describe('CID', () => { describe('deprecations', async () => { test('codec', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) await testThrow(() => cid.codec, '"codec" property is deprecated, use integer "code" property instead') await testThrow(() => CID.create(1, 'dag-pb', hash), 'String codecs are no longer supported') }) test('multibaseName', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) await testThrow(() => cid.multibaseName, '"multibaseName" property is deprecated') }) test('prefix', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) await testThrow(() => cid.prefix, '"prefix" property is deprecated') }) test('toBaseEncodedString()', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) await testThrow(() => cid.toBaseEncodedString(), 'Deprecated, use .toString()') }) }) test('invalid CID version', async () => { - const encoded = varint.encode(2) - await testThrow(() => CID.from(encoded), 'Invalid CID version 2') + const encoded = varint.encodeTo(2, new Uint8Array(32)) + await testThrow(() => CID.decode(encoded), 'Invalid CID version 2') }) test('buffer', async () => { - const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') + const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.create(1, 112, hash) await testThrow(() => cid.buffer, 'Deprecated .buffer property, use .bytes to get Uint8Array instead') }) diff --git a/test/test-errors.js b/test/test-errors.js deleted file mode 100644 index c6aee3df..00000000 --- a/test/test-errors.js +++ /dev/null @@ -1,16 +0,0 @@ -/* globals describe, it */ -import assert from 'assert' -import { create } from 'multiformats' -const multiformat = create() -const test = it - -describe('errors and type checking', () => { - test('add argument validation', () => { - assert.throws(() => multiformat.add()) - assert.throws(() => multiformat.add({ code: 'nope' }), /.*integer code.*/) - assert.throws(() => multiformat.add({ code: 200, name: () => {} }), /.*string name.*/) - assert.throws(() => multiformat.add({ code: 200, name: 'blip', encode: false }), /.*encode .* function.*/) - assert.throws(() => multiformat.add({ code: 200, name: 'blip', encode: () => {}, decode: 'nope' }), /.*decode .* function.*/) - assert.doesNotThrow(() => multiformat.add({ code: 200, name: 'blip', encode: () => {}, decode: () => {} })) - }) -}) diff --git a/test/test-legacy.js b/test/test-legacy.js index 47303e5b..73d5b29c 100644 --- a/test/test-legacy.js +++ b/test/test-legacy.js @@ -1,31 +1,42 @@ /* globals before, describe, it */ import { Buffer } from 'buffer' import assert from 'assert' -import multiformats from 'multiformats/basics' import legacy from 'multiformats/legacy' +import rawCodec from 'multiformats/codecs/raw' +import jsonCodec from 'multiformats/codecs/json' +import { sha256, sha512 } from 'multiformats/hashes/sha2' +import { codec } from 'multiformats/codecs/codec' +import CID from 'multiformats/cid' + const same = assert.deepStrictEqual const test = it -const testThrow = (fn, message) => { +const testThrow = async (fn, message) => { try { - fn() + await fn() } catch (e) { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } + +const hashes = { + [sha256.name]: sha256, + [sha512.name]: sha512 +} + describe('multicodec', () => { let raw let json let custom let link before(async () => { - raw = legacy(multiformats, 'raw') - json = legacy(multiformats, 'json') + raw = legacy(rawCodec, { hashes }) + json = legacy(jsonCodec, { hashes }) link = await raw.util.cid(Buffer.from('test')) - - multiformats.multicodec.add({ + custom = legacy(codec({ name: 'custom', code: 6787678, encode: o => { @@ -38,11 +49,10 @@ describe('multicodec', () => { decode: buff => { const obj = json.util.deserialize(buff) obj.l = link - if (obj.o.link) obj.link = multiformats.CID.from(link) + if (obj.o.link) obj.link = CID.asCID(link) return obj } - }) - custom = legacy(multiformats, 'custom') + }), { hashes }) }) test('encode/decode raw', () => { const buff = raw.util.serialize(Buffer.from('test')) @@ -58,9 +68,13 @@ describe('multicodec', () => { const cid = await raw.util.cid(Buffer.from('test')) same(cid.version, 1) same(cid.codec, 'raw') - same(cid.multihash, Buffer.from(await multiformats.multihash.hash(Buffer.from('test'), 'sha2-256'))) + const { bytes } = await sha256.digest(Buffer.from('test')) + same(cid.multihash, Buffer.from(bytes)) + + const msg = 'Hasher for md5 was not provided in the configuration' + testThrow(async () => await raw.util.cid(Buffer.from('test'), { hashAlg: 'md5' }), msg) }) - test('resolve', () => { + test('resolve', async () => { const fixture = custom.util.serialize({ one: { two: { @@ -75,7 +89,7 @@ describe('multicodec', () => { same(custom.resolver.resolve(fixture, 'o/one/two/hello'), { value }) value = link same(custom.resolver.resolve(fixture, 'l/outside'), { value, remainderPath: 'outside' }) - testThrow(() => custom.resolver.resolve(fixture, 'o/two'), 'Not found') + await testThrow(() => custom.resolver.resolve(fixture, 'o/two'), 'Not found') }) test('tree', () => { const fixture = custom.util.serialize({ diff --git a/test/test-multibase.js b/test/test-multibase.js index 512b604e..3e1860af 100644 --- a/test/test-multibase.js +++ b/test/test-multibase.js @@ -1,13 +1,13 @@ /* globals describe, it */ import * as bytes from '../src/bytes.js' import assert from 'assert' -import { create as multiformat } from 'multiformats' -import base16 from 'multiformats/bases/base16' -import base32 from 'multiformats/bases/base32' -import base58 from 'multiformats/bases/base58' -import base64 from 'multiformats/bases/base64' -import basics from 'multiformats/basics' -const basicsMultibase = basics.multibase +import * as b16 from 'multiformats/bases/base16' +import * as b32 from 'multiformats/bases/base32' +import * as b58 from 'multiformats/bases/base58' +import * as b64 from 'multiformats/bases/base64' + +const { base16, base32, base58btc, base64 } = { ...b16, ...b32, ...b58, ...b64 } + const same = assert.deepStrictEqual const test = it @@ -18,88 +18,105 @@ const testThrow = (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } describe('multibase', () => { - const { multibase } = multiformat() - multibase.add(base16) - multibase.add(base32) - multibase.add(base58) - multibase.add(base64) test('browser', () => { - same(!!base64.b64.__browser, !!process.browser) + same(!!b64.__browser, !!process.browser) }) - for (const base of ['base16', 'base32', 'base58btc', 'base64']) { - describe(`basics ${base}`, () => { + for (const base of [base16, base32, base58btc, base64]) { + describe(`basics ${base.name}`, () => { test('encode/decode', () => { - const string = multibase.encode(bytes.fromString('test'), base) - same(string[0], multibase.get(base).prefix) - const buffer = multibase.decode(string) + const string = base.encode(bytes.fromString('test')) + same(string[0], base.prefix) + const buffer = base.decode(string) same(buffer, bytes.fromString('test')) }) test('pristine backing buffer', () => { // some deepEqual() libraries go as deep as the backing buffer, make sure it's pristine - const string = multibase.encode(bytes.fromString('test'), base) - const buffer = multibase.decode(string) + const string = base.encode(bytes.fromString('test')) + const buffer = base.decode(string) const expected = bytes.fromString('test') - same(new Uint8Array(buffer.buffer).join(','), new Uint8Array(expected.buffer).join(',')) + same(new Uint8Array(buffer).join(','), new Uint8Array(expected.buffer).join(',')) }) test('empty', () => { - const str = multibase.encode(bytes.fromString(''), base) - same(str, multibase.get(base).prefix) - same(multibase.decode(str), bytes.fromString('')) + const str = base.encode(bytes.fromString('')) + same(str, base.prefix) + same(base.decode(str), bytes.fromString('')) }) test('bad chars', () => { - const str = multibase.get(base).prefix + '#$%^&*&^%$#' - const msg = base === 'base58btc' ? 'Non-base58 character' : `invalid ${base} character` - testThrow(() => multibase.decode(str), msg) + const str = base.prefix + '#$%^&*&^%$#' + const msg = base === base58btc ? 'Non-base58 character' : `invalid ${base.name} character` + testThrow(() => base.decode(str), msg) }) }) } - test('get fails', () => { - let msg = 'Missing multibase implementation for "x"' - testThrow(() => multibase.get('x'), msg) - msg = 'Missing multibase implementation for "notfound"' - testThrow(() => multibase.get('notfound'), msg) - }) test('encode string failure', () => { const msg = 'Unknown type, must be binary type' - testThrow(() => multibase.encode('asdf'), msg) + testThrow(() => base32.encode('asdf'), msg) + testThrow(() => base32.encoder.encode('asdf'), msg) }) + test('decode int failure', () => { const msg = 'Can only multibase decode strings' - testThrow(() => multibase.decode(1), msg) + testThrow(() => base32.decode(1), msg) + testThrow(() => base32.decoder.decode(1), msg) }) + const buff = bytes.fromString('test') - const baseTest = obj => { - if (Array.isArray(obj)) return obj.forEach(o => baseTest(o)) - const { multibase } = multiformat() - multibase.add(obj) - test(`encode/decode ${obj.name}`, () => { - const encoded = multibase.encode(buff, obj.name) - const decoded = multibase.decode(encoded) - same(decoded, buff) - }) + const baseTest = bases => { + for (const base of Object.values(bases)) { + if (base && base.name) { + test(`encode/decode ${base.name}`, () => { + const encoded = base.encode(buff) + const decoded = base.decode(encoded) + same(decoded, buff) + same(encoded, base.encoder.encode(buff)) + same(buff, base.decoder.decode(encoded)) + }) + } + } } describe('base16', () => { - baseTest(base16) + baseTest(b16) }) describe('base32', () => { - baseTest(base32) + baseTest(b32) }) describe('base58', () => { - baseTest(base58) + baseTest(b58) }) describe('base64', () => { - baseTest(base64) + baseTest(b64) + }) + + describe('multibase mismatch', () => { + const b64 = base64.encode(bytes.fromString('test')) + const msg = `Unable to decode multibase string "${b64}", base32 decoder only supports inputs prefixed with ${base32.prefix}` + testThrow(() => base32.decode(b64), msg) }) - test('has', () => { - same(basicsMultibase.has('E'), false) - same(basicsMultibase.has('baseNope'), false) - same(basicsMultibase.has('base32'), true) - same(basicsMultibase.has('c'), true) + + describe('decoder composition', () => { + const base = base32.decoder.or(base58btc.decoder) + + const b32 = base32.encode(bytes.fromString('test')) + same(base.decode(b32), bytes.fromString('test')) + + const b58 = base58btc.encode(bytes.fromString('test')) + same(base.decode(b58), bytes.fromString('test')) + + const b64 = base64.encode(bytes.fromString('test')) + const msg = `Unable to decode multibase string "${b64}", only inputs prefixed with ${base32.prefix},${base58btc.prefix} are supported` + testThrow(() => base.decode(b64), msg) + + const baseExt = base.or(base64) + same(baseExt.decode(b64), bytes.fromString('test')) + + // original composition stayes intact + testThrow(() => base.decode(b64), msg) }) }) diff --git a/test/test-multicodec.js b/test/test-multicodec.js index baf96dcf..3ab9b44a 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -1,7 +1,9 @@ /* globals describe, it */ import * as bytes from '../src/bytes.js' import assert from 'assert' -import multiformats from 'multiformats/basics' +import raw from 'multiformats/codecs/raw' +import json from 'multiformats/codecs/json' +import { codec } from 'multiformats/codecs/codec' const same = assert.deepStrictEqual const test = it @@ -12,53 +14,54 @@ const testThrow = async (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } describe('multicodec', () => { - const { multicodec } = multiformats - test('encode/decode raw', () => { - const buff = multicodec.encode(bytes.fromString('test'), 'raw') + const buff = raw.encode(bytes.fromString('test')) same(buff, bytes.fromString('test')) - same(multicodec.decode(buff, 'raw'), bytes.fromString('test')) + same(raw.decode(buff, 'raw'), bytes.fromString('test')) }) test('encode/decode json', () => { - const buff = multicodec.encode({ hello: 'world' }, 'json') + const buff = json.encode({ hello: 'world' }) same(buff, bytes.fromString(JSON.stringify({ hello: 'world' }))) - same(multicodec.decode(buff, 'json'), { hello: 'world' }) + same(json.decode(buff), { hello: 'world' }) }) - test('raw cannot encode string', async () => { - await testThrow(() => multicodec.encode('asdf', 'raw'), 'Unknown type, must be binary type') + test('json.encoder', () => { + const { encoder } = json + same(encoder === json.encoder, true, 'getter cached decoder') + + const buff = encoder.encode({ hello: 'world' }) + same(buff, bytes.fromString(JSON.stringify({ hello: 'world' }))) + }) + + test('json.decoder', () => { + const { decoder } = json + same(decoder === json.decoder, true, 'getter cached encoder') + + const buff = json.encode({ hello: 'world' }) + same(decoder.decode(buff), { hello: 'world' }) }) - test('get failure', async () => { - await testThrow(() => multicodec.get(true), 'Unknown key type') - let msg = 'Do not have multiformat entry for "8237440"' - await testThrow(() => multicodec.get(8237440), msg) - msg = 'Do not have multiformat entry for "notfound"' - await testThrow(() => multicodec.get('notfound'), msg) + test('raw cannot encode string', async () => { + await testThrow(() => raw.encode('asdf'), 'Unknown type, must be binary type') }) test('add with function', () => { - let calls = 0 - multicodec.add((...args) => { - calls++ - same(args.length, 1, 'called with single arg') - assert(args[0] === multiformats, 'called with multiformats as argument') - return { code: 200, name: 'blip', encode: (a) => a[1], decode: (a) => a } + const blip = codec({ + code: 200, + name: 'blip', + encode: (a) => a[1], + decode: (a) => a }) - same(calls, 1, 'called exactly once') + const two = bytes.fromString('two') const three = bytes.fromString('three') - same(multicodec.encode(['one', two, three], 'blip'), two, 'new codec encoder was added') - same(multicodec.decode(three, 200), three, 'new codec decoder was added') - }) - test('has', async () => { - same(multicodec.has('json'), true) - same(multicodec.has(0x0200), true) - await testThrow(() => multicodec.has({}), 'Unknown type') + same(blip.encode(['one', two, three]), two) + same(blip.decode(three, 200), three) }) }) diff --git a/test/test-multihash.js b/test/test-multihash.js index c469be86..c1ad4e7e 100644 --- a/test/test-multihash.js +++ b/test/test-multihash.js @@ -1,16 +1,20 @@ /* globals describe, it */ -import * as bytes from '../src/bytes.js' +import { coerce, fromHex, fromString } from '../src/bytes.js' import assert from 'assert' -import { create as multiformat } from 'multiformats' -import intTable from 'multicodec/src/int-table.js' import valid from './fixtures/valid-multihash.js' import invalid from './fixtures/invalid-multihash.js' import crypto from 'crypto' -import sha2 from 'multiformats/hashes/sha2' -const same = assert.deepStrictEqual +import { sha256, sha512, __browser } from 'multiformats/hashes/sha2' +import { decode as decodeDigest, create as createDigest } from 'multiformats/hashes/digest' const test = it -const encode = name => data => bytes.coerce(crypto.createHash(name).update(data).digest()) -const table = Array.from(intTable.entries()) +const encode = name => data => coerce(crypto.createHash(name).update(data).digest()) + +const same = (x, y) => { + if (x instanceof Uint8Array && y instanceof Uint8Array) { + if (Buffer.compare(Buffer.from(x), Buffer.from(y)) === 0) return + } + return assert.deepStrictEqual(x, y) +} const sample = (code, size, hex) => { const toHex = (i) => { @@ -18,7 +22,7 @@ const sample = (code, size, hex) => { const h = i.toString(16) return h.length % 2 === 1 ? `0${h}` : h } - return bytes.fromHex(`${toHex(code)}${toHex(size)}${hex}`) + return fromHex(`${toHex(code)}${toHex(size)}${hex}`) } const testThrowAsync = async (fn, message) => { @@ -28,81 +32,74 @@ const testThrowAsync = async (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } describe('multihash', () => { - const { multihash } = multiformat(table) - multihash.add(sha2) - const { validate } = multihash const empty = new Uint8Array(0) describe('encode', () => { test('valid', () => { for (const test of valid) { const { encoding, hex, size } = test - const { code, name, varint } = encoding + const { code, varint } = encoding const buf = sample(varint || code, size, hex) - same(multihash.encode(hex ? bytes.fromHex(hex) : empty, code), buf) - same(multihash.encode(hex ? bytes.fromHex(hex) : empty, name), buf) + same(createDigest(code, hex ? fromHex(hex) : empty).bytes, buf) } }) test('hash sha2-256', async () => { - const hash = await multihash.hash(bytes.fromString('test'), 'sha2-256') - const { digest, code } = multihash.decode(hash) - same(code, multihash.get('sha2-256').code) - same(digest, encode('sha256')(bytes.fromString('test'))) - same(await validate(hash), true) - same(await validate(hash, bytes.fromString('test')), true) + const hash = await sha256.digest(fromString('test')) + same(hash.code, sha256.code) + same(hash.digest, encode('sha256')(fromString('test'))) + + const hash2 = decodeDigest(hash.bytes) + same(hash2.code, sha256.code) + same(hash2.bytes, hash.bytes) }) test('hash sha2-512', async () => { - const hash = await multihash.hash(bytes.fromString('test'), 'sha2-512') - const { digest, code } = multihash.decode(hash) - same(code, multihash.get('sha2-512').code) - same(digest, encode('sha512')(bytes.fromString('test'))) - same(await validate(hash), true) - same(await validate(hash, bytes.fromString('test')), true) - }) - test('no such hash', async () => { - let msg = 'Do not have multiformat entry for "notfound"' - await testThrowAsync(() => multihash.hash(bytes.fromString('test'), 'notfound'), msg) - msg = 'Missing hash implementation for "json"' - await testThrowAsync(() => multihash.hash(bytes.fromString('test'), 'json'), msg) + const hash = await sha512.digest(fromString('test')) + same(hash.code, sha512.code) + same(hash.digest, encode('sha512')(fromString('test'))) + + const hash2 = decodeDigest(hash.bytes) + same(hash2.code, sha512.code) + same(hash2.bytes, hash.bytes) }) }) describe('decode', () => { - test('valid fixtures', () => { - for (const test of valid) { - const { encoding, hex, size } = test - const { code, name, varint } = encoding - const buf = sample(varint || code, size, hex) - const digest = hex ? bytes.fromHex(hex) : empty - same(multihash.decode(buf), { code, name, digest, length: size }) - } - }) + for (const { encoding, hex, size } of valid) { + test(`valid fixture ${hex}`, () => { + const { code, varint } = encoding + const bytes = sample(varint || code, size, hex) + const digest = hex ? fromHex(hex) : empty + const hash = decodeDigest(bytes) + + same(hash.bytes, bytes) + same(hash.code, code) + same(hash.size, size) + same(hash.digest, digest) + }) + } + test('get from buffer', async () => { - const hash = await multihash.hash(bytes.fromString('test'), 'sha2-256') - const { code, name } = multihash.get(hash) - same({ code, name }, { code: 18, name: 'sha2-256' }) + const hash = await sha256.digest(fromString('test')) + + same(hash.code, 18) }) }) describe('validate', async () => { - test('invalid hash sha2-256', async () => { - const hash = await multihash.hash(bytes.fromString('test'), 'sha2-256') - const msg = 'Buffer does not match hash' - await testThrowAsync(() => validate(hash, bytes.fromString('tes2t')), msg) - }) test('invalid fixtures', async () => { for (const test of invalid) { - const buff = bytes.fromHex(test.hex) - await testThrowAsync(() => validate(buff), test.message) + const buff = fromHex(test.hex) + await testThrowAsync(() => decodeDigest(buff), test.message) } }) }) test('throw on hashing non-buffer', async () => { - await testThrowAsync(() => multihash.hash('asdf'), 'Unknown type, must be binary type') + await testThrowAsync(() => sha256.digest('asdf'), 'Unknown type, must be binary type') }) test('browser', () => { - same(sha2.__browser, !!process.browser) + same(__browser, !!process.browser) }) }) diff --git a/vendor/base-x.js b/vendor/base-x.js new file mode 100644 index 00000000..b75ea756 --- /dev/null +++ b/vendor/base-x.js @@ -0,0 +1,127 @@ +// base-x encoding / decoding +// Copyright (c) 2018 base-x contributors +// Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp) +// Distributed under the MIT software license, see the accompanying +// file LICENSE or http://www.opensource.org/licenses/mit-license.php. +function base (ALPHABET) { + if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') } + var BASE_MAP = new Uint8Array(256); + for (var j = 0; j < BASE_MAP.length; j++) { + BASE_MAP[j] = 255; + } + for (var i = 0; i < ALPHABET.length; i++) { + var x = ALPHABET.charAt(i); + var xc = x.charCodeAt(0); + if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') } + BASE_MAP[xc] = i; + } + var BASE = ALPHABET.length; + var LEADER = ALPHABET.charAt(0); + var FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up + var iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up + function encode (source) { + if (source instanceof Uint8Array) ; else if (ArrayBuffer.isView(source)) { + source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength); + } else if (Array.isArray(source)) { + source = Uint8Array.from(source); + } + if (!(source instanceof Uint8Array)) { throw new TypeError('Expected Uint8Array') } + if (source.length === 0) { return '' } + // Skip & count leading zeroes. + var zeroes = 0; + var length = 0; + var pbegin = 0; + var pend = source.length; + while (pbegin !== pend && source[pbegin] === 0) { + pbegin++; + zeroes++; + } + // Allocate enough space in big-endian base58 representation. + var size = ((pend - pbegin) * iFACTOR + 1) >>> 0; + var b58 = new Uint8Array(size); + // Process the bytes. + while (pbegin !== pend) { + var carry = source[pbegin]; + // Apply "b58 = b58 * 256 + ch". + var i = 0; + for (var it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) { + carry += (256 * b58[it1]) >>> 0; + b58[it1] = (carry % BASE) >>> 0; + carry = (carry / BASE) >>> 0; + } + if (carry !== 0) { throw new Error('Non-zero carry') } + length = i; + pbegin++; + } + // Skip leading zeroes in base58 result. + var it2 = size - length; + while (it2 !== size && b58[it2] === 0) { + it2++; + } + // Translate the result into a string. + var str = LEADER.repeat(zeroes); + for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); } + return str + } + function decodeUnsafe (source) { + if (typeof source !== 'string') { throw new TypeError('Expected String') } + if (source.length === 0) { return new Uint8Array() } + var psz = 0; + // Skip leading spaces. + if (source[psz] === ' ') { return } + // Skip and count leading '1's. + var zeroes = 0; + var length = 0; + while (source[psz] === LEADER) { + zeroes++; + psz++; + } + // Allocate enough space in big-endian base256 representation. + var size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up. + var b256 = new Uint8Array(size); + // Process the characters. + while (source[psz]) { + // Decode character + var carry = BASE_MAP[source.charCodeAt(psz)]; + // Invalid character + if (carry === 255) { return } + var i = 0; + for (var it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) { + carry += (BASE * b256[it3]) >>> 0; + b256[it3] = (carry % 256) >>> 0; + carry = (carry / 256) >>> 0; + } + if (carry !== 0) { throw new Error('Non-zero carry') } + length = i; + psz++; + } + // Skip trailing spaces. + if (source[psz] === ' ') { return } + // Skip leading zeroes in b256. + var it4 = size - length; + while (it4 !== size && b256[it4] === 0) { + it4++; + } + var vch = new Uint8Array(zeroes + (size - it4)); + var j = zeroes; + while (it4 !== size) { + vch[j++] = b256[it4++]; + } + return vch + } + function decode (string) { + var buffer = decodeUnsafe(string); + if (buffer) { return buffer } + throw new Error('Non-base' + BASE + ' character') + } + return { + encode: encode, + decodeUnsafe: decodeUnsafe, + decode: decode + } +} +var src = base; + +var _brrp__multiformats_scope_baseX = src; + +export default _brrp__multiformats_scope_baseX; diff --git a/vendor/varint.js b/vendor/varint.js new file mode 100644 index 00000000..fdc9f1f6 --- /dev/null +++ b/vendor/varint.js @@ -0,0 +1,91 @@ +var encode_1 = encode; + +var MSB = 0x80 + , REST = 0x7F + , MSBALL = ~REST + , INT = Math.pow(2, 31); + +function encode(num, out, offset) { + out = out || []; + offset = offset || 0; + var oldOffset = offset; + + while(num >= INT) { + out[offset++] = (num & 0xFF) | MSB; + num /= 128; + } + while(num & MSBALL) { + out[offset++] = (num & 0xFF) | MSB; + num >>>= 7; + } + out[offset] = num | 0; + + encode.bytes = offset - oldOffset + 1; + + return out +} + +var decode = read; + +var MSB$1 = 0x80 + , REST$1 = 0x7F; + +function read(buf, offset) { + var res = 0 + , offset = offset || 0 + , shift = 0 + , counter = offset + , b + , l = buf.length; + + do { + if (counter >= l) { + read.bytes = 0; + throw new RangeError('Could not decode varint') + } + b = buf[counter++]; + res += shift < 28 + ? (b & REST$1) << shift + : (b & REST$1) * Math.pow(2, shift); + shift += 7; + } while (b >= MSB$1) + + read.bytes = counter - offset; + + return res +} + +var N1 = Math.pow(2, 7); +var N2 = Math.pow(2, 14); +var N3 = Math.pow(2, 21); +var N4 = Math.pow(2, 28); +var N5 = Math.pow(2, 35); +var N6 = Math.pow(2, 42); +var N7 = Math.pow(2, 49); +var N8 = Math.pow(2, 56); +var N9 = Math.pow(2, 63); + +var length = function (value) { + return ( + value < N1 ? 1 + : value < N2 ? 2 + : value < N3 ? 3 + : value < N4 ? 4 + : value < N5 ? 5 + : value < N6 ? 6 + : value < N7 ? 7 + : value < N8 ? 8 + : value < N9 ? 9 + : 10 + ) +}; + +var varint = { + encode: encode_1 + , decode: decode + , encodingLength: length +}; + +var _brrp_varint = varint; + +export default _brrp_varint;