From 63c6613dac097418e23aa7c13d5ed3035ce014d3 Mon Sep 17 00:00:00 2001 From: Volker Mische Date: Wed, 10 Apr 2019 19:19:45 +0200 Subject: [PATCH] feat: new IPLD Format API BREAKING CHANGE: The API is now async/await based There are numerous changes, the most significant one is that the API is no longer callback based, but it using async/await. For the full new API please see the [IPLD Formats spec]. [IPLD Formats spec]: https://github.com/ipld/interface-ipld-format --- README.md | 35 ++++---- package.json | 6 +- src/index.js | 2 + src/resolver.js | 165 ++++++++++++-------------------------- src/util.js | 107 +++++++++++-------------- test/interop.spec.js | 156 ++++++++++++------------------------ test/mod.spec.js | 20 +++++ test/resolver.spec.js | 180 ++++++++++++++---------------------------- test/util.spec.js | 178 ++++++++++++++--------------------------- 9 files changed, 306 insertions(+), 543 deletions(-) create mode 100644 test/mod.spec.js diff --git a/README.md b/README.md index 4177a8c..9fafb05 100644 --- a/README.md +++ b/README.md @@ -72,22 +72,12 @@ const file = { size: 11 } -dagCBOR.util.serialize(file, (err, serialized) => { - if (err) { - throw err - } +const serialized = dagCBOR.util.serialize(file) +console.log(`Encoded as a ${serialized.length} byte Buffer`) - console.log(`Encoded as a ${serialized.length} byte Buffer`) - - dagCBOR.util.deserialize(serialized, (err, node) => { - if (err) { - throw err - } - - console.log('Decoded as:', node) - require('assert').deepEqual(node, file) // should match - }) -}) +const node = dagCBOR.util.deserialize(serialized) +console.log('Decoded as:', node) +require('assert').deepEqual(node, file) // should match // → Encoded as a 22 byte Buffer // → Decoded as: { name: 'hello.txt', size: 11 } @@ -95,19 +85,21 @@ dagCBOR.util.serialize(file, (err, serialized) => { ## API -### `dagCBOR.util.serialize(obj, callback)` +### `dagCBOR.util.serialize(obj)` Encodes an object into IPLD CBOR form, replacing any CIDs found within the object to CBOR tags (with an id of `42`). - `obj` (any): any object able to be serialized as CBOR - - `callback` (`Function`): function to be called when serialization is complete, arguments are: `error` and the serialized node if no error was encountered. - ### `dagCBOR.util.deserialize(serialized, callback)` +Returns the serialized node. + +### `dagCBOR.util.deserialize(serialized)` Decodes an IPLD CBOR encoded representation, restoring any CBOR tags (id `42`) to CIDs. - `serialized` (`Buffer` or `String`): a binary blob representing an IPLD CBOR encoded object. - - `callback` (`Function`): function to be called when deserialization is complete, arguments are: `error` and the deserialized object if no error was encountered. + +Returns the deserialized object. ### `dagCBOR.util.configureDecoder([options])` @@ -123,7 +115,7 @@ Possible values in the `options` argument are: Calling `dagCBOR.util.configureDecoder()` with no arguments will reset to the default decoder `size`, `maxSize` and `tags`. -### `dagCBOR.util.cid(obj[, options,] callback)` +### `dagCBOR.util.cid(obj[, options,])` Create a [CID](https://github.com/multiformats/js-cid) for the given unserialized object. @@ -132,7 +124,8 @@ Create a [CID](https://github.com/multiformats/js-cid) for the given unserialize * `hashAlg` (`String`): a [registered multicodec](https://github.com/multiformats/multicodec/blob/master/table.csv) hash algorithm. * `hashLen` (`String`): an optional hash length * `version` (`Number`): CID version number, defaults to `1` - - `callback` (`Function`): function to be called when the object is serialized and a CID is created from that serialized form, arguments are: `error` and the created CID if no error was encountered. + +Returns a Promise with the created CID. ## Contribute diff --git a/package.json b/package.json index 156ed14..1c6f3ec 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,11 @@ "borc": "^2.1.0", "cids": "~0.6.0", "is-circular": "^1.0.2", - "multihashing-async": "~0.6.0", - "traverse": "~0.6.6" + "multicodec": "~0.5.0", + "multihashing-async": "~0.7.0" }, "devDependencies": { "aegir": "^18.2.0", - "async": "^2.6.2", - "bs58": "^4.0.1", "chai": "^4.2.0", "detect-node": "^2.0.4", "dirty-chai": "^2.0.1", diff --git a/src/index.js b/src/index.js index adc8a1b..b693da8 100644 --- a/src/index.js +++ b/src/index.js @@ -2,3 +2,5 @@ exports.util = require('./util.js') exports.resolver = require('./resolver.js') +exports.codec = exports.util.codec +exports.defaultHashAlg = exports.util.defaultHashAlg diff --git a/src/resolver.js b/src/resolver.js index fd978e5..8190e62 100644 --- a/src/resolver.js +++ b/src/resolver.js @@ -1,135 +1,70 @@ 'use strict' -const util = require('./util') -const traverse = require('traverse') const CID = require('cids') -exports = module.exports - -exports.multicodec = 'dag-cbor' -exports.defaultHashAlg = 'sha2-256' +const util = require('./util') -/* - * resolve: receives a path and a binary blob and returns the value on path, - * throw if not possible. `binaryBlob` is CBOR encoded data. +/** + * Resolves a path within a CBOR block. + * + * Returns the value or a link and the partial mising path. This way the + * IPLD Resolver can fetch the link and continue to resolve. + * + * @param {Buffer} binaryBlob - Binary representation of a CBOR block + * @param {string} [path='/'] - Path that should be resolved + * @returns {Object} result - Result of the path it it was resolved successfully + * @returns {*} result.value - Value the path resolves to + * @returns {string} result.remainderPath - If the path resolves half-way to a + * link, then the `remainderPath` is the part after the link that can be used + * for further resolving */ -exports.resolve = (binaryBlob, path, callback) => { - if (typeof path === 'function') { - callback = path - path = undefined - } - - util.deserialize(binaryBlob, (err, node) => { - if (err) { - return callback(err) +exports.resolve = (binaryBlob, path) => { + let node = util.deserialize(binaryBlob) + + const parts = path.split('/').filter(Boolean) + while (parts.length) { + const key = parts.shift() + if (node[key] === undefined) { + throw new Error(`Object has no property '${key}'`) } - // root - - if (!path || path === '/') { - return callback(null, { + node = node[key] + if (CID.isCID(node)) { + return { value: node, - remainderPath: '' - }) - } - - // within scope - - const parts = path.split('/') - const val = traverse(node).get(parts) - - if (val !== undefined) { - return callback(null, { - value: val, - remainderPath: '' - }) - } - - // out of scope - let value - const len = parts.length - - for (let i = 0; i < len; i++) { - const partialPath = parts.shift() - - if (Array.isArray(node) && !Buffer.isBuffer(node)) { - value = node[Number(partialPath)] - } if (node[partialPath]) { - value = node[partialPath] - } else { - // can't traverse more - if (!value) { - return callback(new Error('path not available at root')) - } else { - parts.unshift(partialPath) - return callback(null, { - value: value, - remainderPath: parts.join('/') - }) - } + remainderPath: parts.join('/') } - node = value } - }) -} - -function flattenObject (obj, delimiter) { - delimiter = delimiter || '/' - - if (Object.keys(obj).length === 0) { - return [] } - return traverse(obj).reduce(function (acc, x) { - if (CID.isCID(x)) { - this.update(undefined) - } - const path = this.path.join(delimiter) - - if (path !== '') { - acc.push({ path: path, value: x }) - } - return acc - }, []) + return { + value: node, + remainderPath: '' + } } -/* - * tree: returns a flattened array with paths: values of the project. options - * are option (i.e. nestness) - */ -exports.tree = (binaryBlob, options, callback) => { - if (typeof options === 'function') { - callback = options - options = undefined +const traverse = function * (node, path) { + // Traverse only objects and arrays + if (Buffer.isBuffer(node) || CID.isCID(node) || typeof node === 'string' || + node === null) { + return + } + for (const item of Object.keys(node)) { + const nextpath = path === undefined ? item : path + '/' + item + yield nextpath + yield * traverse(node[item], nextpath) } - - options = options || {} - - util.deserialize(binaryBlob, (err, node) => { - if (err) { - return callback(err) - } - const flat = flattenObject(node) - const paths = flat.map((el) => el.path) - - callback(null, paths) - }) } -exports.isLink = (binaryBlob, path, callback) => { - exports.resolve(binaryBlob, path, (err, result) => { - if (err) { - return callback(err) - } - - if (result.remainderPath.length > 0) { - return callback(new Error('path out of scope')) - } +/** + * Return all available paths of a block. + * + * @generator + * @param {Buffer} binaryBlob - Binary representation of a CBOR block + * @yields {string} - A single path + */ +exports.tree = function * (binaryBlob) { + const node = util.deserialize(binaryBlob) - if (CID.isCID(result.value)) { - callback(null, result.value) - } else { - callback(null, false) - } - }) + yield * traverse(node) } diff --git a/src/util.js b/src/util.js index 8cabea2..ccdebfa 100644 --- a/src/util.js +++ b/src/util.js @@ -1,12 +1,11 @@ 'use strict' const cbor = require('borc') +const multicodec = require('multicodec') const multihashing = require('multihashing-async') const CID = require('cids') const isCircular = require('is-circular') -const resolver = require('./resolver') - // https://github.com/ipfs/go-ipfs/issues/3570#issuecomment-273931692 const CID_CBOR_TAG = 42 @@ -49,14 +48,7 @@ function replaceCIDbyTAG (dagNode) { const keys = Object.keys(obj) - if (keys.length === 1 && keys[0] === '/') { - // Multiaddr encoding - // if (typeof link === 'string' && isMultiaddr(link)) { - // link = new Multiaddr(link).buffer - // } - - return tagCID(obj['/']) - } else if (keys.length > 0) { + if (keys.length > 0) { // Recursive transform const out = {} keys.forEach((key) => { @@ -77,6 +69,9 @@ function replaceCIDbyTAG (dagNode) { exports = module.exports +exports.codec = multicodec.DAG_CBOR +exports.defaultHashAlg = multicodec.SHA2_256 + const defaultTags = { [CID_CBOR_TAG]: (val) => { // remove that 0 @@ -90,6 +85,14 @@ const defaultMaxSize = 64 * 1024 * 1024 // max heap size when auto-growing, 64 M let maxSize = defaultMaxSize let decoder = null +/** + * Configure the underlying CBOR decoder. + * + * @param {Object} [options] - The options the decoder takes. The decoder will reset to the defaul values if no options are given. + * @param {number} [options.size=65536] - The current heap size used in CBOR parsing, this may grow automatically as larger blocks are encountered up to `maxSize` + * @param {number} [options.maxSize=67108864] - The maximum size the CBOR parsing heap is allowed to grow to before `dagCBOR.util.deserialize()` returns an error + * @param {Object} [options.tags] - An object whose keys are CBOR tag numbers and values are transform functions that accept a `value` and return a decoded representation of that `value` + */ exports.configureDecoder = (options) => { let tags = defaultTags @@ -121,69 +124,55 @@ exports.configureDecoder = (options) => { exports.configureDecoder() // Setup default cbor.Decoder -exports.serialize = (dagNode, callback) => { - let serialized +/** + * Serialize internal representation into a binary CBOR block. + * + * @param {Object} node - Internal representation of a CBOR block + * @returns {Buffer} - The encoded binary representation + */ +exports.serialize = (node) => { + const nodeTagged = replaceCIDbyTAG(node) + const serialized = cbor.encode(nodeTagged) - try { - const dagNodeTagged = replaceCIDbyTAG(dagNode) - serialized = cbor.encode(dagNodeTagged) - } catch (err) { - return setImmediate(() => callback(err)) - } - setImmediate(() => callback(null, serialized)) + return serialized } -exports.deserialize = (data, callback) => { - let deserialized - +/** + * Deserialize CBOR block into the internal representation. + * + * @param {Buffer} data - Binary representation of a CBOR block + * @returns {Object} - An object that conforms to the IPLD Data Model + */ +exports.deserialize = (data) => { if (data.length > currentSize && data.length <= maxSize) { exports.configureDecoder({ size: data.length }) } if (data.length > currentSize) { - return setImmediate(() => callback(new Error('Data is too large to deserialize with current decoder'))) + throw new Error('Data is too large to deserialize with current decoder') } - try { - deserialized = decoder.decodeFirst(data) - } catch (err) { - return setImmediate(() => callback(err)) - } + const deserialized = decoder.decodeFirst(data) - setImmediate(() => callback(null, deserialized)) + return deserialized } /** - * @callback CidCallback - * @param {?Error} error - Error if getting the CID failed - * @param {?CID} cid - CID if call was successful - */ -/** - * Get the CID of the DAG-Node. + * Calculate the CID of the binary blob. * - * @param {Object} dagNode - Internal representation - * @param {Object} [options] - Options to create the CID - * @param {number} [options.version=1] - CID version number - * @param {string} [options.hashAlg] - Defaults to hashAlg for the resolver - * @param {number} [options.hashLen] - Optionally trim the digest to this length - * @param {CidCallback} callback - Callback that handles the return value - * @returns {void} + * @param {Object} binaryBlob - Encoded IPLD Node + * @param {Object} [userOptions] - Options to create the CID + * @param {number} [userOptions.cidVersion=1] - CID version number + * @param {string} [UserOptions.hashAlg] - Defaults to the defaultHashAlg of the format + * @returns {Promise.} */ -exports.cid = (dagNode, options, callback) => { - if (typeof options === 'function') { - callback = options - options = {} - } - options = options || {} - const hashAlg = options.hashAlg || resolver.defaultHashAlg - const hashLen = options.hashLen - const version = typeof options.version === 'undefined' ? 1 : options.version - - exports.serialize(dagNode, (err, serialized) => { - if (err) return callback(err) - multihashing(serialized, hashAlg, hashLen, (err, mh) => { - if (err) return callback(err) - callback(null, new CID(version, resolver.multicodec, mh)) - }) - }) +exports.cid = async (binaryBlob, userOptions) => { + const defaultOptions = { cidVersion: 1, hashAlg: exports.defaultHashAlg } + const options = Object.assign(defaultOptions, userOptions) + + const multihash = await multihashing(binaryBlob, options.hashAlg) + const codecName = multicodec.print[exports.codec] + const cid = new CID(options.cidVersion, codecName, multihash) + + return cid } diff --git a/test/interop.spec.js b/test/interop.spec.js index 07789de..7ac4ccd 100644 --- a/test/interop.spec.js +++ b/test/interop.spec.js @@ -1,6 +1,4 @@ /* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ - 'use strict' const chai = require('chai') @@ -36,133 +34,83 @@ describe('dag-cbor interop tests', () => { if (!isNode) { return } describe('deserialize and compare', () => { - it('array-link', (done) => { - dagCBOR.util.deserialize(arrayLinkCBOR, (err, node) => { - expect(err).to.not.exist() - - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.arrayLink)).to.be.true() - done() - }) - }) - }) + it('array-link', async () => { + const node = dagCBOR.util.deserialize(arrayLinkCBOR) + expect(node).to.eql(arrayLinkJS) - it('empty-array', (done) => { - dagCBOR.util.deserialize(emptyArrayCBOR, (err, node) => { - expect(err).to.not.exist() - expect(node).to.eql(emptyArrayJS) - - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.emptyArray)).to.be.true() - done() - }) - }) + const cid = await dagCBOR.util.cid(arrayLinkCBOR) + expect(cid.equals(expectedCIDs.arrayLink)).to.be.true() }) - it('empty-obj', (done) => { - dagCBOR.util.deserialize(emptyObjCBOR, (err, node) => { - expect(err).to.not.exist() - expect(node).to.eql(emptyObjJS) - - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.emptyObj)).to.be.true() - done() - }) - }) + it('empty-array', async () => { + const node = dagCBOR.util.deserialize(emptyArrayCBOR) + expect(node).to.eql(emptyArrayJS) + + const cid = await dagCBOR.util.cid(emptyArrayCBOR) + expect(cid.equals(expectedCIDs.emptyArray)).to.be.true() }) - it('foo', (done) => { - dagCBOR.util.deserialize(fooCBOR, (err, node) => { - expect(err).to.not.exist() - expect(node).to.eql(fooJS) - - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.foo)).to.be.true() - done() - }) - }) + it('empty-obj', async () => { + const node = dagCBOR.util.deserialize(emptyObjCBOR) + expect(node).to.eql(emptyObjJS) + + const cid = await dagCBOR.util.cid(emptyObjCBOR) + expect(cid.equals(expectedCIDs.emptyObj)).to.be.true() }) - it('obj-no-link', (done) => { - dagCBOR.util.deserialize(objNoLinkCBOR, (err, node) => { - expect(err).to.not.exist() - expect(node).to.eql(objNoLinkJS) - - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.objNoLink)).to.be.true() - done() - }) - }) + it('foo', async () => { + const node = dagCBOR.util.deserialize(fooCBOR) + expect(node).to.eql(fooJS) + + const cid = await dagCBOR.util.cid(fooCBOR) + expect(cid.equals(expectedCIDs.foo)).to.be.true() }) - it('obj-with-link', (done) => { - if (!isNode) { done() } + it('obj-no-link', async () => { + const node = dagCBOR.util.deserialize(objNoLinkCBOR) + expect(node).to.eql(objNoLinkJS) + + const cid = await dagCBOR.util.cid(objNoLinkCBOR) + expect(cid.equals(expectedCIDs.objNoLink)).to.be.true() + }) - dagCBOR.util.deserialize(objWithLinkCBOR, (err, node) => { - expect(err).to.not.exist() + it('obj-with-link', async () => { + if (!isNode) { return } - dagCBOR.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid.equals(expectedCIDs.objWithLink)).to.be.true() - done() - }) - }) + const cid = await dagCBOR.util.cid(objWithLinkCBOR) + expect(cid.equals(expectedCIDs.objWithLink)).to.be.true() }) }) describe('serialise and compare', () => { - it('array-link', (done) => { - dagCBOR.util.serialize(arrayLinkJS, (err, serialized) => { - expect(err).to.not.exist() - - expect(serialized).to.eql(arrayLinkCBOR) - done() - }) + it('array-link', () => { + const serialized = dagCBOR.util.serialize(arrayLinkJS) + expect(serialized).to.eql(arrayLinkCBOR) }) - it('empty-array', (done) => { - dagCBOR.util.serialize(emptyArrayJS, (err, serialized) => { - expect(err).to.not.exist() - expect(serialized).to.eql(emptyArrayCBOR) - done() - }) + it('empty-array', () => { + const serialized = dagCBOR.util.serialize(emptyArrayJS) + expect(serialized).to.eql(emptyArrayCBOR) }) - it('empty-obj', (done) => { - dagCBOR.util.serialize(emptyObjJS, (err, serialized) => { - expect(err).to.not.exist() - expect(serialized).to.eql(emptyObjCBOR) - done() - }) + it('empty-obj', () => { + const serialized = dagCBOR.util.serialize(emptyObjJS) + expect(serialized).to.eql(emptyObjCBOR) }) - it('foo', (done) => { - dagCBOR.util.serialize(fooJS, (err, serialized) => { - expect(err).to.not.exist() - expect(serialized).to.eql(fooCBOR) - done() - }) + it('foo', () => { + const serialized = dagCBOR.util.serialize(fooJS) + expect(serialized).to.eql(fooCBOR) }) - it('obj-no-link', (done) => { - dagCBOR.util.serialize(objNoLinkJS, (err, serialized) => { - expect(err).to.not.exist() - expect(serialized).to.eql(objNoLinkCBOR) - done() - }) + it('obj-no-link', () => { + const serialized = dagCBOR.util.serialize(objNoLinkJS) + expect(serialized).to.eql(objNoLinkCBOR) }) - it('obj-with-link', (done) => { - dagCBOR.util.serialize(objWithLinkJS, (err, serialized) => { - expect(err).to.not.exist() - expect(serialized).to.eql(objWithLinkCBOR) - done() - }) + it('obj-with-link', () => { + const serialized = dagCBOR.util.serialize(objWithLinkJS) + expect(serialized).to.eql(objWithLinkCBOR) }) }) }) diff --git a/test/mod.spec.js b/test/mod.spec.js new file mode 100644 index 0000000..e13f912 --- /dev/null +++ b/test/mod.spec.js @@ -0,0 +1,20 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const multicodec = require('multicodec') + +const mod = require('../src') + +describe('IPLD Format', () => { + it('codec is dag-cbor', () => { + expect(mod.codec).to.equal(multicodec.DAG_CBOR) + }) + + it('defaultHashAlg is sha2-256', () => { + expect(mod.defaultHashAlg).to.equal(multicodec.SHA2_256) + }) +}) diff --git a/test/resolver.spec.js b/test/resolver.spec.js index bbe6fbc..5fbdeec 100644 --- a/test/resolver.spec.js +++ b/test/resolver.spec.js @@ -1,5 +1,5 @@ -/* eslint max-nested-callbacks: ["error", 8] */ /* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ 'use strict' const chai = require('chai') @@ -7,159 +7,99 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') -const each = require('async/each') const CID = require('cids') const dagCBOR = require('../src') const resolver = dagCBOR.resolver describe('IPLD format resolver (local)', () => { - let emptyNodeBlob - let nodeBlob - - before((done) => { - const emptyNode = {} - const node = { - name: 'I am a node', - someLink: { - '/': 'QmaNh5d3hFiqJAGjHmvxihSnWDGqYZCn7H2XHpbttYjCNE' - }, - nest: { - foo: { - bar: 'baz' - } - }, - array: [ - { a: 'b' }, - 2 - ] - } - - waterfall([ - (cb) => parallel([ - (cb) => dagCBOR.util.serialize(emptyNode, cb), - (cb) => dagCBOR.util.serialize(node, cb) - ], cb), - (blocks, cb) => { - emptyNodeBlob = blocks[0] - nodeBlob = blocks[1] - cb() + const emptyNode = {} + const node = { + name: 'I am a node', + someLink: new CID('QmaNh5d3hFiqJAGjHmvxihSnWDGqYZCn7H2XHpbttYjCNE'), + nest: { + foo: { + bar: 'baz' } - ], done) - }) - - it('multicodec is dag-cbor', () => { - expect(resolver.multicodec).to.equal('dag-cbor') - }) - - it('defaultHashAlg is sha2-256', () => { - expect(resolver.defaultHashAlg).to.equal('sha2-256') - }) + }, + array: [ + { a: 'b' }, + 2 + ], + nullValue: null, + boolValue: true + } + + const emptyNodeBlob = dagCBOR.util.serialize(emptyNode) + const nodeBlob = dagCBOR.util.serialize(node) describe('empty node', () => { describe('resolver.resolve', () => { - it('root', (done) => { - resolver.resolve(emptyNodeBlob, '/', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.be.eql({}) - done() - }) + it('root', () => { + const result = resolver.resolve(emptyNodeBlob, '/') + expect(result.value).to.be.eql({}) }) }) - it('resolver.tree', (done) => { - resolver.tree(emptyNodeBlob, (err, paths) => { - expect(err).to.not.exist() - expect(paths).to.eql([]) - done() - }) + it('resolver.tree', () => { + const paths = resolver.tree(emptyNodeBlob).next() + expect(paths.value).to.be.undefined() + expect(paths.done).to.be.true() }) }) describe('node', () => { - it('resolver.tree', (done) => { - resolver.tree(nodeBlob, (err, paths) => { - expect(err).to.not.exist() - - expect(paths).to.eql([ - 'name', - 'nest', - 'nest/foo', - 'nest/foo/bar', - 'array', - 'array/0', - 'array/0/a', - 'array/1', - 'someLink' - ]) - - done() - }) - }) - - it('resolver.isLink with valid Link', (done) => { - resolver.isLink(nodeBlob, 'someLink', (err, link) => { - expect(err).to.not.exist() - expect(CID.isCID(link)).to.equal(true) - done() - }) - }) - - it('resolver.isLink with invalid Link', (done) => { - resolver.isLink(nodeBlob, '', (err, link) => { - expect(err).to.not.exist() - expect(link).to.equal(false) - done() - }) + it('resolver.tree', () => { + const tree = resolver.tree(nodeBlob) + const paths = [...tree] + expect(paths).to.have.members([ + 'name', + 'nest', + 'nest/foo', + 'nest/foo/bar', + 'array', + 'array/0', + 'array/0/a', + 'array/1', + 'someLink', + 'boolValue', + 'nullValue' + ]) }) describe('resolver.resolve', () => { - it('path within scope', (done) => { - resolver.resolve(nodeBlob, 'name', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('I am a node') - done() - }) + it('path within scope', () => { + const result = resolver.resolve(nodeBlob, 'name') + expect(result.value).to.equal('I am a node') }) - it('path within scope, but nested', (done) => { - resolver.resolve(nodeBlob, 'nest/foo/bar', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('baz') - done() - }) + it('path within scope, but nested', () => { + const result = resolver.resolve(nodeBlob, 'nest/foo/bar') + expect(result.value).to.equal('baz') }) - it('should resolve falsy values for path within scope', (done) => { - const node = { + it('should resolve falsy values for path within scope', () => { + const falsyNode = { nu11: null, f4lse: false, empty: '', zero: 0 } - dagCBOR.util.serialize(node, (err, nodeBlob) => { - expect(err).to.not.exist() + const falsyNodeBlob = dagCBOR.util.serialize(falsyNode) - each(Object.keys(node), (key, cb) => { - resolver.resolve(nodeBlob, key, (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal(node[key]) - cb() - }) - }, done) + Object.keys(falsyNode).map((key) => { + const result = resolver.resolve(falsyNodeBlob, key) + expect(result.value).to.equal(falsyNode[key]) }) }) - it('path out of scope', (done) => { - resolver.resolve(nodeBlob, 'someLink/a/b/c', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.eql(new CID('QmaNh5d3hFiqJAGjHmvxihSnWDGqYZCn7H2XHpbttYjCNE')) - expect(result.remainderPath).to.equal('a/b/c') - done() - }) + it('path out of scope', () => { + const result = resolver.resolve(nodeBlob, 'someLink/a/b/c') + expect(result.value.equals( + new CID('QmaNh5d3hFiqJAGjHmvxihSnWDGqYZCn7H2XHpbttYjCNE')) + ).to.be.true() + expect(result.remainderPath).to.equal('a/b/c') }) }) }) diff --git a/test/util.spec.js b/test/util.spec.js index 36f6fc2..659330b 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -6,7 +6,6 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) const garbage = require('garbage') -const map = require('async/map') const dagCBOR = require('../src') const multihash = require('multihashes') const CID = require('cids') @@ -24,150 +23,89 @@ describe('util', () => { link: new CID('QmRgutAxd8t7oGkSm4wmeuByG6M51wcTso6cubDdQtuEfL') } } + const serializedObj = dagCBOR.util.serialize(obj) - it('.serialize and .deserialize', (done) => { - dagCBOR.util.serialize(obj, (err, serialized) => { - expect(err).to.not.exist() - expect(Buffer.isBuffer(serialized)).to.equal(true) - - // Check for the tag 42 - // d8 = tag, 2a = 42 - expect( - serialized.toString('hex').match(/d82a/g) - ).to.have.length(4) - - dagCBOR.util.deserialize(serialized, (err, deserialized) => { - expect(err).to.not.exist() - expect(obj).to.eql(deserialized) - done() - }) - }) + it('.serialize and .deserialize', () => { + expect(Buffer.isBuffer(serializedObj)).to.equal(true) + + // Check for the tag 42 + // d8 = tag, 2a = 42 + expect( + serializedObj.toString('hex').match(/d82a/g) + ).to.have.length(4) + + const deserializedObj = dagCBOR.util.deserialize(serializedObj) + expect(obj).to.eql(deserializedObj) }) - it('.serialize and .deserialize large objects', (done) => { + it('.serialize and .deserialize large objects', () => { // larger than the default borc heap size, should auto-grow the heap const dataSize = 128 * 1024 const largeObj = { someKey: [].slice.call(new Uint8Array(dataSize)) } - dagCBOR.util.serialize(largeObj, (err, serialized) => { - expect(err).to.not.exist() - expect(Buffer.isBuffer(serialized)).to.be.true() - - dagCBOR.util.deserialize(serialized, (err, deserialized) => { - expect(err).to.not.exist() - expect(largeObj).to.eql(deserialized) - // reset decoder to default - dagCBOR.util.configureDecoder() - done() - }) - }) + const serialized = dagCBOR.util.serialize(largeObj) + expect(Buffer.isBuffer(serialized)).to.be.true() + + const deserialized = dagCBOR.util.deserialize(serialized) + expect(largeObj).to.eql(deserialized) + // reset decoder to default + dagCBOR.util.configureDecoder() }) - it('.deserialize fail on large objects beyond maxSize', (done) => { + it('.deserialize fail on large objects beyond maxSize', () => { // larger than the default borc heap size, should bust the heap if we turn off auto-grow const dataSize = (128 * 1024) + 1 const largeObj = { someKey: [].slice.call(new Uint8Array(dataSize)) } dagCBOR.util.configureDecoder({ size: 64 * 1024, maxSize: 128 * 1024 }) // 64 Kb start, 128 Kb max - dagCBOR.util.serialize(largeObj, (err, serialized) => { - expect(err).to.not.exist() - expect(Buffer.isBuffer(serialized)).to.be.true() - - dagCBOR.util.deserialize(serialized, (err, deserialized) => { - expect(err).to.be.an('error') - expect(deserialized).to.not.exist() - // reset decoder to default - dagCBOR.util.configureDecoder() - done() - }) - }) - }) + const serialized = dagCBOR.util.serialize(largeObj) + expect(Buffer.isBuffer(serialized)).to.be.true() - it('error catching', (done) => { - const circlarObj = {} - circlarObj.a = circlarObj - dagCBOR.util.serialize(circlarObj, (err, serialized) => { - expect(err).to.exist() - expect(serialized).to.not.exist() - done() - }) + expect(() => dagCBOR.util.deserialize(serialized)).to.throw( + 'Data is too large to deserialize with current decoder') + // reset decoder to default + dagCBOR.util.configureDecoder() }) - it('.cid', (done) => { - dagCBOR.util.cid(obj, (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('dag-cbor') - expect(cid.multihash).to.exist() - const mh = multihash.decode(cid.multihash) - expect(mh.name).to.equal('sha2-256') - done() - }) + it('.serialize and .deserialize object with slash as property', () => { + const slashObject = { '/': true } + const serialized = dagCBOR.util.serialize(slashObject) + const deserialized = dagCBOR.util.deserialize(serialized) + expect(deserialized).to.eql(slashObject) }) - it('.cid with hashAlg', (done) => { - dagCBOR.util.cid(obj, { hashAlg: 'sha2-512' }, (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('dag-cbor') - expect(cid.multihash).to.exist() - const mh = multihash.decode(cid.multihash) - expect(mh.name).to.equal('sha2-512') - expect(mh.length).to.equal(64) - done() - }) + it('error catching', () => { + const circlarObj = {} + circlarObj.a = circlarObj + expect(() => dagCBOR.util.serialize(circlarObj)).to.throw( + 'The object passed has circular reference') }) - it('.cid with hashAlg and hashLen', (done) => { - dagCBOR.util.cid(obj, { hashAlg: 'keccak-256', hashLen: 28 }, (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('dag-cbor') - expect(cid.multihash).to.exist() - const mh = multihash.decode(cid.multihash) - expect(mh.name).to.equal('keccak-256') - expect(mh.length).to.equal(28) - // The CID must be 32 bytes including 4 bytes for - // - expect(cid.buffer.length).to.equal(32) - expect(cid.toBaseEncodedString()).to.equal('z6dSUELEcAsg5oXs7gsv42rYfczTLizSBTpGUa5M3bxe') - done() - }) + it('.cid', async () => { + const cid = await dagCBOR.util.cid(serializedObj) + expect(cid.version).to.equal(1) + expect(cid.codec).to.equal('dag-cbor') + expect(cid.multihash).to.exist() + const mh = multihash.decode(cid.multihash) + expect(mh.name).to.equal('sha2-256') }) - it('strings', (done) => { - dagCBOR.util.cid('some test string', (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('dag-cbor') - expect(cid.multihash).to.exist() - done() - }) + it('.cid with hashAlg', async () => { + const cid = await dagCBOR.util.cid(serializedObj, { hashAlg: 'sha2-512' }) + expect(cid.version).to.equal(1) + expect(cid.codec).to.equal('dag-cbor') + expect(cid.multihash).to.exist() + const mh = multihash.decode(cid.multihash) + expect(mh.name).to.equal('sha2-512') + expect(mh.length).to.equal(64) }) - it.skip('serialize and deserialize - garbage', (done) => { - const inputs = [] - - for (let i = 0; i < 1000; i++) { - inputs.push({ in: garbage(100) }) + it('fuzz serialize and deserialize with garbage', () => { + for (let ii = 0; ii < 1000; ii++) { + let original = { in: garbage(100) } + let encoded = dagCBOR.util.serialize(original) + let decoded = dagCBOR.util.deserialize(encoded) + expect(decoded).to.eql(original) } - - map(inputs, (input, cb) => { - dagCBOR.util.serialize(input, cb) - }, (err, encoded) => { - if (err) { - return done(err) - } - map(encoded, (enc, cb) => { - dagCBOR.util.deserialize(enc, cb) - }, (err, out) => { - if (err) { - return done(err) - } - - expect(inputs).to.eql(out) - done() - }) - }) }) })