From a04a8f61327dbe25a8161fdba127811184eb0f2a Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Thu, 20 Aug 2020 00:33:24 +0000 Subject: [PATCH 01/26] pkg: drop period --- README.md | 60 ++-- package.json | 2 +- src/bases/base.js | 205 +++++++++++ src/bases/base16.js | 25 +- src/bases/base32.js | 65 ++-- src/bases/base58.js | 31 +- src/bases/base64-browser.js | 16 +- src/bases/base64-import.js | 17 +- src/bases/base64.js | 144 +++++--- src/bases/interface.ts | 85 +++++ src/basics-browser.js | 11 +- src/basics-import.js | 9 +- src/basics.js | 31 +- src/block.js | 233 +++++++++++++ src/block/interface.ts | 39 +++ src/bytes.js | 35 +- src/cid.js | 662 +++++++++++++++++++++--------------- src/cid/interface.ts | 15 + src/codecs/codec.js | 101 ++++++ src/codecs/interface.ts | 24 ++ src/codecs/json.js | 14 +- src/codecs/raw.js | 19 +- src/hashes/digest.js | 111 ++++++ src/hashes/hasher.js | 54 +++ src/hashes/interface.ts | 46 +++ src/hashes/sha2-browser.js | 33 +- src/hashes/sha2.js | 34 +- src/index.js | 325 ++---------------- src/legacy.js | 119 +++++-- src/varint.js | 47 +++ test/test-multicodec.js | 45 +-- 31 files changed, 1829 insertions(+), 828 deletions(-) create mode 100644 src/bases/base.js create mode 100644 src/bases/interface.ts create mode 100644 src/block.js create mode 100644 src/block/interface.ts create mode 100644 src/cid/interface.ts create mode 100644 src/codecs/codec.js create mode 100644 src/codecs/interface.ts create mode 100644 src/hashes/digest.js create mode 100644 src/hashes/hasher.js create mode 100644 src/hashes/interface.ts create mode 100644 src/varint.js diff --git a/README.md b/README.md index f548df0b..08db10e8 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,20 @@ This allows you to pass around an interface containing only the code you need which can greatly reduce dependencies and bundle size. ```js -import { create } from 'multiformats' -import sha2 from 'multiformats/hashes/sha2' +import * as CID from 'multiformats/cid' +import { sha256 } from 'multiformats/hashes/sha2' import dagcbor from '@ipld/dag-cbor' -const { multihash, multicodec, CID } = create() -multihash.add(sha2) -multicodec.add(dagcbor) +import { base32 } from 'multiformats/bases/base32' +import { base58btc } from 'multiformats/bases/base58' -const buffer = multicodec.encode({ hello, 'world' }, 'dag-cbor') -const hash = await multihash.hash(buffer, 'sha2-256') +const bytes = dagcbor.encode({ hello: 'world' }) + +const hash = await sha256.digest(bytes) // raw codec is the only codec that is there by default -const cid = new CID(1, 'raw', hash) +const cid = CID.create(1, dagcbor.code, hash, { + base: base32, + base58btc +}) ``` However, if you're doing this much you should probably use multiformats @@ -31,13 +34,12 @@ with the `Block` API. ```js // Import basics package with dep-free codecs, hashes, and base encodings -import multiformats from 'multiformats/basics' +import { block } 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() + +const encoder = block.encoder(dagcbor) +const hello = encoder.encode({ hello: 'world' }) +const cid = await hello.cid() ``` # Plugins @@ -83,35 +85,17 @@ Returns a new multiformats interface. Can optionally pass in a table of multiformat entries. -# multihash - -## multihash.encode - -## multihash.decode - -## multihash.validate - -## multihash.add - -## multihash.hash - -# multicodec - -## multicodec.encode - -## multicodec.decode - -## multicodec.add +## multiformats.configure -# multibase +## multiformats.varint -## multibase.encode +## multiformats.bytes -## multibase.decode +## multiformats.digest -## multibase.add +## multiformats.hasher -# CID +# multiformats.CID Changes from `cids`: diff --git a/package.json b/package.json index 5486ed90..eb561f38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "multiformats", "version": "0.0.0-dev", - "description": "Interface for multihash, multicodec, multibase and CID.", + "description": "Interface for multihash, multicodec, multibase and CID", "main": "index.js", "type": "module", "scripts": { diff --git a/src/bases/base.js b/src/bases/base.js new file mode 100644 index 00000000..7739c031 --- /dev/null +++ b/src/bases/base.js @@ -0,0 +1,205 @@ +// @ts-check + +/** + * @typedef {import('./interface').BaseEncoder} BaseEncoder + * @typedef {import('./interface').BaseDecoder} BaseDecoder + * @typedef {import('./interface').BaseCodec} BaseCodec + */ + +/** + * @template T + * @typedef {import('./interface').Multibase} Multibase + */ +/** + * @template 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) { + // @ts-ignore + return `${this.prefix}${this.baseEncode(bytes)}` + } +} + +/** + * @template T + * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder + */ + +/** + * 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 {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) { + switch (text[0]) { + case this.prefix: { + return this.baseDecode(text.slice(1)) + } + default: { + throw Error(`${this.name} expects input starting with ${this.prefix} and can not decode "${text}"`) + } + } + } +} + +/** + * @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) + +export const notImplemented = ({ name, prefix }) => + from({ + name, + prefix, + encode: _ => { + throw Error(`No ${name} encoder implementation was provided`) + }, + decode: _ => { + throw Error(`No ${name} decoder implemnetation was provided`) + } + }) 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..97de853e 100644 --- a/src/bases/base58.js +++ b/src/bases/base58.js @@ -1,16 +1,25 @@ +// @ts-check + import baseX from 'base-x' 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..06b73728 --- /dev/null +++ b/src/bases/interface.ts @@ -0,0 +1,85 @@ +// 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 + +/** + * 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 +} diff --git a/src/basics-browser.js b/src/basics-browser.js index f3e86b4c..4de223c8 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, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' + +const bases = { ..._bases, ...base64 } + +export { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics-import.js b/src/basics-import.js index 676d43b2..12f297f8 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, CID, block, Block, 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, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics.js b/src/basics.js index 893f7656..2724fc7a 100644 --- a/src/basics.js +++ b/src/basics.js @@ -1,13 +1,22 @@ -import { create } from './index.js' +// @ts-check + +import { notImplemented } from './bases/base.js' +import * as base32 from './bases/base32.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 configure from './index.js' + +const bases = { ...base32 } +const hashes = { ...sha2 } +const codecs = { raw, json } + +const { cid, CID, block, Block, hasher, digest, varint, bytes } = configure({ + base: bases.base32, + base58btc: notImplemented({ name: 'base58btc', prefix: 'z' }), + hasher: hashes.sha256 +}) + +export { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, bases, codecs } diff --git a/src/block.js b/src/block.js new file mode 100644 index 00000000..d00f8716 --- /dev/null +++ b/src/block.js @@ -0,0 +1,233 @@ +// @ts-check + +import { createV1 } from './cid.js' + +/** + * @class + * @template T + */ +class BlockEncoder { + /** + * @param {Encoder} codec + * @param {BlockConfig} config + */ + constructor (codec, config) { + this.codec = codec + this.config = config + } + + /** + * @param {T} data + * @param {BlockConfig} [options] + * @returns {Block} + */ + encode (data, options) { + const { codec } = this + const bytes = codec.encode(data) + return new Block(null, codec.code, data, bytes, { ...this.config, ...options }) + } +} + +/** + * @class + * @template T + */ +class BlockDecoder { + /** + * @param {Decoder} codec + * @param {BlockConfig} config + */ + constructor (codec, config) { + this.codec = codec + this.config = config + } + + /** + * @param {Uint8Array} bytes + * @param {BlockConfig} [options] + * @returns {Block} + */ + decode (bytes, options) { + const data = this.codec.decode(bytes) + return new Block(null, this.codec.code, data, bytes, { ...this.config, ...options }) + } +} + +/** + * @template T + * @class + */ +export class Block { + /** + * @param {CID|null} cid + * @param {number} code + * @param {T} data + * @param {Uint8Array} bytes + * @param {BlockConfig} config + */ + constructor (cid, code, data, bytes, { hasher, base, base58btc }) { + /** @type {CID|Promise|null} */ + this._cid = cid + this.code = code + this.data = data + this.bytes = bytes + this.hasher = hasher + this.base = base + this.base58btc = base58btc + } + + async cid () { + const { _cid: cid } = this + if (cid != null) { + return await cid + } else { + const { bytes, code, hasher } = this + // First we store promise to avoid a race condition if cid is called + // whlie promise is pending. + const promise = createCID(hasher, bytes, code, this) + this._cid = promise + const cid = await promise + // Once promise resolves we store an actual CID. + this._cid = cid + return cid + } + } +} + +/** + * + * @param {Hasher} hasher + * @param {Uint8Array} bytes + * @param {number} code + * @param {BlockConfig} context + */ + +const createCID = async (hasher, bytes, code, context) => { + const multihash = await hasher.digest(bytes) + return createV1(code, multihash, context) +} + +/** + * @template T + */ +class BlockCodec { + /** + * @param {Encoder} encoder + * @param {Decoder} decoder + * @param {BlockConfig} config + */ + + constructor (encoder, decoder, config) { + this.encoder = new BlockEncoder(encoder, config) + this.decoder = new BlockDecoder(decoder, config) + this.config = config + } + + /** + * @param {Uint8Array} bytes + * @param {BlockConfig} [options] + * @returns {Block} + */ + decode (bytes, options) { + return this.decoder.decode(bytes, { ...this.config, ...options }) + } + + /** + * @param {T} data + * @param {BlockConfig} [options] + * @returns {Block} + */ + encode (data, options) { + return this.encoder.encode(data, { ...this.config, ...options }) + } +} + +/** + * @typedef {Object} Config + * @property {MultibaseCodec} base + * @property {MultibaseCodec<'z'>} base58btc + */ + +class BlockAPI { + /** + * @param {BlockConfig} config + */ + constructor (config) { + this.config = config + this.Block = Block + } + + /** + * @template T + * @param {Encoder} options + * @param {Partial} [options] + */ + encoder (codec, options) { + return new BlockEncoder(codec, { ...this.config, ...options }) + } + + /** + * @template T + * @param {Decoder} options + * @param {Partial} [options] + */ + decoder (codec, options) { + return new BlockDecoder(codec, { ...this.config, ...options }) + } + + /** + * @template T + * @param {Object} codec + * @param {Encoder} codec.encoder + * @param {Decoder} codec.decoder + * @param {Partial} [options] + * @returns {BlockCodec} + */ + + codec ({ encoder, decoder }, options) { + return new BlockCodec(encoder, decoder, { ...this.config, ...options }) + } +} + +/** + * @param {BlockConfig} config + */ +export const configure = (config) => new BlockAPI(config) + +export default configure + +/** + * @typedef {import('./cid').CID} CID + * @typedef {import('./block/interface').Config} BlockConfig + * @typedef {import('./hashes/interface').MultihashHasher} Hasher + **/ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder + */ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder + */ + +/** + * @template T + * @typedef {import('./bases/interface').MultibaseCodec} MultibaseCodec + */ + +/** + * @template T + * @typedef {import('./codecs/interface').BlockEncoder} Encoder + */ + +/** + * @template T + * @typedef {import('./codecs/interface').BlockDecoder} Decoder + */ + +/** + * @template T + * @typedef {import('./codecs/interface').BlockCodec} Codec + */ diff --git a/src/block/interface.ts b/src/block/interface.ts new file mode 100644 index 00000000..4c1e8664 --- /dev/null +++ b/src/block/interface.ts @@ -0,0 +1,39 @@ +// Block +import { MultibaseCodec } from "../bases/interface" +import { BlockEncoder as Encoder, BlockDecoder as Decoder } from "../codecs/interface" +import { MultihashHasher as Hasher } from "../hashes/interface" +import { CID } from "../cid" + + +// Just a representation for awaitable `T`. +export type Awaitable = + | T + | Promise + + +export interface Block { + cid(): Awaitable + encode(): Awaitable +} + + +export interface Config { + /** + * Multihasher to be use for the CID of the block. Will use a default + * if not provided. + */ + hasher: Hasher + /** + * Base encoder that will be passed by the CID of the block. + */ + base: MultibaseCodec + + /** + * Base codec that will be used with CIDv0. + */ + base58btc: MultibaseCodec<'z'> +} + + + + 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..8377d07f 100644 --- a/src/cid.js +++ b/src/cid.js @@ -1,300 +1,338 @@ -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' -// 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 + * @typedef {import('./bases/interface').BaseEncoder} BaseEncoder + * @typedef {import('./bases/interface').BaseDecoder} BaseDecoder + */ -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 + * @typedef {import('./cid/interface').Config} Config */ -export default multiformats => { - const { multibase, varint, multihash } = multiformats +/** + * @implements {Config} + */ +export 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 + * @param {Config} config + * */ - 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, { base, base58btc }) { + this.code = code + this.version = version + this.multihash = multihash + this.bytes = bytes + + this.base = base + this.base58btc = base58btc + + // 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}`) + if (this.code !== DAG_PB_CODE) { + throw new Error('Cannot convert a non dag-pb CID to CIDv0') + } + + const { code, digest } = this.multihash + + // sha2-256 + if (code !== SHA_256_CODE) { + throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') + } + + return createV0(Digest.decodeImplicitSha256(digest), this) } } } - 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 createV1(this.code, multihash, this) + } + case 1: { + return this } 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] */ - 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 || this.base58btc.encoder) + default: + return toStringV1(bytes, _baseCache, base || this.base.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()') - } +class CIDAPI { + /** + * Returns API for working with CIDs. + * @param {Config} config + */ + constructor (config) { + this.config = config + this.CID = CID + } - [Symbol.for('nodejs.util.inspect.custom')] () { - return 'CID(' + this.toString() + ')' - } + create (version, code, digest) { + return create(version, code, digest, this.config) + } - toString (base) { - const { version, bytes } = this - if (version === 0) { - if (base && base !== 'base58btc') { - throw new Error(`Cannot string encode V0 in ${base} encoding`) - } - const { encode } = multibase.get('base58btc') - return encode(bytes) - } + parse (cid) { + return parse(cid, this.config) + } - 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 + decode (cid) { + return decode(cid, this.config) + } + + asCID (input) { + return asCID(input, this.config) + } + + /** + * Creates a new CID from either string, binary or an object representation. + * Throws an error if provided `value` is not a valid CID. + * + * @param {CID|string|Uint8Array} value + * @returns {CID} + */ + from (value) { + if (typeof value === 'string') { + return parse(value, this.config) + } else if (value instanceof Uint8Array) { + return decode(value, this.config) + } else { + const cid = asCID(value, this.config) + if (cid) { + // If we got the same CID back we create a copy. + if (cid === value) { + return new CID(cid.version, cid.code, cid.multihash, cid.bytes, this.config) + } else { + return cid + } } else { - return string + throw new TypeError(`Can not create CID from given value ${value}`) } } + } +} - toJSON () { - return { - code: this.code, - version: this.version, - hash: this.multihash +/** + * + * @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. + * @param {Config} config - Base encoding that will be used for toString + * serialization. If omitted configured default will be used. + * @returns {CID} + */ +export const create = (version, code, digest, config) => { + 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, config) } } - - equals (other) { - return this.code === other.code && - this.version === other.version && - Bytes.equals(this.multihash, other.multihash) + case 1: { + const bytes = encodeCID(version, code, digest.bytes) + return new CID(version, code, digest, bytes, config) } + default: { + throw new Error('Invalid version') + } + } +} - get [Symbol.toStringTag] () { - return 'CID' +/** + * Simplified version of `create` for CIDv0. + * @param {MultihashDigest} digest - Multihash. + * @param {Config} config + */ +export const createV0 = (digest, config) => create(0, DAG_PB_CODE, digest, config) + +/** + * Simplified version of `create` for CIDv1. + * @template {number} Code + * @param {Code} code - Content encoding format code. + * @param {MultihashDigest} digest - Miltihash of the content. + * @param {Config} config - Base encoding used of the serialziation. If + * omitted configured default is used. + * @returns {CID} + */ +export const createV1 = (code, digest, config) => create(1, code, digest, config) + +/** + * 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). + * + * @param {string} source + * @param {Config} config + */ +export const parse = (source, config) => { + const { base, base58btc } = config + const [name, bytes] = source[0] === 'Q' + ? [BASE_58_BTC, base58btc.decoder.decode(`${BASE_58_BTC_PREFIX}{source}`)] + : [base.encoder.name, base.decoder.decode(source)] + + const cid = decode(bytes, config) + // Cache string representation to avoid computing it on `this.toString()` + // @ts-ignore - Can't access private + cid._baseCache.set(name, source) + + return cid +} + +/** + * 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 + * @param {Config} config + */ +export const decode = (cid, config) => { + const [version, offset] = varint.decode(cid) + switch (version) { + // CIDv0 + case 18: { + const multihash = Digest.decodeImplicitSha256(cid) + return createV0(multihash, config) } + // CIDv1 + case 1: { + const [code, length] = varint.decode(cid.subarray(offset)) + const digest = Digest.decode(cid.subarray(offset + length)) + return createV1(code, digest, config) + } + default: { + throw new RangeError(`Invalid CID version ${version}`) + } + } +} - /** +/** * 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 @@ -304,39 +342,133 @@ export default multiformats => { * This allows two different incompatible versions of CID library to * co-exist and interop as long as binary interface is compatible. * @param {any} value + * @param {Config} config * @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 - } +export const asCID = (value, config) => { + 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, config } = value + return new CID(version, code, multihash, bytes, config) + } 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 = version === 0 + ? Digest.decodeImplicitSha256(multihash) + : Digest.decode(multihash) + return create(version, code, digest, config) + } else { + // Otherwise value is not a CID (or an incompatible version of it) in + // which case we return `null`. + return null + } +} +/** + * + * @param {Uint8Array} bytes + * @param {Map} cache + * @param {MultibaseEncoder<'z'>} base + */ +const toStringV0 = (bytes, cache, base) => { + const cid = cache.get(BASE_58_BTC) + if (cid == null) { + const multibase = base.encode(bytes) + if (multibase[0] !== BASE_58_BTC_PREFIX) { + throw Error('CIDv0 can only be encoded to base58btc encoding, invalid') } + const cid = multibase.slice(1) + cache.set(BASE_58_BTC, cid) + return cid + } else { + return cid + } +} - static isCID (value) { - deprecate(/^0\.0/, IS_CID_DEPRECATION) - return !!(value && (value[cidSymbol] || value.asCID === value)) - } +/** + * @template Prefix + * @param {Uint8Array} bytes + * @param {Map} cache + * @param {MultibaseEncoder} base + */ +const toStringV1 = (bytes, cache, base) => { + const cid = cache.get(base.name) + if (cid == null) { + const cid = base.encode(bytes) + cache.set(base.name, cid) + return cid + } else { + return cid + } +} + +/** + * @param {Config} config + */ +export const configure = config => new CIDAPI(config) + +export default configure + +const BASE_58_BTC = 'base58btc' +const BASE_58_BTC_PREFIX = 'z' +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: - return CID +const cid = CID.asCID(value) +if (cid) { + // Make sure to use cid instead of value + doSomethingWithCID(cid) } +` diff --git a/src/cid/interface.ts b/src/cid/interface.ts new file mode 100644 index 00000000..00ec97bb --- /dev/null +++ b/src/cid/interface.ts @@ -0,0 +1,15 @@ +import { MultibaseCodec, BaseCodec } from "../bases/interface.js" + + +export interface Config { + /** + * Multibase codec used by CID to encode / decode to and out of + * string representation. + */ + base: MultibaseCodec + /** + * CIDv0 requires base58btc encoding decoding so CID must be + * provided means to perform that task. + */ + base58btc: MultibaseCodec<'z'> +} \ No newline at end of file diff --git a/src/codecs/codec.js b/src/codecs/codec.js new file mode 100644 index 00000000..76ca9f5f --- /dev/null +++ b/src/codecs/codec.js @@ -0,0 +1,101 @@ +// @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 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 T + * @typedef {import('./interface').BlockDecoder} BlockDecoder + */ + +/** + * @class + * @template T + * @implements {BlockDecoder} + */ +export class Decoder { + /** + * @param {(bytes:Uint8Array) => T} decode + */ + constructor (code, decode) { + this.code = code + this.decode = decode + } +} + +/** + * @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, decode } = this + const decoder = new Decoder(name, 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..d55cec93 --- /dev/null +++ b/src/codecs/interface.ts @@ -0,0 +1,24 @@ +/** + * IPLD encoder part of the codec. + */ +export interface BlockEncoder { + name: string + code: number + encode(data: T): Uint8Array +} + +/** + * IPLD decoder part of the codec. + */ +export interface BlockDecoder { + code: number + decode(bytes: Uint8Array): 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 { } + 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..0250e085 --- /dev/null +++ b/src/hashes/digest.js @@ -0,0 +1,111 @@ +// @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('Given multihash has incorrect length') + } + + return new Digest(code, size, digest, bytes) +} + +/** + * Turns bytes representation of multihash digest into an instance. + * @param {Uint8Array} hash + */ +export const decodeImplicitSha256 = (hash) => { + if (hash.byteLength !== SHA256_SIZE) { + throw new Error('Given hash has incorrect length') + } + + return new ImplicitSha256Digest(hash) +} + +/** + * @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) + } +} + +const SHA256_SIZE = 32 +const SHA256_CODE = 0x12 + +/** + * @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 + } +} + +/** + * @class + * @implements {MultihashDigest} + * @extends {Digest<0x12, 32>} + */ +class ImplicitSha256Digest extends Digest { + /** + * @param {Uint8Array} digest + */ + constructor (digest) { + super(SHA256_CODE, SHA256_SIZE, digest, digest) + } +} diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js new file mode 100644 index 00000000..310f15b7 --- /dev/null +++ b/src/hashes/hasher.js @@ -0,0 +1,54 @@ +// @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) { + const digest = await this.encode(input) + return Digest.create(this.code, digest) + } +} + +/** + * @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..2c8ddd2a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,312 +1,29 @@ -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 - */ +// @ts-check -/** - * @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 - */ +import cid, { CID } from './cid.js' +import block, { Block } from './block.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' -/** - * @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 - */ +export { CID, Block, cid, block, hasher, digest, varint, bytes, codec } /** - * @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) - } - - /** - * @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) + * @param {import('./block/interface').Config} config + */ +export const configure = (config) => { + return { + cid: cid(config), + block: block(config), + hasher, + codec, + digest, + varint, + bytes } - 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) - - return multiformats -} -export { create, bytes, varint } +export default configure diff --git a/src/legacy.js b/src/legacy.js index d626856e..79a42afc 100644 --- a/src/legacy.js +++ b/src/legacy.js @@ -1,31 +1,50 @@ -import CID from 'cids' +// @ts-check + +import OldCID from 'cids' import * as bytes from './bytes.js' import { Buffer } from 'buffer' +import * as LibCID from './cid.js' + +/** + * @template T + * @param {Object} multiformats + * @param {Object} multiformats.hashes + * @param {Object>} multiformats.codecs + * @param {MultibaseCodec} multiformats.base + * @param {MultibaseCodec<'z'>} multiformats.base58btc + * @param {BlockCodec} codec + */ -const legacy = (multiformats, name) => { +const legacy = (multiformats, codec) => { 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 = LibCID.asCID(obj, multiformats) + if (newCID) { + const { version, multihash: { bytes } } = newCID + const { buffer, byteOffset, byteLength } = bytes + const { name } = multiformats.codecs[newCID.code] const multihash = Buffer.from(buffer, byteOffset, byteLength) - return new CID(version, name, Buffer.from(multihash)) + return new OldCID(version, name, multihash) + } + + if (bytes.isBinary(obj)) { + 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 = LibCID.asCID(obj, multiformats) if (cid) return cid if (bytes.isBinary(obj)) return bytes.coerce(obj) if (obj && typeof obj === 'object') { @@ -35,47 +54,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 = multiformats.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) + let value = codec.decode(buff) + const entries = path.split('/').filter(x => x) while (path.length) { - value = value[path.shift()] + value = value[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..840df47e --- /dev/null +++ b/src/varint.js @@ -0,0 +1,47 @@ +import varint from 'varint' + +/** + * @param {Uint8Array} data + * @returns {[number, number]} + */ +export const decode = (data) => { + const code = varint.decode(data) + return [code, varint.decode.bytes] +} + +/** + * @param {number} int + * @returns {Uint8Array} + */ +export const encode = (int) => { + if (cache.has(int)) return cache.get(int) + const bytes = new Uint8Array(varint.encodingLength(int)) + varint.encode(int, bytes, 0) + cache.set(int, bytes) + + return bytes +} + +/** + * @param {number} int + * @param {Uint8Array} target + * @param {number} [offset=0] + */ +export const encodeTo = (int, target, offset = 0) => { + const cached = cache.get(int) + if (cached) { + target.set(target, offset) + } else { + varint.encode(int, target, 0) + } +} + +/** + * @param {number} int + * @returns {number} + */ +export const encodingLength = (int) => { + return varint.encodingLength(int) +} + +const cache = new Map() diff --git a/test/test-multicodec.js b/test/test-multicodec.js index baf96dcf..a14256ba 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -1,7 +1,8 @@ /* globals describe, it */ import * as bytes from '../src/bytes.js' import assert from 'assert' -import multiformats from 'multiformats/basics' +import * as multiformats from 'multiformats/basics' +import { codec } from 'multiformats/codecs/codec' const same = assert.deepStrictEqual const test = it @@ -16,49 +17,35 @@ const testThrow = async (fn, message) => { } describe('multicodec', () => { - const { multicodec } = multiformats + const { codecs: { raw, json } } = 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('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) + await testThrow(() => raw.encode('asdf', 'raw'), '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) }) }) From fd0ce178fdb8bd7a0c69c3d84c6e88e2c19a3cea Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 9 Sep 2020 01:21:14 -0700 Subject: [PATCH 02/26] invalidate cache From edbc56cf5b8cc874f37448870409635f29183d52 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Wed, 16 Sep 2020 18:48:04 +0000 Subject: [PATCH 03/26] fix: vendor varint for pure ESM --- package.json | 4 +-- src/varint.js | 2 +- vendor/varint.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 vendor/varint.js diff --git a/package.json b/package.json index eb561f38..392076f4 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "npm_config_yes=true npx ipjs@latest build --tests", + "build:vendor": "npx brrp -x varint > vendor/varint.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", @@ -71,8 +72,7 @@ "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/varint.js b/src/varint.js index 840df47e..b4632e14 100644 --- a/src/varint.js +++ b/src/varint.js @@ -1,4 +1,4 @@ -import varint from 'varint' +import varint from '../vendor/varint.js' /** * @param {Uint8Array} data 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; From b7aa2a6255eb4939f115673f9d30273675087b2f Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 16 Sep 2020 12:13:02 -0700 Subject: [PATCH 04/26] fix: pass encode offset --- src/varint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/varint.js b/src/varint.js index b4632e14..15340f9a 100644 --- a/src/varint.js +++ b/src/varint.js @@ -32,7 +32,7 @@ export const encodeTo = (int, target, offset = 0) => { if (cached) { target.set(target, offset) } else { - varint.encode(int, target, 0) + varint.encode(int, target, offset) } } From 70cd8cef9420576afc206fa4a1e2f278e603f9f9 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 16 Sep 2020 12:15:02 -0700 Subject: [PATCH 05/26] fix: remove unecessary ImplicitSha256Digest --- src/cid.js | 8 +++----- src/hashes/digest.js | 29 ----------------------------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/src/cid.js b/src/cid.js index 8377d07f..c107acd7 100644 --- a/src/cid.js +++ b/src/cid.js @@ -87,7 +87,7 @@ export class CID { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return createV0(Digest.decodeImplicitSha256(digest), this) + return createV0(Digest.decode(digest), this) } } } @@ -317,7 +317,7 @@ export const decode = (cid, config) => { switch (version) { // CIDv0 case 18: { - const multihash = Digest.decodeImplicitSha256(cid) + const multihash = Digest.decode(cid) return createV0(multihash, config) } // CIDv1 @@ -362,9 +362,7 @@ export const asCID = (value, config) => { // symbol we still rebase it to the this `CID` implementation by // delegating that to a constructor. const { version, multihash, code } = value - const digest = version === 0 - ? Digest.decodeImplicitSha256(multihash) - : Digest.decode(multihash) + const digest = Digest.decode(multihash) return create(version, code, digest, config) } else { // Otherwise value is not a CID (or an incompatible version of it) in diff --git a/src/hashes/digest.js b/src/hashes/digest.js index 0250e085..da5208e6 100644 --- a/src/hashes/digest.js +++ b/src/hashes/digest.js @@ -40,18 +40,6 @@ export const decode = (multihash) => { return new Digest(code, size, digest, bytes) } -/** - * Turns bytes representation of multihash digest into an instance. - * @param {Uint8Array} hash - */ -export const decodeImplicitSha256 = (hash) => { - if (hash.byteLength !== SHA256_SIZE) { - throw new Error('Given hash has incorrect length') - } - - return new ImplicitSha256Digest(hash) -} - /** * @param {MultihashDigest} a * @param {MultihashDigest} b @@ -65,9 +53,6 @@ export const equals = (a, b) => { } } -const SHA256_SIZE = 32 -const SHA256_CODE = 0x12 - /** * @typedef {import('./interface').MultihashDigest} MultihashDigest */ @@ -95,17 +80,3 @@ export class Digest { this.bytes = bytes } } - -/** - * @class - * @implements {MultihashDigest} - * @extends {Digest<0x12, 32>} - */ -class ImplicitSha256Digest extends Digest { - /** - * @param {Uint8Array} digest - */ - constructor (digest) { - super(SHA256_CODE, SHA256_SIZE, digest, digest) - } -} From 66682dfd3e3ae08d79bdb2ef78cfb3eff2a1f50b Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 16 Sep 2020 12:15:41 -0700 Subject: [PATCH 06/26] chore: add links, tree and get APIs to Block --- src/block.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/src/block.js b/src/block.js index d00f8716..f668d4b5 100644 --- a/src/block.js +++ b/src/block.js @@ -1,6 +1,6 @@ // @ts-check -import { createV1 } from './cid.js' +import { createV1, asCID } from './cid.js' /** * @class @@ -92,6 +92,102 @@ export class Block { return cid } } + + links () { + return links(this.data, [], this) + } + + tree () { + return tree(this.data, [], this) + } + + /** + * @param {string} path + */ + get (path) { + return get(this.data, path.split('/').filter(Boolean), this) + } +} + +/** + * @template T + * @param {T} source + * @param {Array} base + * @param {BlockConfig} config + * @returns {Iterable<[string, CID]>} + */ +const links = function * (source, base, config) { + for (const [key, value] of Object.entries(source)) { + const path = [...base, key] + if (value != null && typeof value === 'object') { + if (Array.isArray(value)) { + for (const [index, element] of value.entries()) { + const elementPath = [...path, index] + const cid = asCID(element, config) + if (cid) { + yield [elementPath.join('/'), cid] + } else if (typeof element === 'object') { + yield * links(element, elementPath, config) + } + } + } else { + const cid = asCID(value, config) + if (cid) { + yield [path.join('/'), cid] + } else { + yield * links(value, path, config) + } + } + } + } +} + +/** + * @template T + * @param {T} source + * @param {Array} base + * @param {BlockConfig} config + * @returns {Iterable} + */ +const tree = function * (source, base, config) { + for (const [key, value] of Object.entries(source)) { + const path = [...base, key] + yield path.join('/') + if (value != null && typeof value === 'object' && !asCID(value, config)) { + if (Array.isArray(value)) { + for (const [index, element] of value.entries()) { + const elementPath = [...path, index] + yield elementPath.join('/') + if (typeof element === 'object' && !asCID(elementPath, config)) { + yield * tree(element, elementPath, config) + } + } + } else { + yield * tree(value, path, config) + } + } + } +} + +/** + * @template T + * @param {T} source + * @param {string[]} path + * @param {BlockConfig} config + */ +const get = (source, path, config) => { + let node = source + for (const [index, key] of path.entries()) { + node = node[key] + if (node == null) { + throw new Error(`Object has no property at ${path.slice(0, index - 1).map(part => `[${JSON.stringify(part)}]`).join('')}`) + } + const cid = asCID(node, config) + if (cid) { + return { value: cid, remaining: path.slice(index).join('/') } + } + } + return { value: node } } /** From 04030cbcfb8eb8700797123ca1b00f5c9183e04f Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 16 Sep 2020 10:02:05 -0700 Subject: [PATCH 07/26] fix: typo in template literal --- src/cid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cid.js b/src/cid.js index c107acd7..cb1d6f9e 100644 --- a/src/cid.js +++ b/src/cid.js @@ -292,7 +292,7 @@ export const createV1 = (code, digest, config) => create(1, code, digest, config export const parse = (source, config) => { const { base, base58btc } = config const [name, bytes] = source[0] === 'Q' - ? [BASE_58_BTC, base58btc.decoder.decode(`${BASE_58_BTC_PREFIX}{source}`)] + ? [BASE_58_BTC, base58btc.decoder.decode(`${BASE_58_BTC_PREFIX}${source}`)] : [base.encoder.name, base.decoder.decode(source)] const cid = decode(bytes, config) From 655860ee3fe68c11c448ad5dc1ff51ea6a444ea9 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 16 Sep 2020 17:20:58 -0700 Subject: [PATCH 08/26] fix: vendor base-x for pure ESM --- package.json | 5 +- src/bases/base58.js | 2 +- vendor/base-x.js | 127 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 vendor/base-x.js diff --git a/package.json b/package.json index 392076f4..f8d50756 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "type": "module", "scripts": { "build": "npm_config_yes=true npx ipjs@latest build --tests", - "build:vendor": "npx brrp -x varint > vendor/varint.js", + "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", @@ -70,7 +72,6 @@ ] }, "dependencies": { - "base-x": "^3.0.8", "buffer": "^5.6.0", "cids": "^1.0.0" }, diff --git a/src/bases/base58.js b/src/bases/base58.js index 97de853e..21931a66 100644 --- a/src/bases/base58.js +++ b/src/bases/base58.js @@ -1,6 +1,6 @@ // @ts-check -import baseX from 'base-x' +import baseX from '../../vendor/base-x.js' import { coerce } from '../bytes.js' import { from } from './base.js' 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; From 783bce4a75111ae6861ccdbfaf72771f006d58ff Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Thu, 17 Sep 2020 03:28:39 -0700 Subject: [PATCH 09/26] feat: bundle base58btc and base32 encoders --- README.md | 8 +- package.json | 9 ++ src/bases/base.js | 113 +++++++++++--- src/bases/interface.ts | 5 + src/basics-browser.js | 4 +- src/basics-import.js | 4 +- src/basics.js | 14 +- src/block.js | 193 ++++++++++------------- src/block/interface.ts | 27 +--- src/cid.js | 330 ++++++++++++++++++---------------------- src/cid/interface.ts | 15 -- src/hashes/digest.js | 2 +- src/hashes/hasher.js | 8 +- src/index.js | 24 +-- src/legacy.js | 24 ++- test/test-cid.js | 239 ++++++++++++++--------------- test/test-errors.js | 16 -- test/test-legacy.js | 28 ++-- test/test-multibase.js | 94 +++++------- test/test-multicodec.js | 2 +- test/test-multihash.js | 94 +++++------- 21 files changed, 581 insertions(+), 672 deletions(-) delete mode 100644 src/cid/interface.ts delete mode 100644 test/test-errors.js diff --git a/README.md b/README.md index 08db10e8..3b97cff4 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,7 @@ const bytes = dagcbor.encode({ hello: 'world' }) const hash = await sha256.digest(bytes) // raw codec is the only codec that is there by default -const cid = CID.create(1, dagcbor.code, hash, { - base: base32, - base58btc -}) +const cid = CID.create(1, dagcbor.code, hash) ``` However, if you're doing this much you should probably use multiformats @@ -35,9 +32,10 @@ with the `Block` API. ```js // Import basics package with dep-free codecs, hashes, and base encodings import { block } from 'multiformats/basics' +import { sha256 } from 'multiformats/hashes/sha2' import dagcbor from '@ipld/dag-cbor' -const encoder = block.encoder(dagcbor) +const encoder = block.encoder(dagcbor, { hasher: sha256 }) const hello = encoder.encode({ hello: 'world' }) const cid = await hello.cid() ``` diff --git a/package.json b/package.json index f8d50756..ae8e6e29 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,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" }, diff --git a/src/bases/base.js b/src/bases/base.js index 7739c031..3d92e418 100644 --- a/src/bases/base.js +++ b/src/bases/base.js @@ -7,11 +7,11 @@ */ /** - * @template T + * @template {string} T * @typedef {import('./interface').Multibase} Multibase */ /** - * @template T + * @template {string} T * @typedef {import('./interface').MultibaseEncoder} MultibaseEncoder */ @@ -42,16 +42,24 @@ class Encoder { * @returns {Multibase} */ encode (bytes) { - // @ts-ignore - return `${this.prefix}${this.baseEncode(bytes)}` + if (bytes instanceof Uint8Array) { + return `${this.prefix}${this.baseEncode(bytes)}` + } else { + throw Error('Unknown type, must be binary type') + } } } /** - * @template T + * @template {string} T * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder */ +/** + * @template {string} T + * @typedef {import('./interface').UnibaseDecoder} UnibaseDecoder + */ + /** * Class represents both BaseDecoder and MultibaseDecoder so it could be used * to decode multibases (with matching prefix) or just base decode strings @@ -60,6 +68,7 @@ class Encoder { * @template {string} Base * @template {string} Prefix * @implements {MultibaseDecoder} + * @implements {UnibaseDecoder} * @implements {BaseDecoder} */ class Decoder { @@ -78,13 +87,83 @@ class Decoder { * @param {string} text */ decode (text) { - switch (text[0]) { - case this.prefix: { - return this.baseDecode(text.slice(1)) - } - default: { - throw Error(`${this.name} expects input starting with ${this.prefix} and can not decode "${text}"`) + if (typeof text === 'string') { + switch (text[0]) { + case this.prefix: { + return this.baseDecode(text.slice(1)) + } + default: { + throw Error(`${this.name} expects input starting with ${this.prefix} and can not decode "${text}"`) + } } + } else { + throw Error('Can only multibase decode strings') + } + } + + /** + * @template {string} OtherPrefix + * @param {UnibaseDecoder|ComposedDecoder} decoder + * @returns {ComposedDecoder} + */ + or (decoder) { + if (decoder instanceof ComposedDecoder) { + return new ComposedDecoder({ [this.prefix]: this, ...decoder.decoders }) + } else { + return new ComposedDecoder({ [this.prefix]: this, [decoder.prefix]: decoder }) + } + } +} + +/** + * @template {string} Prefix + * @implements {MultibaseDecoder} + */ +class ComposedDecoder { + /** + * @template {string} T + * @param {UnibaseDecoder} decoder + * @returns {ComposedDecoder} + */ + static from (decoder) { + return new ComposedDecoder({ [decoder.prefix]: decoder }) + } + + /** + * @param {Object>} decoders + */ + constructor (decoders) { + /** @type {Object>} */ + this.decoders = decoders + // so that we can distinguish between unibase and multibase + /** @type {void} */ + this.prefix = null + } + + /** + * @template {string} OtherPrefix + * @param {UnibaseDecoder|ComposedDecoder} decoder + * @returns {ComposedDecoder} + */ + or (decoder) { + if (decoder instanceof ComposedDecoder) { + return new ComposedDecoder({ ...this.decoders, ...decoder.decoders }) + } else { + return new ComposedDecoder({ ...this.decoders, [decoder.prefix]: decoder }) + } + } + + /** + * @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 ${input}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) } } } @@ -191,15 +270,3 @@ export const withSettings = ({ name, prefix, settings, encode, decode }) => */ export const from = ({ name, prefix, encode, decode }) => new Codec(name, prefix, encode, decode) - -export const notImplemented = ({ name, prefix }) => - from({ - name, - prefix, - encode: _ => { - throw Error(`No ${name} encoder implementation was provided`) - }, - decode: _ => { - throw Error(`No ${name} decoder implemnetation was provided`) - } - }) diff --git a/src/bases/interface.ts b/src/bases/interface.ts index 06b73728..da4ddf36 100644 --- a/src/bases/interface.ts +++ b/src/bases/interface.ts @@ -83,3 +83,8 @@ export interface MultibaseCodec { encoder: MultibaseEncoder decoder: MultibaseDecoder } + + +export interface UnibaseDecoder extends MultibaseDecoder { + prefix: Prefix +} diff --git a/src/basics-browser.js b/src/basics-browser.js index 4de223c8..20e9afec 100644 --- a/src/basics-browser.js +++ b/src/basics-browser.js @@ -1,8 +1,8 @@ // @ts-check import * as base64 from './bases/base64-browser.js' -import { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' const bases = { ..._bases, ...base64 } -export { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics-import.js b/src/basics-import.js index 12f297f8..2421269f 100644 --- a/src/basics-import.js +++ b/src/basics-import.js @@ -1,6 +1,6 @@ -import { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import { CID, Block, 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, CID, block, Block, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics.js b/src/basics.js index 2724fc7a..37de1fe8 100644 --- a/src/basics.js +++ b/src/basics.js @@ -1,22 +1,16 @@ // @ts-check -import { notImplemented } from './bases/base.js' 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 configure from './index.js' +import { CID, Block, hasher, digest, varint, bytes } from './index.js' -const bases = { ...base32 } +const bases = { ...base32, ...base58 } const hashes = { ...sha2 } const codecs = { raw, json } -const { cid, CID, block, Block, hasher, digest, varint, bytes } = configure({ - base: bases.base32, - base58btc: notImplemented({ name: 'base58btc', prefix: 'z' }), - hasher: hashes.sha256 -}) - -export { cid, CID, block, Block, hasher, digest, varint, bytes, hashes, bases, codecs } +export { CID, Block, hasher, digest, varint, bytes, hashes, bases, codecs } diff --git a/src/block.js b/src/block.js index f668d4b5..5a0ca433 100644 --- a/src/block.js +++ b/src/block.js @@ -1,63 +1,12 @@ // @ts-check -import { createV1, asCID } from './cid.js' +import CID from './cid.js' /** - * @class * @template T - */ -class BlockEncoder { - /** - * @param {Encoder} codec - * @param {BlockConfig} config - */ - constructor (codec, config) { - this.codec = codec - this.config = config - } - - /** - * @param {T} data - * @param {BlockConfig} [options] - * @returns {Block} - */ - encode (data, options) { - const { codec } = this - const bytes = codec.encode(data) - return new Block(null, codec.code, data, bytes, { ...this.config, ...options }) - } -} - -/** * @class - * @template T */ -class BlockDecoder { - /** - * @param {Decoder} codec - * @param {BlockConfig} config - */ - constructor (codec, config) { - this.codec = codec - this.config = config - } - - /** - * @param {Uint8Array} bytes - * @param {BlockConfig} [options] - * @returns {Block} - */ - decode (bytes, options) { - const data = this.codec.decode(bytes) - return new Block(null, this.codec.code, data, bytes, { ...this.config, ...options }) - } -} - -/** - * @template T - * @class - */ -export class Block { +export default class Block { /** * @param {CID|null} cid * @param {number} code @@ -65,15 +14,13 @@ export class Block { * @param {Uint8Array} bytes * @param {BlockConfig} config */ - constructor (cid, code, data, bytes, { hasher, base, base58btc }) { + constructor (cid, code, data, bytes, { hasher }) { /** @type {CID|Promise|null} */ this._cid = cid this.code = code this.data = data this.bytes = bytes this.hasher = hasher - this.base = base - this.base58btc = base58btc } async cid () { @@ -84,7 +31,7 @@ export class Block { const { bytes, code, hasher } = this // First we store promise to avoid a race condition if cid is called // whlie promise is pending. - const promise = createCID(hasher, bytes, code, this) + const promise = createCID(hasher, bytes, code) this._cid = promise const cid = await promise // Once promise resolves we store an actual CID. @@ -94,18 +41,49 @@ export class Block { } links () { - return links(this.data, [], this) + return links(this.data, []) } tree () { - return tree(this.data, [], this) + return tree(this.data, []) } /** * @param {string} path */ get (path) { - return get(this.data, path.split('/').filter(Boolean), this) + return get(this.data, path.split('/').filter(Boolean)) + } + + /** + * @template T + * @param {Encoder} codec + * @param {BlockConfig} options + */ + static encoder (codec, options) { + return new BlockEncoder(codec, options) + } + + /** + * @template T + * @param {Decoder} codec + * @param {BlockConfig} options + */ + static decoder (codec, options) { + return new BlockDecoder(codec, options) + } + + /** + * @template T + * @param {Object} codec + * @param {Encoder} codec.encoder + * @param {Decoder} codec.decoder + * @param {Object} [options] + * @returns {BlockCodec} + */ + + static codec ({ encoder, decoder }, options) { + return new BlockCodec(encoder, decoder, options) } } @@ -113,29 +91,28 @@ export class Block { * @template T * @param {T} source * @param {Array} base - * @param {BlockConfig} config * @returns {Iterable<[string, CID]>} */ -const links = function * (source, base, config) { +const links = function * (source, base) { for (const [key, value] of Object.entries(source)) { const path = [...base, key] if (value != null && typeof value === 'object') { if (Array.isArray(value)) { for (const [index, element] of value.entries()) { const elementPath = [...path, index] - const cid = asCID(element, config) + const cid = CID.asCID(element) if (cid) { yield [elementPath.join('/'), cid] } else if (typeof element === 'object') { - yield * links(element, elementPath, config) + yield * links(element, elementPath) } } } else { - const cid = asCID(value, config) + const cid = CID.asCID(value) if (cid) { yield [path.join('/'), cid] } else { - yield * links(value, path, config) + yield * links(value, path) } } } @@ -146,24 +123,23 @@ const links = function * (source, base, config) { * @template T * @param {T} source * @param {Array} base - * @param {BlockConfig} config * @returns {Iterable} */ -const tree = function * (source, base, config) { +const tree = function * (source, base) { for (const [key, value] of Object.entries(source)) { const path = [...base, key] yield path.join('/') - if (value != null && typeof value === 'object' && !asCID(value, config)) { + if (value != null && typeof value === 'object' && !CID.asCID(value)) { if (Array.isArray(value)) { for (const [index, element] of value.entries()) { const elementPath = [...path, index] yield elementPath.join('/') - if (typeof element === 'object' && !asCID(elementPath, config)) { - yield * tree(element, elementPath, config) + if (typeof element === 'object' && !CID.asCID(element)) { + yield * tree(element, elementPath) } } } else { - yield * tree(value, path, config) + yield * tree(value, path) } } } @@ -173,16 +149,15 @@ const tree = function * (source, base, config) { * @template T * @param {T} source * @param {string[]} path - * @param {BlockConfig} config */ -const get = (source, path, config) => { +const get = (source, path) => { let node = source for (const [index, key] of path.entries()) { node = node[key] if (node == null) { throw new Error(`Object has no property at ${path.slice(0, index - 1).map(part => `[${JSON.stringify(part)}]`).join('')}`) } - const cid = asCID(node, config) + const cid = CID.asCID(node) if (cid) { return { value: cid, remaining: path.slice(index).join('/') } } @@ -195,12 +170,11 @@ const get = (source, path, config) => { * @param {Hasher} hasher * @param {Uint8Array} bytes * @param {number} code - * @param {BlockConfig} context */ -const createCID = async (hasher, bytes, code, context) => { +const createCID = async (hasher, bytes, code) => { const multihash = await hasher.digest(bytes) - return createV1(code, multihash, context) + return CID.createV1(code, multihash) } /** @@ -239,61 +213,56 @@ class BlockCodec { } /** - * @typedef {Object} Config - * @property {MultibaseCodec} base - * @property {MultibaseCodec<'z'>} base58btc + * @class + * @template T */ - -class BlockAPI { +class BlockEncoder { /** + * @param {Encoder} codec * @param {BlockConfig} config */ - constructor (config) { + constructor (codec, config) { + this.codec = codec this.config = config - this.Block = Block } /** - * @template T - * @param {Encoder} options - * @param {Partial} [options] + * @param {T} data + * @param {BlockConfig} [options] + * @returns {Block} */ - encoder (codec, options) { - return new BlockEncoder(codec, { ...this.config, ...options }) + encode (data, options) { + const { codec } = this + const bytes = codec.encode(data) + return new Block(null, codec.code, data, bytes, { ...this.config, ...options }) } +} +/** + * @class + * @template T + */ +class BlockDecoder { /** - * @template T - * @param {Decoder} options - * @param {Partial} [options] + * @param {Decoder} codec + * @param {BlockConfig} config */ - decoder (codec, options) { - return new BlockDecoder(codec, { ...this.config, ...options }) + constructor (codec, config) { + this.codec = codec + this.config = config } /** - * @template T - * @param {Object} codec - * @param {Encoder} codec.encoder - * @param {Decoder} codec.decoder + * @param {Uint8Array} bytes * @param {Partial} [options] - * @returns {BlockCodec} + * @returns {Block} */ - - codec ({ encoder, decoder }, options) { - return new BlockCodec(encoder, decoder, { ...this.config, ...options }) + decode (bytes, options) { + const data = this.codec.decode(bytes) + return new Block(null, this.codec.code, data, bytes, { ...this.config, ...options }) } } - -/** - * @param {BlockConfig} config - */ -export const configure = (config) => new BlockAPI(config) - -export default configure - /** - * @typedef {import('./cid').CID} CID * @typedef {import('./block/interface').Config} BlockConfig * @typedef {import('./hashes/interface').MultihashHasher} Hasher **/ diff --git a/src/block/interface.ts b/src/block/interface.ts index 4c1e8664..4d8af806 100644 --- a/src/block/interface.ts +++ b/src/block/interface.ts @@ -1,9 +1,6 @@ // Block -import { MultibaseCodec } from "../bases/interface" -import { BlockEncoder as Encoder, BlockDecoder as Decoder } from "../codecs/interface" -import { MultihashHasher as Hasher } from "../hashes/interface" -import { CID } from "../cid" - +import CID from "../cid" +import { MultihashHasher } from '../hashes/interface' // Just a representation for awaitable `T`. export type Awaitable = @@ -16,24 +13,6 @@ export interface Block { encode(): Awaitable } - export interface Config { - /** - * Multihasher to be use for the CID of the block. Will use a default - * if not provided. - */ - hasher: Hasher - /** - * Base encoder that will be passed by the CID of the block. - */ - base: MultibaseCodec - - /** - * Base codec that will be used with CIDv0. - */ - base58btc: MultibaseCodec<'z'> + hasher: MultihashHasher } - - - - diff --git a/src/cid.js b/src/cid.js index cb1d6f9e..1fcd28ff 100644 --- a/src/cid.js +++ b/src/cid.js @@ -2,11 +2,11 @@ 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' /** * @typedef {import('./hashes/interface').MultihashDigest} MultihashDigest - * @typedef {import('./bases/interface').BaseEncoder} BaseEncoder - * @typedef {import('./bases/interface').BaseDecoder} BaseDecoder */ /** @@ -15,30 +15,24 @@ import * as Digest from './hashes/digest.js' */ /** - * @typedef {import('./cid/interface').Config} Config + * @template Prefix + * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder */ -/** - * @implements {Config} - */ -export class CID { +export default class CID { /** * @param {0|1} version * @param {number} code * @param {MultihashDigest} multihash * @param {Uint8Array} bytes - * @param {Config} config * */ - constructor (version, code, multihash, bytes, { base, base58btc }) { + constructor (version, code, multihash, bytes) { this.code = code this.version = version this.multihash = multihash this.bytes = bytes - this.base = base - this.base58btc = base58btc - // ArrayBufferView this.byteOffset = bytes.byteOffset this.byteLength = bytes.byteLength @@ -76,18 +70,18 @@ export class CID { return this } default: { - if (this.code !== DAG_PB_CODE) { + const { code, multihash } = this + + if (code !== DAG_PB_CODE) { throw new Error('Cannot convert a non dag-pb CID to CIDv0') } - const { code, digest } = this.multihash - // sha2-256 - if (code !== SHA_256_CODE) { + if (multihash.code !== SHA_256_CODE) { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return createV0(Digest.decode(digest), this) + return CID.createV0(multihash) } } } @@ -100,7 +94,7 @@ export class CID { case 0: { const { code, digest } = this.multihash const multihash = Digest.create(code, digest) - return createV1(this.code, multihash, this) + return CID.createV1(this.code, multihash) } case 1: { return this @@ -123,14 +117,15 @@ export class CID { /** * @param {MultibaseEncoder} [base] + * @returns {string} */ toString (base) { const { bytes, version, _baseCache } = this switch (version) { case 0: - return toStringV0(bytes, _baseCache, base || this.base58btc.encoder) + return toStringV0(bytes, _baseCache, base || base58btc.encoder) default: - return toStringV1(bytes, _baseCache, base || this.base.encoder) + return toStringV1(bytes, _baseCache, base || base32.encoder) } } @@ -178,198 +173,168 @@ export class CID { get prefix () { throw new Error('"prefix" property is deprecated') } -} - -class CIDAPI { - /** - * Returns API for working with CIDs. - * @param {Config} config - */ - constructor (config) { - this.config = config - this.CID = CID - } - - create (version, code, digest) { - return create(version, code, digest, this.config) - } - - parse (cid) { - return parse(cid, this.config) - } - - decode (cid) { - return decode(cid, this.config) - } - - asCID (input) { - return asCID(input, this.config) - } /** - * Creates a new CID from either string, binary or an object representation. - * Throws an error if provided `value` is not a valid CID. + * 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. * - * @param {CID|string|Uint8Array} value - * @returns {CID} + * 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} */ - from (value) { - if (typeof value === 'string') { - return parse(value, this.config) - } else if (value instanceof Uint8Array) { - return decode(value, this.config) + 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 { - const cid = asCID(value, this.config) - if (cid) { - // If we got the same CID back we create a copy. - if (cid === value) { - return new CID(cid.version, cid.code, cid.multihash, cid.bytes, this.config) - } else { - return cid - } - } else { - throw new TypeError(`Can not create CID from given value ${value}`) - } + // Otherwise value is not a CID (or an incompatible version of it) in + // which case we return `null`. + return null } } -} -/** + /** * * @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. - * @param {Config} config - Base encoding that will be used for toString - * serialization. If omitted configured default will be used. * @returns {CID} */ -export const create = (version, code, digest, config) => { - 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, config) - } - } - case 1: { - const bytes = encodeCID(version, code, digest.bytes) - return new CID(version, code, digest, bytes, config) + static create (version, code, digest) { + if (typeof code !== 'number') { + throw new Error('String codecs are no longer supported') } - default: { - throw new Error('Invalid version') + + 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) + } + } + 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. - * @param {Config} config */ -export const createV0 = (digest, config) => create(0, DAG_PB_CODE, digest, config) + 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. - * @param {Config} config - Base encoding used of the serialziation. If - * omitted configured default is used. * @returns {CID} */ -export const createV1 = (code, digest, config) => create(1, code, digest, config) + static createV1 (code, digest) { + return CID.create(1, code, digest) + } -/** + /** + * 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}`) + } + } + } + + /** * 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 {Config} config + * @param {MultibaseDecoder} [base] */ -export const parse = (source, config) => { - const { base, base58btc } = config - const [name, bytes] = source[0] === 'Q' - ? [BASE_58_BTC, base58btc.decoder.decode(`${BASE_58_BTC_PREFIX}${source}`)] - : [base.encoder.name, base.decoder.decode(source)] - - const cid = decode(bytes, config) - // Cache string representation to avoid computing it on `this.toString()` - // @ts-ignore - Can't access private - cid._baseCache.set(name, source) - - return cid + static parse (source, base) { + const [prefix, bytes] = parseCIDtoBytes(source, base) + + 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) + + return cid + } } -/** - * 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 - * @param {Config} config - */ -export const decode = (cid, config) => { - const [version, offset] = varint.decode(cid) - switch (version) { - // CIDv0 - case 18: { - const multihash = Digest.decode(cid) - return createV0(multihash, config) +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}`)] } - // CIDv1 - case 1: { - const [code, length] = varint.decode(cid.subarray(offset)) - const digest = Digest.decode(cid.subarray(offset + length)) - return createV1(code, digest, config) + 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: { - throw new RangeError(`Invalid CID version ${version}`) + if (base == null) { + throw Error('To parse non base32 or base56btc encoded CID multibase decoder must be provided') + } + return [source[0], base.decode(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 - * @param {Config} config - * @returns {CID|null} - */ -export const asCID = (value, config) => { - 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, config } = value - return new CID(version, code, multihash, bytes, config) - } 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 create(version, code, digest, config) - } else { - // Otherwise value is not a CID (or an incompatible version of it) in - // which case we return `null`. - return null - } -} /** * * @param {Uint8Array} bytes @@ -377,14 +342,15 @@ export const asCID = (value, config) => { * @param {MultibaseEncoder<'z'>} base */ const toStringV0 = (bytes, cache, base) => { - const cid = cache.get(BASE_58_BTC) + 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 multibase = base.encode(bytes) - if (multibase[0] !== BASE_58_BTC_PREFIX) { - throw Error('CIDv0 can only be encoded to base58btc encoding, invalid') - } - const cid = multibase.slice(1) - cache.set(BASE_58_BTC, cid) + const cid = base.encode(bytes).slice(1) + cache.set(prefix, cid) return cid } else { return cid @@ -392,31 +358,23 @@ const toStringV0 = (bytes, cache, base) => { } /** - * @template Prefix + * @template {string} Prefix * @param {Uint8Array} bytes * @param {Map} cache * @param {MultibaseEncoder} base */ const toStringV1 = (bytes, cache, base) => { - const cid = cache.get(base.name) + const { prefix } = base + const cid = cache.get(prefix) if (cid == null) { const cid = base.encode(bytes) - cache.set(base.name, cid) + cache.set(prefix, cid) return cid } else { return cid } } -/** - * @param {Config} config - */ -export const configure = config => new CIDAPI(config) - -export default configure - -const BASE_58_BTC = 'base58btc' -const BASE_58_BTC_PREFIX = 'z' const DAG_PB_CODE = 0x70 const SHA_256_CODE = 0x12 diff --git a/src/cid/interface.ts b/src/cid/interface.ts deleted file mode 100644 index 00ec97bb..00000000 --- a/src/cid/interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MultibaseCodec, BaseCodec } from "../bases/interface.js" - - -export interface Config { - /** - * Multibase codec used by CID to encode / decode to and out of - * string representation. - */ - base: MultibaseCodec - /** - * CIDv0 requires base58btc encoding decoding so CID must be - * provided means to perform that task. - */ - base58btc: MultibaseCodec<'z'> -} \ No newline at end of file diff --git a/src/hashes/digest.js b/src/hashes/digest.js index da5208e6..ea7a29bb 100644 --- a/src/hashes/digest.js +++ b/src/hashes/digest.js @@ -34,7 +34,7 @@ export const decode = (multihash) => { const digest = bytes.subarray(sizeOffset + digestOffset) if (digest.byteLength !== size) { - throw new Error('Given multihash has incorrect length') + throw new Error('Incorrect length') } return new Digest(code, size, digest, bytes) diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js index 310f15b7..0ce42066 100644 --- a/src/hashes/hasher.js +++ b/src/hashes/hasher.js @@ -39,8 +39,12 @@ export class Hasher { * @returns {Promise} */ async digest (input) { - const digest = await this.encode(input) - return Digest.create(this.code, digest) + 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') + } } } diff --git a/src/index.js b/src/index.js index 2c8ddd2a..3ce64497 100644 --- a/src/index.js +++ b/src/index.js @@ -1,29 +1,11 @@ // @ts-check -import cid, { CID } from './cid.js' -import block, { Block } from './block.js' +import CID from './cid.js' +import Block from './block.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' -export { CID, Block, cid, block, hasher, digest, varint, bytes, codec } - -/** - * - * @param {import('./block/interface').Config} config - */ -export const configure = (config) => { - return { - cid: cid(config), - block: block(config), - hasher, - codec, - digest, - varint, - bytes - } -} - -export default configure +export { CID, Block, hasher, digest, varint, bytes, codec } diff --git a/src/legacy.js b/src/legacy.js index 79a42afc..803a4d48 100644 --- a/src/legacy.js +++ b/src/legacy.js @@ -3,31 +3,27 @@ import OldCID from 'cids' import * as bytes from './bytes.js' import { Buffer } from 'buffer' -import * as LibCID from './cid.js' +import CID from './cid.js' /** * @template T - * @param {Object} multiformats - * @param {Object} multiformats.hashes - * @param {Object>} multiformats.codecs - * @param {MultibaseCodec} multiformats.base - * @param {MultibaseCodec<'z'>} multiformats.base58btc * @param {BlockCodec} codec + * @param {Object} options + * @param {Object} options.hashes */ -const legacy = (multiformats, codec) => { +const legacy = (codec, { hashes }) => { const toLegacy = obj => { if (OldCID.isCID(obj)) { return obj } - const newCID = LibCID.asCID(obj, multiformats) + const newCID = CID.asCID(obj) if (newCID) { - const { version, multihash: { bytes } } = newCID + const { version, code, multihash: { bytes } } = newCID const { buffer, byteOffset, byteLength } = bytes - const { name } = multiformats.codecs[newCID.code] const multihash = Buffer.from(buffer, byteOffset, byteLength) - return new OldCID(version, name, multihash) + return new OldCID(version, code, multihash) } if (bytes.isBinary(obj)) { @@ -44,7 +40,7 @@ const legacy = (multiformats, codec) => { } const fromLegacy = obj => { - const cid = LibCID.asCID(obj, multiformats) + const cid = CID.asCID(obj) if (cid) return cid if (bytes.isBinary(obj)) return bytes.coerce(obj) if (obj && typeof obj === 'object') { @@ -78,7 +74,7 @@ const legacy = (multiformats, codec) => { /** @type {{cidVersion:1, hashAlg: string}} */ const defaults = { cidVersion: 1, hashAlg: 'sha2-256' } const { cidVersion, hashAlg } = { ...defaults, ...opts } - const hasher = multiformats.hashes[hashAlg] + const hasher = hashes[hashAlg] if (hasher == null) { throw new Error(`Hasher for ${hashAlg} was not provided in the configuration`) } @@ -96,7 +92,7 @@ const legacy = (multiformats, codec) => { const resolve = (buff, path) => { let value = codec.decode(buff) const entries = path.split('/').filter(x => x) - while (path.length) { + while (entries.length) { value = value[entries.shift()] if (typeof value === 'undefined') throw new Error('Not found') if (OldCID.isCID(value)) { diff --git a/test/test-cid.js b/test/test-cid.js index 8e025f1a..be53b658 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -1,14 +1,15 @@ /* 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 @@ -33,85 +34,68 @@ const testThrowAny = async fn => { } 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) 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 +106,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 +115,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,9 +140,9 @@ 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) @@ -168,22 +152,22 @@ describe('CID', () => { /* 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 +178,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) }) }) @@ -205,13 +189,14 @@ describe('CID', () => { const h2 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1o' test('.equals v0 to v0', () => { - same(CID.from(h1).equals(CID.from(h1)), true) - same(CID.from(h1).equals(CID.from(h2)), false) + same(CID.parse(h1).equals(CID.parse(h1)), true) + same(CID.parse(h1).equals(CID.parse(h2)), false) }) test('.equals v0 to v1 and vice versa', () => { const cidV1Str = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' - const cidV1 = CID.from(cidV1Str) + const cidV1 = CID.parse(cidV1Str) + const cidV0 = cidV1.toV0() same(cidV0.equals(cidV1), false) @@ -221,83 +206,91 @@ describe('CID', () => { }) 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') }) @@ -305,51 +298,52 @@ describe('CID', () => { 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 +359,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,23 +369,23 @@ 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)) @@ -400,8 +393,8 @@ describe('CID', () => { }) 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 +403,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,23 +411,23 @@ 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()') }) @@ -442,10 +435,10 @@ describe('CID', () => { test('invalid CID version', async () => { const encoded = varint.encode(2) - await testThrow(() => CID.from(encoded), 'Invalid CID version 2') + 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..3ec2db72 100644 --- a/test/test-legacy.js +++ b/test/test-legacy.js @@ -1,8 +1,13 @@ /* 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 @@ -15,17 +20,22 @@ const testThrow = (fn, message) => { } 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 +48,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,7 +67,8 @@ 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)) }) test('resolve', () => { const fixture = custom.util.serialize({ diff --git a/test/test-multibase.js b/test/test-multibase.js index 512b604e..3328b0e4 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 @@ -22,84 +22,70 @@ const testThrow = (fn, message) => { } 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) }) + test('decode int failure', () => { const msg = 'Can only multibase decode strings' - testThrow(() => multibase.decode(1), msg) + testThrow(() => base32.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) { + test(`encode/decode ${base.name}`, () => { + const encoded = base.encode(buff) + const decoded = base.decode(encoded) + same(decoded, buff) + }) + } + } } describe('base16', () => { - baseTest(base16) + baseTest(b16) }) describe('base32', () => { - baseTest(base32) + baseTest(b32) }) describe('base58', () => { - baseTest(base58) + baseTest(b58) }) describe('base64', () => { - baseTest(base64) - }) - test('has', () => { - same(basicsMultibase.has('E'), false) - same(basicsMultibase.has('baseNope'), false) - same(basicsMultibase.has('base32'), true) - same(basicsMultibase.has('c'), true) + baseTest(b64) }) }) diff --git a/test/test-multicodec.js b/test/test-multicodec.js index a14256ba..b42eeffc 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -32,7 +32,7 @@ describe('multicodec', () => { }) test('raw cannot encode string', async () => { - await testThrow(() => raw.encode('asdf', 'raw'), 'Unknown type, must be binary type') + await testThrow(() => raw.encode('asdf'), 'Unknown type, must be binary type') }) test('add with function', () => { diff --git a/test/test-multihash.js b/test/test-multihash.js index c469be86..118132b1 100644 --- a/test/test-multihash.js +++ b/test/test-multihash.js @@ -1,16 +1,14 @@ /* 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' +import { sha256, sha512, __browser } from 'multiformats/hashes/sha2' +import { decode as decodeDigest, create as createDigest } from 'multiformats/hashes/digest' const same = assert.deepStrictEqual 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 sample = (code, size, hex) => { const toHex = (i) => { @@ -18,7 +16,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) => { @@ -32,77 +30,69 @@ const testThrowAsync = async (fn, message) => { } 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) }) }) From 5e9a43370d8f9a86c9d8cda797afec7658bc4936 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 18 Sep 2020 16:38:41 -0700 Subject: [PATCH 10/26] chore: add code type info --- src/bases/base.js | 2 +- src/block.js | 52 ++++++++++++++++++++++++----------------- src/codecs/codec.js | 25 +++++++++++++------- src/codecs/interface.ts | 21 +++++++++++------ src/legacy.js | 3 ++- 5 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/bases/base.js b/src/bases/base.js index 3d92e418..219630fe 100644 --- a/src/bases/base.js +++ b/src/bases/base.js @@ -136,7 +136,7 @@ class ComposedDecoder { /** @type {Object>} */ this.decoders = decoders // so that we can distinguish between unibase and multibase - /** @type {void} */ + /** @type {null} */ this.prefix = null } diff --git a/src/block.js b/src/block.js index 5a0ca433..3bed5724 100644 --- a/src/block.js +++ b/src/block.js @@ -3,13 +3,14 @@ import CID from './cid.js' /** + * @template {number} Code * @template T * @class */ export default class Block { /** * @param {CID|null} cid - * @param {number} code + * @param {Code} code * @param {T} data * @param {Uint8Array} bytes * @param {BlockConfig} config @@ -56,8 +57,9 @@ export default class Block { } /** + * @template {number} Code * @template T - * @param {Encoder} codec + * @param {Encoder} codec * @param {BlockConfig} options */ static encoder (codec, options) { @@ -65,8 +67,9 @@ export default class Block { } /** + * @template {number} Code * @template T - * @param {Decoder} codec + * @param {Decoder} codec * @param {BlockConfig} options */ static decoder (codec, options) { @@ -74,12 +77,13 @@ export default class Block { } /** + * @template {number} Code * @template T * @param {Object} codec - * @param {Encoder} codec.encoder - * @param {Decoder} codec.decoder - * @param {Object} [options] - * @returns {BlockCodec} + * @param {Encoder} codec.encoder + * @param {Decoder} codec.decoder + * @param {BlockConfig} options + * @returns {BlockCodec} */ static codec ({ encoder, decoder }, options) { @@ -178,12 +182,13 @@ const createCID = async (hasher, bytes, code) => { } /** + * @template {number} Code * @template T */ class BlockCodec { /** - * @param {Encoder} encoder - * @param {Decoder} decoder + * @param {Encoder} encoder + * @param {Decoder} decoder * @param {BlockConfig} config */ @@ -195,8 +200,8 @@ class BlockCodec { /** * @param {Uint8Array} bytes - * @param {BlockConfig} [options] - * @returns {Block} + * @param {Partial} [options] + * @returns {Block} */ decode (bytes, options) { return this.decoder.decode(bytes, { ...this.config, ...options }) @@ -204,8 +209,8 @@ class BlockCodec { /** * @param {T} data - * @param {BlockConfig} [options] - * @returns {Block} + * @param {Partial} [options] + * @returns {Block} */ encode (data, options) { return this.encoder.encode(data, { ...this.config, ...options }) @@ -214,11 +219,12 @@ class BlockCodec { /** * @class + * @template {number} Code * @template T */ class BlockEncoder { /** - * @param {Encoder} codec + * @param {Encoder} codec * @param {BlockConfig} config */ constructor (codec, config) { @@ -228,8 +234,8 @@ class BlockEncoder { /** * @param {T} data - * @param {BlockConfig} [options] - * @returns {Block} + * @param {Partial} [options] + * @returns {Block} */ encode (data, options) { const { codec } = this @@ -240,11 +246,12 @@ class BlockEncoder { /** * @class + * @template {number} Code * @template T */ class BlockDecoder { /** - * @param {Decoder} codec + * @param {Decoder} codec * @param {BlockConfig} config */ constructor (codec, config) { @@ -255,7 +262,7 @@ class BlockDecoder { /** * @param {Uint8Array} bytes * @param {Partial} [options] - * @returns {Block} + * @returns {Block} */ decode (bytes, options) { const data = this.codec.decode(bytes) @@ -283,16 +290,19 @@ class BlockDecoder { */ /** + * @template {number} Code * @template T - * @typedef {import('./codecs/interface').BlockEncoder} Encoder + * @typedef {import('./codecs/interface').BlockEncoder} Encoder */ /** + * @template {number} Code * @template T - * @typedef {import('./codecs/interface').BlockDecoder} Decoder + * @typedef {import('./codecs/interface').BlockDecoder} Decoder */ /** + * @template {number} Code * @template T - * @typedef {import('./codecs/interface').BlockCodec} Codec + * @typedef {import('./codecs/interface').BlockCodec} Codec */ diff --git a/src/codecs/codec.js b/src/codecs/codec.js index 76ca9f5f..5dd08675 100644 --- a/src/codecs/codec.js +++ b/src/codecs/codec.js @@ -15,8 +15,9 @@ export const codec = ({ name, code, decode, encode }) => new Codec(name, code, encode, decode) /** + * @template {number} Code * @template T - * @typedef {import('./interface').BlockEncoder} BlockEncoder + * @typedef {import('./interface').BlockEncoder} BlockEncoder */ /** @@ -24,7 +25,7 @@ export const codec = ({ name, code, decode, encode }) => * @template T * @template {string} Name * @template {number} Code - * @implements {BlockEncoder} + * @implements {BlockEncoder} */ export class Encoder { /** @@ -40,28 +41,34 @@ export class Encoder { } /** + * @template {number} Code * @template T - * @typedef {import('./interface').BlockDecoder} BlockDecoder + * @typedef {import('./interface').BlockDecoder} BlockDecoder */ /** * @class + * @template {number} Code * @template T - * @implements {BlockDecoder} + * @implements {BlockDecoder} */ export class Decoder { /** + * @param {string} name + * @param {Code} code * @param {(bytes:Uint8Array) => T} decode */ - constructor (code, decode) { + constructor (name, code, decode) { + this.name = name this.code = code this.decode = decode } } /** + * @template {number} Code * @template T - * @typedef {import('./interface').BlockCodec} BlockCodec + * @typedef {import('./interface').BlockCodec} BlockCodec */ /** @@ -69,7 +76,7 @@ export class Decoder { * @template {string} Name * @template {number} Code * @template T - * @implements {BlockCodec} + * @implements {BlockCodec} */ export class Codec { /** @@ -86,8 +93,8 @@ export class Codec { } get decoder () { - const { name, decode } = this - const decoder = new Decoder(name, decode) + const { name, code, decode } = this + const decoder = new Decoder(name, code, decode) Object.defineProperty(this, 'decoder', { value: decoder }) return decoder } diff --git a/src/codecs/interface.ts b/src/codecs/interface.ts index d55cec93..6ce17c0d 100644 --- a/src/codecs/interface.ts +++ b/src/codecs/interface.ts @@ -1,18 +1,18 @@ /** * IPLD encoder part of the codec. */ -export interface BlockEncoder { +export interface BlockEncoder { name: string - code: number - encode(data: T): Uint8Array + code: Code + encode(data: T): ByteView } /** * IPLD decoder part of the codec. */ -export interface BlockDecoder { - code: number - decode(bytes: Uint8Array): T +export interface BlockDecoder { + code: Code + decode(bytes: ByteView): T } /** @@ -20,5 +20,12 @@ export interface BlockDecoder { * separate those capabilties as sender requires encoder and receiver * requires decoder. */ -export interface BlockCodec extends BlockEncoder, BlockDecoder { } +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. +type ByteView = + | Uint8Array + | Uint8Array & { data: T } diff --git a/src/legacy.js b/src/legacy.js index 803a4d48..d189526d 100644 --- a/src/legacy.js +++ b/src/legacy.js @@ -27,6 +27,7 @@ const legacy = (codec, { hashes }) => { } if (bytes.isBinary(obj)) { + // @ts-ignore return Buffer.from(obj) } @@ -93,7 +94,7 @@ const legacy = (codec, { hashes }) => { let value = codec.decode(buff) const entries = path.split('/').filter(x => x) while (entries.length) { - value = value[entries.shift()] + value = value[/** @type {string} */(entries.shift())] if (typeof value === 'undefined') throw new Error('Not found') if (OldCID.isCID(value)) { return { value, remainderPath: entries.join('/') } From 8bcdb5d224d4aa1ae16cf08ad4a466b88d062f79 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 18 Sep 2020 19:39:14 -0700 Subject: [PATCH 11/26] fix: switch to field base check over instanceof --- src/bases/base.js | 61 ++++++++++++++++++++++++++---------------- src/bases/interface.ts | 13 +++++++-- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/bases/base.js b/src/bases/base.js index 219630fe..750d4e65 100644 --- a/src/bases/base.js +++ b/src/bases/base.js @@ -51,15 +51,18 @@ class Encoder { } /** - * @template {string} T - * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder + * @template {string} Prefix + * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder */ /** - * @template {string} T - * @typedef {import('./interface').UnibaseDecoder} UnibaseDecoder + * @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 @@ -107,37 +110,48 @@ class Decoder { * @returns {ComposedDecoder} */ or (decoder) { - if (decoder instanceof ComposedDecoder) { - return new ComposedDecoder({ [this.prefix]: this, ...decoder.decoders }) - } else { - return new ComposedDecoder({ [this.prefix]: this, [decoder.prefix]: 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 { /** - * @template {string} T - * @param {UnibaseDecoder} decoder - * @returns {ComposedDecoder} + * @template {string} Prefix + * @param {UnibaseDecoder} decoder + * @returns {ComposedDecoder} */ static from (decoder) { - return new ComposedDecoder({ [decoder.prefix]: decoder }) + return new ComposedDecoder(/** @type {Decoders} */ ({ + [decoder.prefix]: decoder + })) } /** - * @param {Object>} decoders + * @param {Record>} decoders */ constructor (decoders) { - /** @type {Object>} */ this.decoders = decoders - // so that we can distinguish between unibase and multibase - /** @type {null} */ - this.prefix = null } /** @@ -146,11 +160,12 @@ class ComposedDecoder { * @returns {ComposedDecoder} */ or (decoder) { - if (decoder instanceof ComposedDecoder) { - return new ComposedDecoder({ ...this.decoders, ...decoder.decoders }) - } else { - return new ComposedDecoder({ ...this.decoders, [decoder.prefix]: decoder }) - } + /** @type {Decoders} */ + const other = (decoder.decoders || { [decoder.prefix]: decoder }) + return new ComposedDecoder({ + ...this.decoders, + ...other + }) } /** diff --git a/src/bases/interface.ts b/src/bases/interface.ts index da4ddf36..474fe26c 100644 --- a/src/bases/interface.ts +++ b/src/bases/interface.ts @@ -37,7 +37,9 @@ export interface BaseCodec { * Multibase represets base encoded strings with a prefix first character * describing it's encoding. */ -export type Multibase = string +export type Multibase = + | string + | string & { [0]: Prefix } /** * Multibase encoder for the specific base encoding encodes bytes into @@ -86,5 +88,12 @@ export interface MultibaseCodec { export interface UnibaseDecoder extends MultibaseDecoder { - prefix: Prefix + // 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> } From 0c2ac57227c0796cd7353e0a712c985503192d1f Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 18 Sep 2020 19:40:16 -0700 Subject: [PATCH 12/26] core: export ByteView type --- src/codecs/interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codecs/interface.ts b/src/codecs/interface.ts index 6ce17c0d..3474ff38 100644 --- a/src/codecs/interface.ts +++ b/src/codecs/interface.ts @@ -26,6 +26,6 @@ export interface BlockCodec extends BlockEncoder = +export type ByteView = | Uint8Array | Uint8Array & { data: T } From 005cb529e636d3fa34c4ed982b8d47a4bd8b6336 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Wed, 23 Sep 2020 04:31:37 +0000 Subject: [PATCH 13/26] fix: browser polyfill for deepStrictEquals doesn't supprot Uint8Arrays --- test/test-cid.js | 16 +++++++++++++--- test/test-multibase.js | 2 +- test/test-multihash.js | 8 +++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/test/test-cid.js b/test/test-cid.js index be53b658..33030c80 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -11,7 +11,13 @@ import { sha256, sha512 } from 'multiformats/hashes/sha2' import util from 'util' 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 @@ -63,7 +69,8 @@ describe('CID', () => { 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(), base58btc.baseEncode(hash.bytes)) }) @@ -146,7 +153,10 @@ describe('CID', () => { 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 diff --git a/test/test-multibase.js b/test/test-multibase.js index 3328b0e4..872d8717 100644 --- a/test/test-multibase.js +++ b/test/test-multibase.js @@ -67,7 +67,7 @@ describe('multibase', () => { const buff = bytes.fromString('test') const baseTest = bases => { for (const base of Object.values(bases)) { - if (base) { + if (base && base.name) { test(`encode/decode ${base.name}`, () => { const encoded = base.encode(buff) const decoded = base.decode(encoded) diff --git a/test/test-multihash.js b/test/test-multihash.js index 118132b1..8158023b 100644 --- a/test/test-multihash.js +++ b/test/test-multihash.js @@ -6,10 +6,16 @@ import invalid from './fixtures/invalid-multihash.js' import crypto from 'crypto' import { sha256, sha512, __browser } from 'multiformats/hashes/sha2' import { decode as decodeDigest, create as createDigest } from 'multiformats/hashes/digest' -const same = assert.deepStrictEqual const test = it 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) => { if (typeof i === 'string') return i From e43c0d3066d08d448687105f35c6723983bbd007 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 22 Sep 2020 23:19:25 -0700 Subject: [PATCH 14/26] fix: coverage for cid --- src/cid.js | 1 + test/test-cid.js | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/cid.js b/src/cid.js index 1fcd28ff..7d2b54ca 100644 --- a/src/cid.js +++ b/src/cid.js @@ -99,6 +99,7 @@ export default class CID { case 1: { return this } + /* c8 ignore next 3 */ default: { throw Error(`Can not convert CID version ${this.version} to version 0. This is a bug please report`) } diff --git a/test/test-cid.js b/test/test-cid.js index 33030c80..419819c3 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -304,6 +304,19 @@ describe('CID', () => { 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', () => { @@ -402,6 +415,48 @@ describe('CID', () => { assert.strictEqual(cid5.code, 85) }) + 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()) + same(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)) + same(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()) + same(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) + same(cid, parsed) + }) + }) + test('new CID from old CID', async () => { const hash = await sha256.digest(Buffer.from('abc')) const cid = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash.bytes))) From 262ba3106a1db4b4903a690c85b8ea81b7a495a4 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 22 Sep 2020 23:19:36 -0700 Subject: [PATCH 15/26] fix: hashes coverage --- src/hashes/hasher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js index 0ce42066..6b14de2d 100644 --- a/src/hashes/hasher.js +++ b/src/hashes/hasher.js @@ -44,6 +44,7 @@ export class Hasher { return Digest.create(this.code, digest) } else { throw Error('Unknown type, must be binary type') + /* c8 ignore next 1 */ } } } From a2d676310c5823881e26cf287e32bf6b611015c8 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 22 Sep 2020 23:19:55 -0700 Subject: [PATCH 16/26] chore: disable coverage for vendor --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ae8e6e29..ac77ec7c 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,11 @@ "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", - "test:node": "hundreds mocha test/test-*.js", + "test:node": "hundreds --exclude vendor mocha test/test-*.js -x vendor/**", "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" }, "keywords": [], "author": "Mikeal Rogers (https://www.mikealrogers.com/)", From 64d39775629051ef9e189b5a23c9db211978b076 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 23 Sep 2020 00:42:49 -0700 Subject: [PATCH 17/26] chore: improve test coverage --- src/bases/base.js | 15 ++------------- src/varint.js | 8 ++------ test/test-cid.js | 22 ++++++++++++++++++---- test/test-legacy.js | 12 ++++++++---- test/test-multibase.js | 31 +++++++++++++++++++++++++++++++ test/test-multicodec.js | 17 +++++++++++++++++ test/test-multihash.js | 1 + 7 files changed, 79 insertions(+), 27 deletions(-) diff --git a/src/bases/base.js b/src/bases/base.js index 750d4e65..5b7b7046 100644 --- a/src/bases/base.js +++ b/src/bases/base.js @@ -96,7 +96,7 @@ class Decoder { return this.baseDecode(text.slice(1)) } default: { - throw Error(`${this.name} expects input starting with ${this.prefix} and can not decode "${text}"`) + throw Error(`Unable to decode multibase string ${JSON.stringify(text)}, ${this.name} decoder only supports inputs prefixed with ${this.prefix}`) } } } else { @@ -136,17 +136,6 @@ class Decoder { * @implements {CombobaseDecoder} */ class ComposedDecoder { - /** - * @template {string} Prefix - * @param {UnibaseDecoder} decoder - * @returns {ComposedDecoder} - */ - static from (decoder) { - return new ComposedDecoder(/** @type {Decoders} */ ({ - [decoder.prefix]: decoder - })) - } - /** * @param {Record>} decoders */ @@ -178,7 +167,7 @@ class ComposedDecoder { if (decoder) { return decoder.decode(input) } else { - throw RangeError(`Unable to decode multibase string ${input}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) + throw RangeError(`Unable to decode multibase string ${JSON.stringify(input)}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) } } } diff --git a/src/varint.js b/src/varint.js index 15340f9a..afaea121 100644 --- a/src/varint.js +++ b/src/varint.js @@ -6,6 +6,7 @@ import varint from '../vendor/varint.js' */ export const decode = (data) => { const code = varint.decode(data) + // @ts-ignore return [code, varint.decode.bytes] } @@ -28,12 +29,7 @@ export const encode = (int) => { * @param {number} [offset=0] */ export const encodeTo = (int, target, offset = 0) => { - const cached = cache.get(int) - if (cached) { - target.set(target, offset) - } else { - varint.encode(int, target, offset) - } + varint.encode(int, target, offset) } /** diff --git a/test/test-cid.js b/test/test-cid.js index 419819c3..9a6eb3cd 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -28,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 => { @@ -36,6 +37,7 @@ const testThrowAny = async fn => { } catch (e) { return } + /* c8 ignore next */ throw new Error('Test failed to throw') } @@ -197,15 +199,20 @@ describe('CID', () => { describe('utilities', () => { const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' const h2 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1o' + const h3 = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' test('.equals v0 to v0', () => { - same(CID.parse(h1).equals(CID.parse(h1)), true) - same(CID.parse(h1).equals(CID.parse(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.parse(cidV1Str) + const cidV1 = CID.parse(h3) const cidV0 = cidV1.toV0() @@ -215,6 +222,13 @@ 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.parse(h1))) diff --git a/test/test-legacy.js b/test/test-legacy.js index 3ec2db72..73d5b29c 100644 --- a/test/test-legacy.js +++ b/test/test-legacy.js @@ -11,13 +11,14 @@ 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') } @@ -69,8 +70,11 @@ describe('multicodec', () => { same(cid.codec, 'raw') 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: { @@ -85,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 872d8717..3e1860af 100644 --- a/test/test-multibase.js +++ b/test/test-multibase.js @@ -18,6 +18,7 @@ const testThrow = (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } @@ -57,11 +58,13 @@ describe('multibase', () => { test('encode string failure', () => { const msg = 'Unknown type, must be binary type' testThrow(() => base32.encode('asdf'), msg) + testThrow(() => base32.encoder.encode('asdf'), msg) }) test('decode int failure', () => { const msg = 'Can only multibase decode strings' testThrow(() => base32.decode(1), msg) + testThrow(() => base32.decoder.decode(1), msg) }) const buff = bytes.fromString('test') @@ -72,6 +75,8 @@ describe('multibase', () => { 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)) }) } } @@ -88,4 +93,30 @@ describe('multibase', () => { describe('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) + }) + + 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 b42eeffc..052eda07 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -13,6 +13,7 @@ const testThrow = async (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } @@ -31,6 +32,22 @@ describe('multicodec', () => { same(json.decode(buff), { hello: 'world' }) }) + 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('raw cannot encode string', async () => { await testThrow(() => raw.encode('asdf'), 'Unknown type, must be binary type') }) diff --git a/test/test-multihash.js b/test/test-multihash.js index 8158023b..c1ad4e7e 100644 --- a/test/test-multihash.js +++ b/test/test-multihash.js @@ -32,6 +32,7 @@ const testThrowAsync = async (fn, message) => { if (e.message !== message) throw e return } + /* c8 ignore next */ throw new Error('Test failed to throw') } From 4baa102aa106268173dffb3e62be66bf6af0def4 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 23 Sep 2020 00:48:24 -0700 Subject: [PATCH 18/26] chore: remove dead code --- src/varint.js | 16 +--------------- test/test-cid.js | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/varint.js b/src/varint.js index afaea121..42d4976b 100644 --- a/src/varint.js +++ b/src/varint.js @@ -10,19 +10,6 @@ export const decode = (data) => { return [code, varint.decode.bytes] } -/** - * @param {number} int - * @returns {Uint8Array} - */ -export const encode = (int) => { - if (cache.has(int)) return cache.get(int) - const bytes = new Uint8Array(varint.encodingLength(int)) - varint.encode(int, bytes, 0) - cache.set(int, bytes) - - return bytes -} - /** * @param {number} int * @param {Uint8Array} target @@ -30,6 +17,7 @@ export const encode = (int) => { */ export const encodeTo = (int, target, offset = 0) => { varint.encode(int, target, offset) + return target } /** @@ -39,5 +27,3 @@ export const encodeTo = (int, target, offset = 0) => { export const encodingLength = (int) => { return varint.encodingLength(int) } - -const cache = new Map() diff --git a/test/test-cid.js b/test/test-cid.js index 9a6eb3cd..982b06f9 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -513,7 +513,7 @@ describe('CID', () => { }) test('invalid CID version', async () => { - const encoded = varint.encode(2) + const encoded = varint.encodeTo(2, new Uint8Array(32)) await testThrow(() => CID.decode(encoded), 'Invalid CID version 2') }) test('buffer', async () => { From 7d2fda316ab81dddf68add4445f5da62d9c50ab6 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 23 Sep 2020 00:49:20 -0700 Subject: [PATCH 19/26] chore: remove block.js --- src/basics-browser.js | 4 +- src/basics-import.js | 4 +- src/basics.js | 4 +- src/block.js | 308 ------------------------------------------ src/index.js | 3 +- 5 files changed, 7 insertions(+), 316 deletions(-) delete mode 100644 src/block.js diff --git a/src/basics-browser.js b/src/basics-browser.js index 20e9afec..f4fef934 100644 --- a/src/basics-browser.js +++ b/src/basics-browser.js @@ -1,8 +1,8 @@ // @ts-check import * as base64 from './bases/base64-browser.js' -import { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +import { CID, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' const bases = { ..._bases, ...base64 } -export { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics-import.js b/src/basics-import.js index 2421269f..f5f2a224 100644 --- a/src/basics-import.js +++ b/src/basics-import.js @@ -1,6 +1,6 @@ -import { CID, Block, hasher, digest, varint, bytes, hashes, codecs, bases as _bases } from './basics.js' +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, Block, hasher, digest, varint, bytes, hashes, codecs, bases } +export { CID, hasher, digest, varint, bytes, hashes, codecs, bases } diff --git a/src/basics.js b/src/basics.js index 37de1fe8..66974a19 100644 --- a/src/basics.js +++ b/src/basics.js @@ -7,10 +7,10 @@ import * as sha2 from './hashes/sha2.js' import raw from './codecs/raw.js' import json from './codecs/json.js' -import { CID, Block, hasher, digest, varint, bytes } from './index.js' +import { CID, hasher, digest, varint, bytes } from './index.js' const bases = { ...base32, ...base58 } const hashes = { ...sha2 } const codecs = { raw, json } -export { CID, Block, hasher, digest, varint, bytes, hashes, bases, codecs } +export { CID, hasher, digest, varint, bytes, hashes, bases, codecs } diff --git a/src/block.js b/src/block.js deleted file mode 100644 index 3bed5724..00000000 --- a/src/block.js +++ /dev/null @@ -1,308 +0,0 @@ -// @ts-check - -import CID from './cid.js' - -/** - * @template {number} Code - * @template T - * @class - */ -export default class Block { - /** - * @param {CID|null} cid - * @param {Code} code - * @param {T} data - * @param {Uint8Array} bytes - * @param {BlockConfig} config - */ - constructor (cid, code, data, bytes, { hasher }) { - /** @type {CID|Promise|null} */ - this._cid = cid - this.code = code - this.data = data - this.bytes = bytes - this.hasher = hasher - } - - async cid () { - const { _cid: cid } = this - if (cid != null) { - return await cid - } else { - const { bytes, code, hasher } = this - // First we store promise to avoid a race condition if cid is called - // whlie promise is pending. - const promise = createCID(hasher, bytes, code) - this._cid = promise - const cid = await promise - // Once promise resolves we store an actual CID. - this._cid = cid - return cid - } - } - - links () { - return links(this.data, []) - } - - tree () { - return tree(this.data, []) - } - - /** - * @param {string} path - */ - get (path) { - return get(this.data, path.split('/').filter(Boolean)) - } - - /** - * @template {number} Code - * @template T - * @param {Encoder} codec - * @param {BlockConfig} options - */ - static encoder (codec, options) { - return new BlockEncoder(codec, options) - } - - /** - * @template {number} Code - * @template T - * @param {Decoder} codec - * @param {BlockConfig} options - */ - static decoder (codec, options) { - return new BlockDecoder(codec, options) - } - - /** - * @template {number} Code - * @template T - * @param {Object} codec - * @param {Encoder} codec.encoder - * @param {Decoder} codec.decoder - * @param {BlockConfig} options - * @returns {BlockCodec} - */ - - static codec ({ encoder, decoder }, options) { - return new BlockCodec(encoder, decoder, options) - } -} - -/** - * @template T - * @param {T} source - * @param {Array} base - * @returns {Iterable<[string, CID]>} - */ -const links = function * (source, base) { - for (const [key, value] of Object.entries(source)) { - const path = [...base, key] - if (value != null && typeof value === 'object') { - if (Array.isArray(value)) { - for (const [index, element] of value.entries()) { - const elementPath = [...path, index] - const cid = CID.asCID(element) - if (cid) { - yield [elementPath.join('/'), cid] - } else if (typeof element === 'object') { - yield * links(element, elementPath) - } - } - } else { - const cid = CID.asCID(value) - if (cid) { - yield [path.join('/'), cid] - } else { - yield * links(value, path) - } - } - } - } -} - -/** - * @template T - * @param {T} source - * @param {Array} base - * @returns {Iterable} - */ -const tree = function * (source, base) { - for (const [key, value] of Object.entries(source)) { - const path = [...base, key] - yield path.join('/') - if (value != null && typeof value === 'object' && !CID.asCID(value)) { - if (Array.isArray(value)) { - for (const [index, element] of value.entries()) { - const elementPath = [...path, index] - yield elementPath.join('/') - if (typeof element === 'object' && !CID.asCID(element)) { - yield * tree(element, elementPath) - } - } - } else { - yield * tree(value, path) - } - } - } -} - -/** - * @template T - * @param {T} source - * @param {string[]} path - */ -const get = (source, path) => { - let node = source - for (const [index, key] of path.entries()) { - node = node[key] - if (node == null) { - throw new Error(`Object has no property at ${path.slice(0, index - 1).map(part => `[${JSON.stringify(part)}]`).join('')}`) - } - const cid = CID.asCID(node) - if (cid) { - return { value: cid, remaining: path.slice(index).join('/') } - } - } - return { value: node } -} - -/** - * - * @param {Hasher} hasher - * @param {Uint8Array} bytes - * @param {number} code - */ - -const createCID = async (hasher, bytes, code) => { - const multihash = await hasher.digest(bytes) - return CID.createV1(code, multihash) -} - -/** - * @template {number} Code - * @template T - */ -class BlockCodec { - /** - * @param {Encoder} encoder - * @param {Decoder} decoder - * @param {BlockConfig} config - */ - - constructor (encoder, decoder, config) { - this.encoder = new BlockEncoder(encoder, config) - this.decoder = new BlockDecoder(decoder, config) - this.config = config - } - - /** - * @param {Uint8Array} bytes - * @param {Partial} [options] - * @returns {Block} - */ - decode (bytes, options) { - return this.decoder.decode(bytes, { ...this.config, ...options }) - } - - /** - * @param {T} data - * @param {Partial} [options] - * @returns {Block} - */ - encode (data, options) { - return this.encoder.encode(data, { ...this.config, ...options }) - } -} - -/** - * @class - * @template {number} Code - * @template T - */ -class BlockEncoder { - /** - * @param {Encoder} codec - * @param {BlockConfig} config - */ - constructor (codec, config) { - this.codec = codec - this.config = config - } - - /** - * @param {T} data - * @param {Partial} [options] - * @returns {Block} - */ - encode (data, options) { - const { codec } = this - const bytes = codec.encode(data) - return new Block(null, codec.code, data, bytes, { ...this.config, ...options }) - } -} - -/** - * @class - * @template {number} Code - * @template T - */ -class BlockDecoder { - /** - * @param {Decoder} codec - * @param {BlockConfig} config - */ - constructor (codec, config) { - this.codec = codec - this.config = config - } - - /** - * @param {Uint8Array} bytes - * @param {Partial} [options] - * @returns {Block} - */ - decode (bytes, options) { - const data = this.codec.decode(bytes) - return new Block(null, this.codec.code, data, bytes, { ...this.config, ...options }) - } -} -/** - * @typedef {import('./block/interface').Config} BlockConfig - * @typedef {import('./hashes/interface').MultihashHasher} Hasher - **/ - -/** - * @template T - * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder - */ - -/** - * @template T - * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder - */ - -/** - * @template T - * @typedef {import('./bases/interface').MultibaseCodec} MultibaseCodec - */ - -/** - * @template {number} Code - * @template T - * @typedef {import('./codecs/interface').BlockEncoder} Encoder - */ - -/** - * @template {number} Code - * @template T - * @typedef {import('./codecs/interface').BlockDecoder} Decoder - */ - -/** - * @template {number} Code - * @template T - * @typedef {import('./codecs/interface').BlockCodec} Codec - */ diff --git a/src/index.js b/src/index.js index 3ce64497..d3d3144e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,10 @@ // @ts-check import CID from './cid.js' -import Block from './block.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' -export { CID, Block, hasher, digest, varint, bytes, codec } +export { CID, hasher, digest, varint, bytes, codec } From d9b92be5ef183a29dc95d676c17c56ad605bae5b Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Wed, 23 Sep 2020 22:12:30 +0000 Subject: [PATCH 20/26] fix: drop old npm ignore --- .npmignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .npmignore diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 40cae745..00000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.github -dist/test From a5d58acc91ce3b77b818ca4199dfe36edf9b2143 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Wed, 23 Sep 2020 22:29:07 +0000 Subject: [PATCH 21/26] fix: moving excludes to package.json --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ac77ec7c..bac80081 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,15 @@ "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", - "test:node": "hundreds --exclude vendor mocha test/test-*.js -x vendor/**", + "test:node": "hundreds mocha test/test-*.js", "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 --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/)", "license": "(Apache-2.0 AND MIT)", From cd3ffc60dc56c0bf9aca56d2169efd46b75598f2 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Wed, 23 Sep 2020 22:30:19 +0000 Subject: [PATCH 22/26] fix: browser same handles uint8arrays terribly --- test/test-cid.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/test-cid.js b/test/test-cid.js index 982b06f9..82c9521d 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -429,13 +429,24 @@ describe('CID', () => { 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()) - same(cid, parsed) + digestsame(cid, parsed) }) test('parse base58btc encoded CIDv1', async () => { @@ -443,7 +454,7 @@ describe('CID', () => { const cid = CID.create(1, 112, hash) const parsed = CID.parse(cid.toString(base58btc)) - same(cid, parsed) + digestsame(cid, parsed) }) test('parse base58btc encoded CIDv0', async () => { @@ -451,7 +462,7 @@ describe('CID', () => { const cid = CID.create(0, 112, hash) const parsed = CID.parse(cid.toString()) - same(cid, parsed) + digestsame(cid, parsed) }) test('fails to parse base64 encoded CIDv1', async () => { @@ -467,7 +478,7 @@ describe('CID', () => { const cid = CID.create(1, 112, hash) const parsed = CID.parse(cid.toString(base64), base64) - same(cid, parsed) + digestsame(cid, parsed) }) }) From c012a2f8dcd47846697d09d72014791a253c35dc Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 23 Sep 2020 17:45:26 -0700 Subject: [PATCH 23/26] chore: update readme --- README.md | 145 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f3354ee4..244febe5 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,132 @@ # 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. +## Intefaces ```js -import * as CID from 'multiformats/cid' +import CID from 'multiformats/cid' +import json from 'multiformats/codecs/json' import { sha256 } from 'multiformats/hashes/sha2' -import dagcbor from '@ipld/dag-cbor' -import { base32 } from 'multiformats/bases/base32' -import { base58btc } from 'multiformats/bases/base58' -const bytes = dagcbor.encode({ hello: 'world' }) +const bytes = json.encode({ hello: 'world' }) const hash = await sha256.digest(bytes) -// raw codec is the only codec that is there by default -const cid = CID.create(1, dagcbor.code, hash) +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 { block } from 'multiformats/basics' -import { sha256 } from 'multiformats/hashes/sha2' -import dagcbor from '@ipld/dag-cbor' +import { base64 } from "multiformats/bases/base64" +cid.toString(base64.encoder) +//> 'mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA' +``` + +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: + +```js +CID.parse('mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA', base64.decoder) +//> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) +``` + +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: + + +```js +cid.toString(base64) +CID.parse(cid.toString(base64), base64) +``` + +**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: + +```js +const v1 = CID.parse('bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea') +v1.toString() +//> 'bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea' + +const v0 = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') +v0.toString() +//> 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' +v0.toV1().toString() +//> 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku' +``` + +### Multicodec Encoders / Decoders / Codecs -const encoder = block.encoder(dagcbor, { hasher: sha256 }) -const hello = encoder.encode({ hello: 'world' }) -const cid = await hello.cid() +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: + +```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)) +}) ``` -# Plugins +Just like with multibase, here codecs are duals of `encoder` and `decoder` parts, +but they also implement both interfaces for convenience: + +```js +const hello = json.encoder.encode({ hello: 'world' }) +json.decode(b1) +//> { hello: 'world' } +``` + +### Multihash Hashers + +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: + +```js +import * as hasher from 'multiformats/hashes/hasher') -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. +const sha256 = hasher.from({ + // As per multiformats table + // https://github.com/multiformats/multicodec/blob/master/table.csv#L9 + name: 'sha2-256', + code: 0x12, -| bundle | bases | hashes | codecs | -|---|---|---|---| -| `multiformats/basics` | `base32`, `base64` | `sha2-256`, `sha2-512` | `json`, `raw` | + encode: (input) => new Uint8Array(crypto.createHash('sha256').update(input).digest()) +}) -## Base Encodings (multibase) +const hash = await sha256.digest(json.encode({ hello: 'world' })) +CID.create(1, json.code, hash) + +//> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) +``` + + + +# Implementations + +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 codecs | bases | import | repo | --- | --- | --- | @@ -58,7 +135,7 @@ However, you can import the following bundles to get a `multiformats` interface `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) | -## Hash Functions (multihash) +## Multihash hashers | hashes | import | repo | | --- | --- | --- | @@ -75,5 +152,3 @@ However, you can import the following bundles to get a `multiformats` interface | `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) | -# API - From 26549589b4f0821d84fbf740858b477d418bad30 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Fri, 25 Sep 2020 15:28:19 -0700 Subject: [PATCH 24/26] doc: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 244febe5..8b35c05b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ encoders / decoders / codecs, codec encoders /decoders and multihash hashers tha Library provides implementations for most basics and many others can be found in linked repositories. -## Intefaces +## Interfaces ```js import CID from 'multiformats/cid' From a826851ea343228083c3b47bd1b641fed4c4f934 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Fri, 25 Sep 2020 15:29:37 -0700 Subject: [PATCH 25/26] fix: drop basics exports BREAKING CHANGE! no more basics export --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index bac80081..0802be82 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,6 @@ ".": { "import": "./src/index.js" }, - "./basics": { - "import": "./src/basics-import.js", - "browser": "./src/basics-browser.js" - }, "./cid": { "import": "./src/cid.js" }, From 6f633a31489f89be435400bc6886af9490421f66 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Fri, 25 Sep 2020 22:34:43 +0000 Subject: [PATCH 26/26] fix: tests off of basics interface --- package.json | 5 ++++- test/test-multicodec.js | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0802be82..2c959394 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,10 @@ "coverage": "c8 --exclude vendor --reporter=html mocha test/test-*.js && npm_config_yes=true npx st -d coverage -p 8080" }, "c8": { - "exclude": [ "test/**", "vendor/**" ] + "exclude": [ + "test/**", + "vendor/**" + ] }, "keywords": [], "author": "Mikeal Rogers (https://www.mikealrogers.com/)", diff --git a/test/test-multicodec.js b/test/test-multicodec.js index 052eda07..3ab9b44a 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -1,7 +1,8 @@ /* globals describe, it */ import * as bytes from '../src/bytes.js' import assert from 'assert' -import * as 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 @@ -18,8 +19,6 @@ const testThrow = async (fn, message) => { } describe('multicodec', () => { - const { codecs: { raw, json } } = multiformats - test('encode/decode raw', () => { const buff = raw.encode(bytes.fromString('test')) same(buff, bytes.fromString('test'))