From 062a2841b4572f2190d7b2273453ec5759698f60 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Thu, 1 Oct 2020 04:51:17 +0000 Subject: [PATCH 1/7] wip: migrate to latest multiformats --- basics.js | 6 - defaults.js | 7 +- index.js | 321 +++++++++++++++++++++++++------------------- package.json | 7 +- reader.js | 3 +- test/test-block.js | 43 +++--- test/test-errors.js | 10 +- test/test-reader.js | 4 +- 8 files changed, 217 insertions(+), 184 deletions(-) delete mode 100644 basics.js diff --git a/basics.js b/basics.js deleted file mode 100644 index 19ffb25..0000000 --- a/basics.js +++ /dev/null @@ -1,6 +0,0 @@ -import multiformats from 'multiformats/basics' -import create from './index.js' - -const Block = create(multiformats) - -export default Block diff --git a/defaults.js b/defaults.js index 1e98ff0..97e8c07 100644 --- a/defaults.js +++ b/defaults.js @@ -1,5 +1,6 @@ -import Block from './basics.js' -import dagcbor from '@ipld/dag-cbor' -Block.multiformats.multicodec.add(dagcbor) +import Block from './index.js' +import { codec as multicodec } from 'multiformats' +import * as dagcbor from '@ipld/dag-cbor' +Block.add(multicodec.codec(dagcbor)) export default Block diff --git a/index.js b/index.js index adfb903..737cc89 100644 --- a/index.js +++ b/index.js @@ -1,174 +1,221 @@ -import withIs from 'class-is' import transform from 'lodash.transform' import createReader from './reader.js' +import json from 'multiformats/codecs/json' +import raw from 'multiformats/codecs/raw' +import { sha256 } from 'multiformats/hashes/sha2' +import { bytes, CID } from 'multiformats' const readonly = value => ({ get: () => value, set: () => { throw new Error('Cannot set read-only property') } }) const immutableTypes = new Set(['number', 'string', 'boolean']) -const create = multiformats => { - const { bytes, CID, multihash, multicodec } = multiformats - const { coerce, isBinary } = bytes - const copyBinary = value => { - const b = coerce(value) - return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)) - } - const reader = createReader(multiformats) - - const clone = obj => transform(obj, (result, value, key) => { - const cid = CID.asCID(value) - if (cid) { - result[key] = cid - } else if (isBinary(value)) { - result[key] = copyBinary(value) - } else if (typeof value === 'object' && value !== null) { - result[key] = clone(value) +const { coerce, isBinary } = bytes +const copyBinary = value => { + const b = coerce(value) + return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)) +} +const reader = createReader(CID) + +const clone = obj => transform(obj, (result, value, key) => { + if (value && value.asCID === value) { + result[key] = value + } else if (isBinary(value)) { + result[key] = copyBinary(value) + } else if (typeof value === 'object' && value !== null) { + result[key] = clone(value) + } else { + result[key] = value + } +}) + +class Block { + constructor (opts) { + if (!opts) throw new Error('Block options are required') + if (opts.codec) { + if (typeof opts.codec !== 'object') { + const codec = Block.codecs.get(opts.codec) + if (!opts.codec) throw new Error(`Cannot find codec ${JSON.stringify(opts.codec)}`) + opts.codec = codec + } } else { - result[key] = value + if (!opts.cid) throw new Error('Cannot create block instance without cid or codec') + opts.codec = Block.codecs.get(opts.cid.code) } - }) - - class Block { - constructor (opts) { - if (!opts) throw new Error('Block options are required') - if (opts.cid) opts.cid = CID.asCID(opts.cid) - if (typeof opts.codec === 'number') { - opts.code = opts.codec - opts.codec = multicodec.get(opts.code).name - } - if (typeof opts.source === 'undefined' && - typeof opts.data === 'undefined') { - throw new Error('Block instances must be created with either an encode source or data') - } - if (typeof opts.source !== 'undefined' && !opts.codec && !opts.code) { - throw new Error('Block instances created from source objects must include desired codec') - } - if (opts.data && !opts.cid && !opts.codec && !opts.code) { - throw new Error('Block instances created from data must include cid or codec') - } - if (!opts.cid && !opts.algo) opts.algo = 'sha2-256' - // Do our best to avoid accidental mutations of the options object after instantiation - // Note: we can't actually freeze the object because we mutate it once per property later - opts = Object.assign({}, opts) - Object.defineProperty(this, 'opts', readonly(opts)) + if (typeof opts.source === 'undefined' && + typeof opts.data === 'undefined') { + throw new Error('Block instances must be created with either an encode source or data') } - - source () { - if (this.opts.cid || this.opts.data || - this._encoded || this._decoded) return null - if (!this.opts.source) return null - return this.opts.source + if (typeof opts.source !== 'undefined' && !opts.codec && !opts.code) { + throw new Error('Block instances created from source objects must include desired codec') } - - async cid () { - if (this.opts.cid) return this.opts.cid - const hash = await multihash.hash(this.encodeUnsafe(), this.opts.algo) - const cid = CID.create(1, this.code, hash) - this.opts.cid = cid - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return cid + if (opts.data && !opts.cid && !opts.codec && !opts.code) { + throw new Error('Block instances created from data must include cid or codec') } + opts.hasher = opts.hasher || sha256 + // Do our best to avoid accidental mutations of the options object after instantiation + // Note: we can't actually freeze the object because we mutate it once per property later + opts = Object.assign({}, opts) + Object.defineProperty(this, 'opts', readonly(opts)) + Object.defineProperty(this, 'asBlock', readonly(this)) + } - get codec () { - if (this.opts.code) { - this.opts.codec = multicodec.get(this.opts.code).name + get hasher () { + if (this.opts.cid) { + if (!this.opts.hasher) { + this.opts.hasher = Block.codecs.get(this.opts.cid.multihash.code) + } else if (this.opts.hasher.code !== this.opts.cid.multihash.code) { + this.opts.hasher = Block.codecs.get(this.opts.cid.multihash.code) + } else { + return this.opts.hasher || sha256 } - if (this.opts.cid) { - this.opts.codec = multicodec.get(this.opts.cid.code).name - } - return this.opts.codec + if (!this.opts.hasher) throw new Error('Do not have hash implementation') } + return this.opts.hasher || sha256 + } + + source () { + if (this.opts.cid || this.opts.data || + this._encoded || this._decoded) return null + if (!this.opts.source) return null + return this.opts.source + } + + async cid () { + if (this.opts.cid) return this.opts.cid + const hash = await this.hasher.digest(this.encodeUnsafe()) + const cid = CID.create(1, this.opts.codec.code, hash) + this.opts.cid = cid + // https://github.com/bcoe/c8/issues/135 + /* c8 ignore next */ + return cid + } - get code () { - if (this.opts.cid) return this.opts.cid.code - if (!this.opts.code) { - this.opts.code = multicodec.get(this.codec).code + get codec () { + if (this.opts.cid) { + if (!this.opts.codec || this.opts.codec.code !== this.opts.cid.code) { + this.opts.codec = Block.codecs.get(this.opts.cid.code) } - return this.opts.code + } else if (this.opts.code) { + this.opts.codec = Block.codecs.get(this.opts.code) } + return this.opts.codec.name + } - async validate () { - // if we haven't created a CID yet we know it will be valid :) - if (!this.opts.cid) return true - const cid = await this.cid() - const data = this.encodeUnsafe() - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return multihash.validate(cid.multihash, data) + get code () { + if (this.opts.cid) return this.opts.cid.code + if (!this.opts.code) { + this.opts.code = this.opts.codec.code } + return this.opts.code + } - _encode () { - this._encoded = this.opts.data || multicodec.get(this.code).encode(this.opts.source) - } + async validate () { + // if we haven't created a CID yet we know it will be valid :) + if (!this.opts.cid) return true + if (!this.opts.hasher) throw new Error('Must have hasher in order to perform comparison') + const cid = await this.cid() + const data = this.encodeUnsafe() + const hash = await this.hasher.digest(data) + if (bytes.equals(cid.multihash.bytes, hash.bytes)) return true + throw new Error('Bytes do not match') + } - encode () { - if (!this._encoded) this._encode() - return copyBinary(this._encoded) + _encode () { + if (!this.opts.data && !this.opts.codec) { + throw new Error('Do not have codec implemention in this Block interface') } + this._encoded = this.opts.data || this.opts.codec.encode(this.opts.source) + } - encodeUnsafe () { - if (!this._encoded) this._encode() - return this._encoded - } + encode () { + if (!this._encoded) this._encode() + return copyBinary(this._encoded) + } - _decode () { - if (typeof this.opts.source !== 'undefined') this._decoded = this.opts.source - else { - const { decode } = multicodec.get(this.code) - this._decoded = decode(this._encoded || this.opts.data) - } - return this._decoded - } + encodeUnsafe () { + if (!this._encoded) this._encode() + return this._encoded + } - decode () { - // TODO: once we upgrade to the latest data model version of - // dag-pb that @gozala wrote we should be able to remove this - // and treat it like every other codec. - /* c8 ignore next */ - if (this.codec === 'dag-pb') return this._decode() - if (!this._decoded) this._decode() - const tt = typeof this._decoded - if (tt === 'number' || tt === 'boolean') { - // return any immutable types - return this._decoded - } - if (isBinary(this._decoded)) return copyBinary(this._decoded) - if (immutableTypes.has(typeof this._decoded) || this._decoded === null) { - return this._decoded - } - return clone(this._decoded) + _decode () { + if (typeof this.opts.source !== 'undefined') this._decoded = this.opts.source + else { + this._decoded = this.opts.codec.decode(this._encoded || this.opts.data) } + return this._decoded + } - decodeUnsafe () { - if (!this._decoded) this._decode() + decode () { + // TODO: once we upgrade to the latest data model version of + // dag-pb that @gozala wrote we should be able to remove this + // and treat it like every other codec. + /* c8 ignore next */ + if (this.codec === 'dag-pb') return this._decode() + if (!this._decoded) this._decode() + const tt = typeof this._decoded + if (tt === 'number' || tt === 'boolean') { + // return any immutable types return this._decoded } - - reader () { - return reader(this.decodeUnsafe()) + if (isBinary(this._decoded)) return copyBinary(this._decoded) + if (immutableTypes.has(typeof this._decoded) || this._decoded === null) { + return this._decoded } + return clone(this._decoded) + } - async equals (block) { - if (block === this) return true - const cid = await this.cid() - if (CID.asCID(block)) return cid.equals(CID.asCID(block)) - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return cid.equals(await block.cid()) - } + decodeUnsafe () { + if (!this._decoded) this._decode() + return this._decoded } - const BlockWithIs = withIs(Block, { className: 'Block', symbolName: '@ipld/block' }) - BlockWithIs.encoder = (source, codec, algo) => new BlockWithIs({ source, codec, algo }) - BlockWithIs.decoder = (data, codec, algo) => new BlockWithIs({ data, codec, algo }) - BlockWithIs.create = (data, cid) => { - if (typeof cid === 'string') cid = CID.from(cid) - return new BlockWithIs({ data, cid }) + reader () { + return reader(this.decodeUnsafe()) + } + + async equals (block) { + if (block === this) return true + const cid = await this.cid() + if (block.asCID === block) return cid.equals(block) + // https://github.com/bcoe/c8/issues/135 + /* c8 ignore next */ + return cid.equals(await block.cid()) } - BlockWithIs.multiformats = multiformats - BlockWithIs.CID = CID - return BlockWithIs } -export default create +Block.codecs = new Map() + +Block.add = codec => { + if (codec.name) Block.codecs.set(codec.name, codec) + if (codec.code) Block.codecs.set(codec.code, codec) +} +Block.add(json) +Block.add(raw) + +Block.encoder = (source, codec, hasher) => new Block({ source, codec, hasher }) +Block.decoder = (data, codec, hasher) => new Block({ data, codec, hasher }) +Block.create = (data, cid) => { + if (typeof cid === 'string') cid = CID.parse(cid) + const codec = Block.codecs.get(cid.code) + return new Block({ data, cid, codec }) +} +Block.defaults = opts => ({ + CID, + defaults: opts, + encoder: (source, codec, hasher) => new Block({ + source, + codec: codec || opts.codec, + hasher: hasher || opts.hasher + }), + decoder: (data, codec, hasher) => new Block({ + data, + codec: codec || opts.codec, + hasher: hasher || opts.hasher + }), + create: Block.create, + codecs: Block.codecs, + add: Block.add +}) +Block.CID = CID + +export default Block diff --git a/package.json b/package.json index 81703fe..eb90efc 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "standard": "^14.3.4" }, "dependencies": { - "@ipld/dag-cbor": "1.1.11", + "@ipld/dag-cbor": "2.0.2", "class-is": "^1.1.0", "lodash.transform": "^4.6.0", - "multiformats": "^3.0.3" + "multiformats": "^4.0.0" }, "repository": { "type": "git", @@ -43,9 +43,6 @@ ".": { "import": "./index.js" }, - "./basics": { - "import": "./basics.js" - }, "./defaults": { "import": "./defaults.js" } diff --git a/reader.js b/reader.js index 6b4d85c..4ce1de4 100644 --- a/reader.js +++ b/reader.js @@ -1,5 +1,4 @@ -export default multiformats => { - const { CID } = multiformats +export default CID => { /* eslint-disable max-depth */ const links = function * (decoded, path = []) { if (typeof decoded !== 'object' || !decoded) return diff --git a/test/test-block.js b/test/test-block.js index ee8ed7b..c78ca81 100644 --- a/test/test-block.js +++ b/test/test-block.js @@ -1,27 +1,24 @@ 'use strict' /* globals it, describe */ -import multiformats from 'multiformats/basics' -import create from '@ipld/block' -import dagjson from '@ipld/dag-json' -import dagcbor from '@ipld/dag-cbor' -import base58 from 'multiformats/bases/base58' +import Block from '@ipld/block' +// import dagjson from '@ipld/dag-json' +import * as dagcbor from '@ipld/dag-cbor' +import { base58btc } from 'multiformats/bases/base58' +import { sha512 } from 'multiformats/hashes/sha2' +import { bytes, CID, codec as multicodec } from 'multiformats' import assert from 'assert' -const { bytes, multicodec, multibase, CID } = multiformats const { fromString, toString } = bytes const isBinary = o => { if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return true return false } -const Block = create(multiformats) const same = assert.deepStrictEqual const test = it -multibase.add(base58) - -for (const codec of [dagjson, dagcbor]) { - multiformats.add(codec) - const { name, code } = codec(multiformats) +for (const codec of [dagcbor]) { + Block.add(multicodec.codec(codec)) + const { name, code, encode } = codec describe(name, () => { for (const id of [name, code]) { describe(`w/ ${typeof id === 'string' ? 'name' : 'code'}`, () => { @@ -29,7 +26,7 @@ for (const codec of [dagjson, dagcbor]) { const block = Block.encoder({ hello: 'world' }, id) const encoded = block.encodeUnsafe() assert.ok(isBinary(encoded)) - const comp = multicodec.encode({ hello: 'world' }, id) + const comp = encode({ hello: 'world' }) same(encoded, comp) done() }) @@ -67,7 +64,7 @@ for (const codec of [dagjson, dagcbor]) { }) test('Block decode', async () => { - const data = multicodec.encode({ hello: 'world' }, id) + const data = encode({ hello: 'world' }) let block = Block.decoder(data, id) let decoded = block.decode() same(decoded, { hello: 'world' }) @@ -114,7 +111,7 @@ for (const codec of [dagjson, dagcbor]) { }) test('decode deep object', done => { - const cid = CID.from('zdpuAtX7ZibcWdSKQwiDCkPjWwRvtcKCPku9H7LhgA4qJW4Wk') + const cid = CID.parse('zdpuAtX7ZibcWdSKQwiDCkPjWwRvtcKCPku9H7LhgA4qJW4Wk') const o = { a: { b: [cid], c: fromString('x') } } const block = Block.encoder(o, id) const decoded = block.decode() @@ -188,8 +185,7 @@ describe('raw', () => { describe('cid()', () => { test('get code from cid', async () => { let block = Block.encoder({ hello: 'world' }, 'dag-cbor') - const c = block.code - same(c, 113) + same(block.code, 113) block = Block.create(block.encode(), await block.cid()) same(block.code, 113) }) @@ -197,16 +193,17 @@ describe('cid()', () => { test('Block cid', async () => { let block = Block.encoder({ hello: 'world' }, 'dag-cbor') let cid = await block.cid() - same(cid.toString('base58btc'), 'zdpuAtX7ZibcWdSKQwiDCkPjWwRvtcKCPku9H7LhgA4qJW4Wk') - block = Block.encoder({ hello: 'world' }, 'dag-cbor', 'sha2-512') + same(cid.toString(base58btc), 'zdpuAtX7ZibcWdSKQwiDCkPjWwRvtcKCPku9H7LhgA4qJW4Wk') + block = Block.encoder({ hello: 'world' }, 'dag-cbor', sha512) cid = await block.cid() - same(cid.toString('base58btc'), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') + same(cid.toString(base58btc), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') block = Block.create(await block.encode(), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') - same((await block.cid()).toString('base58btc'), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') + same((await block.cid()).toString(base58btc), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') same(block.codec, 'dag-cbor') + Block.add(sha512) same(await block.validate(), true) block = Block.create(await block.encode(), cid) - same((await block.cid()).toString('base58btc'), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') + same((await block.cid()).toString(base58btc), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') same(block.codec, 'dag-cbor') block = Block.create(fromString('asdf'), 'zBwW8ZGUCK3yY7Xxmqzm1sCjzE2Z8msvEdRCX1s9RKS61i5V8owNmCwfazw6hfetkzLW4KejDt1i566b8yEYuWAQi2Yyr') let threw = true @@ -214,7 +211,7 @@ describe('cid()', () => { await block.validate() threw = false } catch (e) { - if (e.message !== 'Buffer does not match hash') throw e + if (e.message !== 'Bytes do not match') throw e } same(threw, true) }) diff --git a/test/test-errors.js b/test/test-errors.js index 1a744a4..1d86be2 100644 --- a/test/test-errors.js +++ b/test/test-errors.js @@ -1,10 +1,8 @@ 'use strict' /* globals it */ -import createBlock from '@ipld/block' -import multiformats from 'multiformats/basics' +import Block from '@ipld/block' import assert from 'assert' -const Block = createBlock(multiformats) const same = assert.deepStrictEqual const test = it @@ -21,15 +19,15 @@ test('No block options', async () => { }) test('No data or source', async () => { - await tryError(() => new Block({}), 'Block instances must be created with either an encode source or data') + await tryError(() => new Block({}), 'Cannot create block instance without cid or codec') }) test('source only', async () => { - await tryError(() => new Block({ source: {} }), 'Block instances created from source objects must include desired codec') + await tryError(() => new Block({ source: {} }), 'Cannot create block instance without cid or codec') }) test('data only', async () => { - await tryError(() => new Block({ data: Buffer.from('asdf') }), 'Block instances created from data must include cid or codec') + await tryError(() => new Block({ data: Buffer.from('asdf') }), 'Cannot create block instance without cid or codec') }) test('set opts', async () => { diff --git a/test/test-reader.js b/test/test-reader.js index 60d867e..2229fd4 100644 --- a/test/test-reader.js +++ b/test/test-reader.js @@ -1,12 +1,12 @@ /* globals it */ -import Block from '@ipld/block/basics' +import Block from '@ipld/block/defaults' import assert from 'assert' const { CID } = Block const same = assert.deepStrictEqual const test = it -const link = CID.from('bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae') +const link = CID.parse('bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae') const fixture = { n: null, From fed6eb66b3c085f263606180b0d6cf0d0244860e Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Fri, 2 Oct 2020 04:24:29 +0000 Subject: [PATCH 2/7] wip: refactor constructor --- index.js | 215 ++++++++++++++++++++---------------------------------- reader.js | 141 +++++++++++++++++------------------ 2 files changed, 149 insertions(+), 207 deletions(-) diff --git a/index.js b/index.js index 737cc89..2beb49b 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import transform from 'lodash.transform' -import createReader from './reader.js' +import reader from './reader.js' import json from 'multiformats/codecs/json' import raw from 'multiformats/codecs/raw' import { sha256 } from 'multiformats/hashes/sha2' @@ -14,7 +14,6 @@ const copyBinary = value => { const b = coerce(value) return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)) } -const reader = createReader(CID) const clone = obj => transform(obj, (result, value, key) => { if (value && value.asCID === value) { @@ -28,145 +27,69 @@ const clone = obj => transform(obj, (result, value, key) => { } }) -class Block { - constructor (opts) { - if (!opts) throw new Error('Block options are required') - if (opts.codec) { - if (typeof opts.codec !== 'object') { - const codec = Block.codecs.get(opts.codec) - if (!opts.codec) throw new Error(`Cannot find codec ${JSON.stringify(opts.codec)}`) - opts.codec = codec - } - } else { - if (!opts.cid) throw new Error('Cannot create block instance without cid or codec') - opts.codec = Block.codecs.get(opts.cid.code) - } - if (typeof opts.source === 'undefined' && - typeof opts.data === 'undefined') { - throw new Error('Block instances must be created with either an encode source or data') - } - if (typeof opts.source !== 'undefined' && !opts.codec && !opts.code) { - throw new Error('Block instances created from source objects must include desired codec') - } - if (opts.data && !opts.cid && !opts.codec && !opts.code) { - throw new Error('Block instances created from data must include cid or codec') - } - opts.hasher = opts.hasher || sha256 - // Do our best to avoid accidental mutations of the options object after instantiation - // Note: we can't actually freeze the object because we mutate it once per property later - opts = Object.assign({}, opts) - Object.defineProperty(this, 'opts', readonly(opts)) - Object.defineProperty(this, 'asBlock', readonly(this)) - } - - get hasher () { - if (this.opts.cid) { - if (!this.opts.hasher) { - this.opts.hasher = Block.codecs.get(this.opts.cid.multihash.code) - } else if (this.opts.hasher.code !== this.opts.cid.multihash.code) { - this.opts.hasher = Block.codecs.get(this.opts.cid.multihash.code) - } else { - return this.opts.hasher || sha256 - } - if (!this.opts.hasher) throw new Error('Do not have hash implementation') - } - return this.opts.hasher || sha256 - } +const setImmutable = (obj, key, value) => { + if (typeof value === 'undefined') throw new Error(`${key} cannot be undefined`) + Object.defineProperty(this, key, readonly(value)) +} - source () { - if (this.opts.cid || this.opts.data || - this._encoded || this._decoded) return null - if (!this.opts.source) return null - return this.opts.source +class Block { + constructor ({ codec, hasher, source, cid, data }) { + setImmutable(this, '_codec', codec) + setImmutable(this, '_hasher', hasher) + if (source) setImmutable(this, '_source', source) + if (cid) setImmutable(this, '_cid', cid) + if (data) setImmutable(this, '_data', data) + if (!source && (!data || !cid)) throw new Error('Missing required argument') + if (source && (!codec || !hasher)) throw new Error('Missing required argument') + setImmutable(this, 'asBlock', this) } async cid () { - if (this.opts.cid) return this.opts.cid - const hash = await this.hasher.digest(this.encodeUnsafe()) + if (this._cid) return this._cid + const hash = await this._hasher.digest(this.encodeUnsafe()) const cid = CID.create(1, this.opts.codec.code, hash) - this.opts.cid = cid - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ + setImmutable(this, '_cid', cid) return cid } - get codec () { - if (this.opts.cid) { - if (!this.opts.codec || this.opts.codec.code !== this.opts.cid.code) { - this.opts.codec = Block.codecs.get(this.opts.cid.code) - } - } else if (this.opts.code) { - this.opts.codec = Block.codecs.get(this.opts.code) - } - return this.opts.codec.name - } - get code () { - if (this.opts.cid) return this.opts.cid.code - if (!this.opts.code) { - this.opts.code = this.opts.codec.code - } - return this.opts.code + if (this._cid) return this._cid.code + return this._codec.code } - async validate () { - // if we haven't created a CID yet we know it will be valid :) - if (!this.opts.cid) return true - if (!this.opts.hasher) throw new Error('Must have hasher in order to perform comparison') - const cid = await this.cid() + encode () { const data = this.encodeUnsafe() - const hash = await this.hasher.digest(data) - if (bytes.equals(cid.multihash.bytes, hash.bytes)) return true - throw new Error('Bytes do not match') + return copyBinary(data) } - _encode () { - if (!this.opts.data && !this.opts.codec) { + encodeUnsafe () { + if (this._data) return this._data + if (!this._codec) { throw new Error('Do not have codec implemention in this Block interface') } - this._encoded = this.opts.data || this.opts.codec.encode(this.opts.source) + const data = this._codec.encode(this._source) + setImmutable(this, '_data', data) + return data } - encode () { - if (!this._encoded) this._encode() - return copyBinary(this._encoded) - } - - encodeUnsafe () { - if (!this._encoded) this._encode() - return this._encoded - } - - _decode () { - if (typeof this.opts.source !== 'undefined') this._decoded = this.opts.source - else { - this._decoded = this.opts.codec.decode(this._encoded || this.opts.data) + decodeUnsafe () { + if (typeof this.opts._source !== 'undefined') return this._source + if (!this._codec) { + throw new Error('Do not have codec implemention in this Block interface') } - return this._decoded + const source = this._codec.decode(this._data) + setImmutable(this, '_source', source) + return source } decode () { - // TODO: once we upgrade to the latest data model version of - // dag-pb that @gozala wrote we should be able to remove this - // and treat it like every other codec. - /* c8 ignore next */ - if (this.codec === 'dag-pb') return this._decode() - if (!this._decoded) this._decode() - const tt = typeof this._decoded - if (tt === 'number' || tt === 'boolean') { - // return any immutable types - return this._decoded - } - if (isBinary(this._decoded)) return copyBinary(this._decoded) - if (immutableTypes.has(typeof this._decoded) || this._decoded === null) { - return this._decoded + const decoded = this.decodeUnsafe() + if (decoded === null) return null + if (isBinary(decoded)) return copyBinary(decoded) + if (immutableTypes.has(typeof decoded) || decoded === null) { + return decoded } - return clone(this._decoded) - } - - decodeUnsafe () { - if (!this._decoded) this._decode() - return this._decoded + return clone(decoded) } reader () { @@ -175,11 +98,9 @@ class Block { async equals (block) { if (block === this) return true - const cid = await this.cid() - if (block.asCID === block) return cid.equals(block) - // https://github.com/bcoe/c8/issues/135 - /* c8 ignore next */ - return cid.equals(await block.cid()) + if (block.asBlock !== block) throw new Error('Not a block instance') + const [a, b] = await Promise.all([this.cid(), block.cid()]) + return a.equals(b) } } @@ -192,27 +113,47 @@ Block.add = codec => { Block.add(json) Block.add(raw) -Block.encoder = (source, codec, hasher) => new Block({ source, codec, hasher }) -Block.decoder = (data, codec, hasher) => new Block({ data, codec, hasher }) -Block.create = (data, cid) => { +Block.encoder = (source, codec, hasher = sha256) => { + if (typeof codec === 'string') codec = Block.codecs.get(codec) + if (!codec) throw new Error('Missing codec') + return new Block({ source, codec, hasher }) +} +Block.decoder = (data, codec, hasher = sha256) => { + if (typeof codec === 'string') codec = Block.codecs.get(codec) + if (!codec) throw new Error('Missing codec') + return new Block({ data, codec, hasher }) +} +Block.createUnsafe = (data, cid, { hasher, codec } = {}) => { + codec = codec || Block.codecs.get(cid.code) + if (!codec) throw new Error(`Missing codec ${cid.code}`) + return new Block({ data, cid, codec, hasher: hasher || null }) +} +Block.create = async (data, cid, { hasher, codec } = {}) => { if (typeof cid === 'string') cid = CID.parse(cid) - const codec = Block.codecs.get(cid.code) - return new Block({ data, cid, codec }) + hasher = hasher || Block.codec.get(cid.multihash.code) + if (!hasher) { + const { code } = cid.multihash.code + throw new Error(`Missing hasher for verification. Pass hasher for hash type ${code} or use createUnsafe()`) + } + const hash = await hasher.digest(data) + if (!bytes.equals(cid.multihash.bytes, hash.bytes)) { + throw new Error('CID hash does not match data') + } + return Block.createUnsafe(data, cid, { hasher, codec }) } Block.defaults = opts => ({ CID, defaults: opts, - encoder: (source, codec, hasher) => new Block({ - source, - codec: codec || opts.codec, - hasher: hasher || opts.hasher + encoder: (source, codec, hasher) => Block.encoder(source, codec || opts.codec, hasher || opts.hasher), + decoder: (data, codec, hasher) => Block.decoder(data, codec || opts.codec, hasher || opts.hasher), + createUnsafe: (data, cid, { hasher, codec } = {}) => Block.createUnsafe(data, cid, { + hasher: hasher || opts.hasher, + codec: codec || opts.codec }), - decoder: (data, codec, hasher) => new Block({ - data, - codec: codec || opts.codec, - hasher: hasher || opts.hasher + create: (data, cid, { hasher, codec } = {}) => Block.create(data, cid, { + hasher: hasher || opts.hasher, + codec: codec || opts.codec }), - create: Block.create, codecs: Block.codecs, add: Block.add }) diff --git a/reader.js b/reader.js index 4ce1de4..4c8d6f2 100644 --- a/reader.js +++ b/reader.js @@ -1,89 +1,90 @@ -export default CID => { - /* eslint-disable max-depth */ - const links = function * (decoded, path = []) { - if (typeof decoded !== 'object' || !decoded) return - for (const key of Object.keys(decoded)) { - const _path = path.slice() - _path.push(key) - const val = decoded[key] - if (val && typeof val === 'object') { - if (Array.isArray(val)) { - for (let i = 0; i < val.length; i++) { - const __path = _path.slice() - __path.push(i) - const o = val[i] - const cid = CID.asCID(o) - if (cid) { - yield [__path.join('/'), cid] - } else if (typeof o === 'object') { - yield * links(o, __path) - } - } - } else { - const cid = CID.asCID(val) +import { CID } from 'multiformats' + +/* eslint-disable max-depth */ +const links = function * (decoded, path = []) { + if (typeof decoded !== 'object' || !decoded) return + for (const key of Object.keys(decoded)) { + const _path = path.slice() + _path.push(key) + const val = decoded[key] + if (val && typeof val === 'object') { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + const __path = _path.slice() + __path.push(i) + const o = val[i] + const cid = CID.asCID(o) if (cid) { - yield [_path.join('/'), cid] - } else { - yield * links(val, _path) + yield [__path.join('/'), cid] + } else if (typeof o === 'object') { + yield * links(o, __path) } } + } else { + const cid = CID.asCID(val) + if (cid) { + yield [_path.join('/'), cid] + } else { + yield * links(val, _path) + } } } } +} - const tree = function * (decoded, path = []) { - if (typeof decoded !== 'object' || !decoded) return - for (const key of Object.keys(decoded)) { - const _path = path.slice() - _path.push(key) - yield _path.join('/') - const val = decoded[key] - if (val && typeof val === 'object' && !CID.asCID(val)) { - if (Array.isArray(val)) { - for (let i = 0; i < val.length; i++) { - const __path = _path.slice() - __path.push(i) - const o = val[i] - yield __path.join('/') - if (typeof o === 'object' && !CID.asCID(o)) { - yield * tree(o, __path) - } +const tree = function * (decoded, path = []) { + if (typeof decoded !== 'object' || !decoded) return + for (const key of Object.keys(decoded)) { + const _path = path.slice() + _path.push(key) + yield _path.join('/') + const val = decoded[key] + if (val && typeof val === 'object' && !CID.asCID(val)) { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + const __path = _path.slice() + __path.push(i) + const o = val[i] + yield __path.join('/') + if (typeof o === 'object' && !CID.asCID(o)) { + yield * tree(o, __path) } - } else { - yield * tree(val, _path) } + } else { + yield * tree(val, _path) } } } - /* eslint-enable max-depth */ +} +/* eslint-enable max-depth */ - class Reader { - constructor (decoded) { - Object.defineProperty(this, 'decoded', { - get: () => decoded - }) - } +class Reader { + constructor (decoded) { + Object.defineProperty(this, 'decoded', { + get: () => decoded + }) + } - get (path) { - let node = this.decoded - path = path.split('/').filter(x => x) - while (path.length) { - const key = path.shift() - if (node[key] === undefined) { throw new Error(`Object has no property ${key}`) } - node = node[key] - const cid = CID.asCID(node) - if (cid) return { value: cid, remaining: path.join('/') } - } - return { value: node } + get (path) { + let node = this.decoded + path = path.split('/').filter(x => x) + while (path.length) { + const key = path.shift() + if (node[key] === undefined) { throw new Error(`Object has no property ${key}`) } + node = node[key] + const cid = CID.asCID(node) + if (cid) return { value: cid, remaining: path.join('/') } } + return { value: node } + } - links () { - return links(this.decoded) - } + links () { + return links(this.decoded) + } - tree () { - return tree(this.decoded) - } + tree () { + return tree(this.decoded) } - return decoded => new Reader(decoded) } + +export default decoded => new Reader(decoded) From b86ea537b378c7c02d076446149dab3a1e286d9f Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Mon, 5 Oct 2020 14:48:01 -0700 Subject: [PATCH 3/7] fix: better cid Co-authored-by: Irakli Gozalishvili --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 2beb49b..23f9b39 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,8 @@ const copyBinary = value => { } const clone = obj => transform(obj, (result, value, key) => { - if (value && value.asCID === value) { + const cid = CID.asCID(value) + if (cid) { result[key] = value } else if (isBinary(value)) { result[key] = copyBinary(value) From f0c2e4454e385a2862d5f4ae67666727bf84c3df Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Mon, 5 Oct 2020 14:48:22 -0700 Subject: [PATCH 4/7] fix: cid reference Co-authored-by: Irakli Gozalishvili --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 23f9b39..c76f17d 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ const copyBinary = value => { const clone = obj => transform(obj, (result, value, key) => { const cid = CID.asCID(value) if (cid) { - result[key] = value + result[key] = cid } else if (isBinary(value)) { result[key] = copyBinary(value) } else if (typeof value === 'object' && value !== null) { From c8749a58639755a226e6ef91051db4cc8f2d98eb Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Mon, 5 Oct 2020 14:48:44 -0700 Subject: [PATCH 5/7] fix: remove support for string cids Co-authored-by: Irakli Gozalishvili --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index c76f17d..78389a8 100644 --- a/index.js +++ b/index.js @@ -130,7 +130,6 @@ Block.createUnsafe = (data, cid, { hasher, codec } = {}) => { return new Block({ data, cid, codec, hasher: hasher || null }) } Block.create = async (data, cid, { hasher, codec } = {}) => { - if (typeof cid === 'string') cid = CID.parse(cid) hasher = hasher || Block.codec.get(cid.multihash.code) if (!hasher) { const { code } = cid.multihash.code From 22eb756639366cab81eb4396fa803bdbd5336d73 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Mon, 5 Oct 2020 14:49:03 -0700 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20don=E2=80=99t=20throw=20on=20compari?= =?UTF-8?q?son?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Irakli Gozalishvili --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 78389a8..353514a 100644 --- a/index.js +++ b/index.js @@ -99,7 +99,7 @@ class Block { async equals (block) { if (block === this) return true - if (block.asBlock !== block) throw new Error('Not a block instance') + if (block.asBlock !== block) return false const [a, b] = await Promise.all([this.cid(), block.cid()]) return a.equals(b) } From a94c4d4b434214a92dcb79a166fe9bb832beca32 Mon Sep 17 00:00:00 2001 From: Mikeal Rogers Date: Tue, 6 Oct 2020 02:43:50 +0000 Subject: [PATCH 7/7] feat: separate encoder and decoder classes --- index.js | 79 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index 353514a..16b47fe 100644 --- a/index.js +++ b/index.js @@ -45,6 +45,31 @@ class Block { setImmutable(this, 'asBlock', this) } + decodeUnsafe () { + if (typeof this.opts._source !== 'undefined') return this._source + throw new Error('Block created without a decoded state') + } + + decode () { + const decoded = this.decodeUnsafe() + if (decoded === null) return null + if (isBinary(decoded)) return copyBinary(decoded) + if (immutableTypes.has(typeof decoded) || decoded === null) { + return decoded + } + return clone(decoded) + } + + encodeUnsafe () { + if (this._data) return this._data + throw new Error('Block created without an encoded state') + } + + encode () { + const data = this.encodeUnsafe() + return copyBinary(data) + } + async cid () { if (this._cid) return this._cid const hash = await this._hasher.digest(this.encodeUnsafe()) @@ -58,21 +83,19 @@ class Block { return this._codec.code } - encode () { - const data = this.encodeUnsafe() - return copyBinary(data) + reader () { + return reader(this.decodeUnsafe()) } - encodeUnsafe () { - if (this._data) return this._data - if (!this._codec) { - throw new Error('Do not have codec implemention in this Block interface') - } - const data = this._codec.encode(this._source) - setImmutable(this, '_data', data) - return data + async equals (block) { + if (block === this) return true + if (block.asBlock !== block) return false + const [a, b] = await Promise.all([this.cid(), block.cid()]) + return a.equals(b) } +} +class BlockDecoder extends Block { decodeUnsafe () { if (typeof this.opts._source !== 'undefined') return this._source if (!this._codec) { @@ -82,26 +105,16 @@ class Block { setImmutable(this, '_source', source) return source } - - decode () { - const decoded = this.decodeUnsafe() - if (decoded === null) return null - if (isBinary(decoded)) return copyBinary(decoded) - if (immutableTypes.has(typeof decoded) || decoded === null) { - return decoded +} +class BlockEncoder extends Block { + encodeUnsafe () { + if (this._data) return this._data + if (!this._codec) { + throw new Error('Do not have codec implemention in this Block interface') } - return clone(decoded) - } - - reader () { - return reader(this.decodeUnsafe()) - } - - async equals (block) { - if (block === this) return true - if (block.asBlock !== block) return false - const [a, b] = await Promise.all([this.cid(), block.cid()]) - return a.equals(b) + const data = this._codec.encode(this._source) + setImmutable(this, '_data', data) + return data } } @@ -117,17 +130,17 @@ Block.add(raw) Block.encoder = (source, codec, hasher = sha256) => { if (typeof codec === 'string') codec = Block.codecs.get(codec) if (!codec) throw new Error('Missing codec') - return new Block({ source, codec, hasher }) + return new BlockEncoder({ source, codec, hasher }) } Block.decoder = (data, codec, hasher = sha256) => { if (typeof codec === 'string') codec = Block.codecs.get(codec) if (!codec) throw new Error('Missing codec') - return new Block({ data, codec, hasher }) + return new BlockDecoder({ data, codec, hasher }) } Block.createUnsafe = (data, cid, { hasher, codec } = {}) => { codec = codec || Block.codecs.get(cid.code) if (!codec) throw new Error(`Missing codec ${cid.code}`) - return new Block({ data, cid, codec, hasher: hasher || null }) + return new BlockDecoder({ data, cid, codec, hasher: hasher || null }) } Block.create = async (data, cid, { hasher, codec } = {}) => { hasher = hasher || Block.codec.get(cid.multihash.code)