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..392076f4 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "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": { "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/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..f668d4b5 --- /dev/null +++ b/src/block.js @@ -0,0 +1,329 @@ +// @ts-check + +import { createV1, asCID } 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 + } + } + + 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 } +} + +/** + * + * @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..cb1d6f9e 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.decode(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.decode(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,131 @@ 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 = 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..da5208e6 --- /dev/null +++ b/src/hashes/digest.js @@ -0,0 +1,82 @@ +// @ts-check + +import { coerce, equals as equalBytes } from '../bytes.js' +import * as varint from '../varint.js' + +/** + * Creates a multihash digest. + * @template {number} Code + * @param {Code} code + * @param {Uint8Array} digest + */ +export const create = (code, digest) => { + const size = digest.byteLength + const sizeOffset = varint.encodingLength(code) + const digestOffset = sizeOffset + varint.encodingLength(size) + + const bytes = new Uint8Array(digestOffset + size) + varint.encodeTo(code, bytes, 0) + varint.encodeTo(size, bytes, sizeOffset) + bytes.set(digest, digestOffset) + + return new Digest(code, size, digest, bytes) +} + +/** + * Turns bytes representation of multihash digest into an instance. + * @param {Uint8Array} multihash + * @returns {Digest} + */ +export const decode = (multihash) => { + const bytes = coerce(multihash) + const [code, sizeOffset] = varint.decode(bytes) + const [size, digestOffset] = varint.decode(bytes.subarray(sizeOffset)) + const digest = bytes.subarray(sizeOffset + digestOffset) + + if (digest.byteLength !== size) { + throw new Error('Given multihash has incorrect length') + } + + return new Digest(code, size, digest, bytes) +} + +/** + * @param {MultihashDigest} a + * @param {MultihashDigest} b + * @returns {boolean} + */ +export const equals = (a, b) => { + if (a === b) { + return true + } else { + return a.code === b.code && a.size === b.size && equalBytes(a.bytes, b.bytes) + } +} + +/** + * @typedef {import('./interface').MultihashDigest} MultihashDigest + */ + +/** + * Represents a multihash digest which carries information about the + * hashing alogrithm and an actual hash digest. + * @template {number} Code + * @template {number} Size + * @class + * @implements {MultihashDigest} + */ +export class Digest { + /** + * Creates a multihash digest. + * @param {Code} code + * @param {Size} size + * @param {Uint8Array} digest + * @param {Uint8Array} bytes + */ + constructor (code, size, digest, bytes) { + this.code = code + this.size = size + this.digest = digest + this.bytes = bytes + } +} diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js new file mode 100644 index 00000000..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..15340f9a --- /dev/null +++ b/src/varint.js @@ -0,0 +1,47 @@ +import varint from '../vendor/varint.js' + +/** + * @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, offset) + } +} + +/** + * @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) }) }) 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;