-
Notifications
You must be signed in to change notification settings - Fork 2
wip: migrate to latest multiformats #16
base: master
Are you sure you want to change the base?
Changes from all commits
062a284
fed6eb6
b86ea53
f0c2e44
c8749a5
22eb756
a94c4d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,174 +1,175 @@ | ||||||
| import withIs from 'class-is' | ||||||
| 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' | ||||||
| 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) | ||||||
| } else { | ||||||
| result[key] = value | ||||||
| } | ||||||
| }) | ||||||
|
|
||||||
| 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)) | ||||||
| } | ||||||
| const { coerce, isBinary } = bytes | ||||||
| const copyBinary = value => { | ||||||
| const b = coerce(value) | ||||||
| return coerce(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength)) | ||||||
| } | ||||||
|
|
||||||
| source () { | ||||||
| if (this.opts.cid || this.opts.data || | ||||||
| this._encoded || this._decoded) return null | ||||||
| if (!this.opts.source) return null | ||||||
| return this.opts.source | ||||||
| } | ||||||
| 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) | ||||||
| } else { | ||||||
| result[key] = value | ||||||
| } | ||||||
| }) | ||||||
|
|
||||||
| 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 | ||||||
| } | ||||||
| const setImmutable = (obj, key, value) => { | ||||||
| if (typeof value === 'undefined') throw new Error(`${key} cannot be undefined`) | ||||||
| Object.defineProperty(this, key, readonly(value)) | ||||||
| } | ||||||
|
|
||||||
| get codec () { | ||||||
| if (this.opts.code) { | ||||||
| this.opts.codec = multicodec.get(this.opts.code).name | ||||||
| } | ||||||
| if (this.opts.cid) { | ||||||
| this.opts.codec = multicodec.get(this.opts.cid.code).name | ||||||
| } | ||||||
| return this.opts.codec | ||||||
| } | ||||||
| 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) | ||||||
| } | ||||||
|
|
||||||
| get code () { | ||||||
| if (this.opts.cid) return this.opts.cid.code | ||||||
| if (!this.opts.code) { | ||||||
| this.opts.code = multicodec.get(this.codec).code | ||||||
| } | ||||||
| return this.opts.code | ||||||
| } | ||||||
| decodeUnsafe () { | ||||||
| if (typeof this.opts._source !== 'undefined') return this._source | ||||||
| throw new Error('Block created without a decoded state') | ||||||
| } | ||||||
|
|
||||||
| 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) | ||||||
| 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) | ||||||
| } | ||||||
|
|
||||||
| _encode () { | ||||||
| this._encoded = this.opts.data || multicodec.get(this.code).encode(this.opts.source) | ||||||
| } | ||||||
| encodeUnsafe () { | ||||||
| if (this._data) return this._data | ||||||
| throw new Error('Block created without an encoded state') | ||||||
| } | ||||||
|
|
||||||
| encode () { | ||||||
| if (!this._encoded) this._encode() | ||||||
| return copyBinary(this._encoded) | ||||||
| } | ||||||
| encode () { | ||||||
| const data = this.encodeUnsafe() | ||||||
| return copyBinary(data) | ||||||
| } | ||||||
|
|
||||||
| encodeUnsafe () { | ||||||
| if (!this._encoded) this._encode() | ||||||
| return this._encoded | ||||||
| } | ||||||
| async cid () { | ||||||
| if (this._cid) return this._cid | ||||||
| const hash = await this._hasher.digest(this.encodeUnsafe()) | ||||||
| const cid = CID.create(1, this.opts.codec.code, hash) | ||||||
| setImmutable(this, '_cid', cid) | ||||||
| return cid | ||||||
| } | ||||||
|
|
||||||
| _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 | ||||||
| } | ||||||
| get code () { | ||||||
| if (this._cid) return this._cid.code | ||||||
| return this._codec.code | ||||||
| } | ||||||
|
|
||||||
| 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) | ||||||
| } | ||||||
| reader () { | ||||||
| return reader(this.decodeUnsafe()) | ||||||
| } | ||||||
|
|
||||||
| decodeUnsafe () { | ||||||
| if (!this._decoded) this._decode() | ||||||
| return this._decoded | ||||||
| } | ||||||
| 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) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| reader () { | ||||||
| return reader(this.decodeUnsafe()) | ||||||
| class BlockDecoder extends Block { | ||||||
| decodeUnsafe () { | ||||||
| if (typeof this.opts._source !== 'undefined') return this._source | ||||||
| if (!this._codec) { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is obsolete because validation occurs at construction site.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. turns out, there is a case in which you want to create a Block without a codec attached. i actually have to drop the constructor error and move that check to the encoder/decoder methods, but it’ll still be necessary here so that you get a good error if you try to decode a block that doesn’t have a codec attached |
||||||
| throw new Error('Do not have codec implemention in this Block interface') | ||||||
| } | ||||||
|
|
||||||
| 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()) | ||||||
| const source = this._codec.decode(this._data) | ||||||
| setImmutable(this, '_source', source) | ||||||
| return source | ||||||
| } | ||||||
| } | ||||||
| class BlockEncoder extends Block { | ||||||
| encodeUnsafe () { | ||||||
| if (this._data) return this._data | ||||||
| if (!this._codec) { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is obsolete because it's validated at the construction site. |
||||||
| 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 | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| 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 }) | ||||||
| } | ||||||
| BlockWithIs.multiformats = multiformats | ||||||
| BlockWithIs.CID = CID | ||||||
| return BlockWithIs | ||||||
| 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) | ||||||
|
|
||||||
| export default create | ||||||
| Block.encoder = (source, codec, hasher = sha256) => { | ||||||
| if (typeof codec === 'string') codec = Block.codecs.get(codec) | ||||||
| if (!codec) throw new Error('Missing codec') | ||||||
| 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 BlockDecoder({ data, codec, hasher }) | ||||||
| } | ||||||
| Block.createUnsafe = (data, cid, { hasher, codec } = {}) => { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Suggested change
|
||||||
| codec = codec || Block.codecs.get(cid.code) | ||||||
| if (!codec) throw new Error(`Missing codec ${cid.code}`) | ||||||
| return new BlockDecoder({ data, cid, codec, hasher: hasher || null }) | ||||||
| } | ||||||
| Block.create = async (data, cid, { hasher, codec } = {}) => { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think calling this
Suggested change
|
||||||
| 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 }) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this this is a reason why
Suggested change
|
||||||
| } | ||||||
| Block.defaults = opts => ({ | ||||||
| CID, | ||||||
| defaults: opts, | ||||||
| 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 | ||||||
| }), | ||||||
| create: (data, cid, { hasher, codec } = {}) => Block.create(data, cid, { | ||||||
| hasher: hasher || opts.hasher, | ||||||
| codec: codec || opts.codec | ||||||
| }), | ||||||
| codecs: Block.codecs, | ||||||
| add: Block.add | ||||||
| }) | ||||||
| Block.CID = CID | ||||||
|
|
||||||
| export default Block | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think things would be a lot cleaner if
Block.encoderandBlock.decoderwould do above invariant checks instead of deferring that to shared constructor.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, i’m going to move all of these in the new patch to
js-multiformats.