Skip to content
This repository was archived by the owner on Jun 2, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions basics.js

This file was deleted.

7 changes: 4 additions & 3 deletions defaults.js
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
299 changes: 150 additions & 149 deletions index.js
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')
Copy link

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.encoder and Block.decoder would do above invariant checks instead of deferring that to shared constructor.

Copy link
Contributor Author

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.

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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is obsolete because validation occurs at construction site.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {
Copy link

Choose a reason for hiding this comment

The 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 } = {}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think createUnsafeDecoder is more descriptive name, and also makes it clear why codec needs to be provided. I think it should not take hasher because it does not need it.

Suggested change
Block.createUnsafe = (data, cid, { hasher, codec } = {}) => {
Block.createUnsafeDecoder = (data, cid, { codec } = {}) => {

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 } = {}) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think calling this decode would be more descriptive and explain why it's async.

Suggested change
Block.create = async (data, cid, { hasher, codec } = {}) => {
Block.decode = async (data, cid, { hasher, 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 })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this this is a reason why createUnsafe to takes hasher. Just using constructor here can simplify createUnsafe.

Suggested change
return Block.createUnsafe(data, cid, { hasher, codec })
return new Block({ cid, data, codec, hasher })

}
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
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -43,9 +43,6 @@
".": {
"import": "./index.js"
},
"./basics": {
"import": "./basics.js"
},
"./defaults": {
"import": "./defaults.js"
}
Expand Down
Loading