diff --git a/package.json b/package.json index 2a173ba..7573e18 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ipld", + "name": "ipld-dag-cbor", "version": "0.6.0", "description": "JavaScript implementation of the IPLD (InterpPlanetary Linked Data)", "main": "lib/index.js", @@ -39,22 +39,21 @@ "dependencies": { "babel-runtime": "^6.6.1", "bs58": "^3.0.0", - "cbor": "^1.0.4", - "lodash.clonedeep": "^4.3.2", - "lodash.defaults": "^4.0.1", - "lodash.includes": "^4.1.3", - "multiaddr": "^2.0.0", + "cbor": "^2.0.1", + "cids": "^0.2.0", "multihashes": "^0.2.2", "multihashing": "^0.2.1", - "nofilter": "0.0.2" + "traverse": "^0.6.6" }, "devDependencies": { - "aegir": "^3.0.4", + "aegir": "8.1.2", + "async": "^2.1.2", "chai": "^3.5.0", + "ipfs-block": "^0.4.0", "pre-commit": "^1.1.3" }, "contributors": [ "David Dias ", "dignifiedquire " ] -} \ No newline at end of file +} diff --git a/src/cbor.js b/src/cbor.js deleted file mode 100644 index 4269700..0000000 --- a/src/cbor.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict' - -const cbor = require('cbor') -const cborUtils = require('cbor/lib/utils') -const Multiaddr = require('multiaddr') -const NoFilter = require('nofilter') -const defaults = require('lodash.defaults') -const includes = require('lodash.includes') -const cloneDeep = require('lodash.clonedeep') - -exports = module.exports - -exports.LINK_TAG = 258 -const LINK_SYMBOL = exports.LINK_SYMBOL = '/' - -exports.marshal = (original) => { - const input = cloneDeep(original) - - function transform (obj) { - if (obj == null) return obj - const keys = Object.keys(obj) - - // Recursive transform - keys.forEach((key) => { - if (typeof obj[key] === 'object') { - obj[key] = transform(obj[key]) - } - }) - - if (includes(keys, LINK_SYMBOL)) { - let link = obj[LINK_SYMBOL] - - // Multiaddr encoding - if (typeof link === 'string' && isMultiaddr(link)) { - link = new Multiaddr(link).buffer - } - - // Remove the @link - delete obj[LINK_SYMBOL] - - // Non empty - if (keys.length > 1) { - throw new Error('Links must not have siblings') - } - - return new cbor.Tagged(exports.LINK_TAG, link) - } - - return obj - } - - return cbor.encode(transform(input)) -} - -exports.unmarshal = (input, opts) => { - opts = defaults(opts || {}, { - encoding: cborUtils.guessEncoding(input) - }) - - const dec = new cbor.Decoder(opts) - const bs = new NoFilter() - - dec.pipe(bs) - dec.end(input, opts.encoding) - - const res = bs.read() - - function transform (obj) { - Object.keys(obj).forEach((key) => { - const val = obj[key] - // This is safe as we reference the same cbor instance - // as we used to decode with - if (val instanceof cbor.Tagged) { - if (typeof val.value === 'string') { - obj[key] = { - [LINK_SYMBOL]: val.value - } - } else if (Buffer.isBuffer(val.value)) { - obj[key] = { - [LINK_SYMBOL]: (new Multiaddr(val.value)).toString() - } - } else { - obj[key] = defaults({ - [LINK_SYMBOL]: val.value[0] - }, transform(val.value[1])) - } - } else if (typeof val === 'object') { - obj[key] = transform(val) - } - }) - - return obj - } - - return transform(res) -} - -function isMultiaddr (str) { - try { - const addr = new Multiaddr(str) - return addr.toString() === str - } catch (err) { - return false - } -} diff --git a/src/index.js b/src/index.js index a917452..adc8a1b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,4 @@ 'use strict' -const cbor = require('./cbor') -const multihash = require('./multihash') - -exports = module.exports - -exports.LINK_TAG = cbor.LINK_TAG -exports.LINK_SYMBOL = cbor.LINK_SYMBOL -exports.marshal = cbor.marshal -exports.unmarshal = cbor.unmarshal -exports.multihash = multihash +exports.util = require('./util.js') +exports.resolver = require('./resolver.js') diff --git a/src/multihash.js b/src/multihash.js deleted file mode 100644 index 9938a0d..0000000 --- a/src/multihash.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -const multihashing = require('multihashing') -const bs58 = require('bs58') - -exports = module.exports = function multihash (obj) { - if (obj.marshal && typeof obj.marshal === 'function') { - // Handle IPLD like objects - obj = obj.marshal() - } - - const multi = multihashing(obj, 'sha2-256') - return bs58.encode(multi) -} diff --git a/src/resolver.js b/src/resolver.js new file mode 100644 index 0000000..7ad2943 --- /dev/null +++ b/src/resolver.js @@ -0,0 +1,117 @@ +'use strict' + +const util = require('./util') +const traverse = require('traverse') + +exports = module.exports + +exports.multicodec = 'dag-cbor' + +/* + * resolve: receives a path and a block and returns the value on path, + * throw if not possible. `block` is an IPFS Block instance (contains data + key) + */ +exports.resolve = (block, path, callback) => { + if (typeof path === 'function') { + callback = path + path = undefined + } + + util.deserialize(block.data, (err, node) => { + if (err) { + return callback(err) + } + + // root + + if (!path || path === '/') { + return callback(null, { + value: node, + remainderPath: '' + }) + } + + // within scope + + // const tree = exports.tree(block) + const parts = path.split('/') + const val = traverse(node).get(parts) + + if (val) { + return callback(null, { + value: val, + remainderPath: '' + }) + } + + // out of scope + let value + let 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('/') + }) + } + } + node = value + } + }) +} + +/* + * tree: returns a flattened array with paths: values of the project. options + * are option (i.e. nestness) + */ +exports.tree = (block, options, callback) => { + if (typeof options === 'function') { + callback = options + options = undefined + } + + if (!options) { + options = {} + } + + util.deserialize(block.data, (err, node) => { + if (err) { + return callback(err) + } + + callback(null, flattenObject(node)) + }) +} + +function flattenObject (obj, delimiter) { + if (!delimiter) { + delimiter = '/' + } + + if (Object.keys(obj).length === 0) { + return [] + } + + return traverse(obj).reduce(function (acc, x) { + if (this.isLeaf) { + acc.push({ + path: this.path.join(delimiter), + value: x + }) + } + + return acc + }, []) +} diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..4c7f313 --- /dev/null +++ b/src/util.js @@ -0,0 +1,26 @@ +'use strict' + +const cbor = require('cbor') +const multihashing = require('multihashing') +const CID = require('cids') +const resolver = require('./resolver') + +exports = module.exports + +exports.serialize = (dagNode, callback) => { + callback(null, cbor.encode(dagNode)) +} + +exports.deserialize = (data, callback) => { + cbor.decodeFirst(data, callback) +} + +exports.cid = (dagNode, callback) => { + exports.serialize(dagNode, (err, serialized) => { + if (err) { + return callback(err) + } + const mh = multihashing(serialized, 'sha2-256') + callback(null, new CID(1, resolver.multicodec, mh)) + }) +} diff --git a/test/cbor.spec.js b/test/cbor.spec.js deleted file mode 100644 index 1132e49..0000000 --- a/test/cbor.spec.js +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const expect = require('chai').expect -const cbor = require('cbor') -const Multiaddr = require('multiaddr') - -const ipld = require('../src') - -describe('IPLD -> CBOR', () => { - it('no /', () => { - const src = { - data: 'hello world', - size: 11 - } - - const expected = { - data: 'hello world', - size: 11 - } - - expect( - ipld.marshal(src) - ).to.be.eql( - cbor.encode(expected) - ) - }) - - it('/, is a string', () => { - const src = { - data: 'hello world', - size: 11, - l1: {'/': 'hello-world'} - } - - const expected = { - data: 'hello world', - size: 11, - l1: new cbor.Tagged(ipld.LINK_TAG, 'hello-world') - } - - expect( - ipld.marshal(src) - ).to.be.eql( - cbor.encode(expected) - ) - }) - - it('/, is a valid multiaddress', () => { - const addr1 = new Multiaddr('/ip4/127.0.0.1/udp/1234') - const addr2 = new Multiaddr('/ipfs/Qmafmh1Cw3H1bwdYpaaj5AbCW4LkYyUWaM7Nykpn5NZoYL') - const src = { - data: 'hello world', - size: 11, - l1: {'/': addr1.toString()}, - l2: {'/': addr2.toString()} - } - - const expected = { - data: 'hello world', - size: 11, - l1: new cbor.Tagged(ipld.LINK_TAG, addr1.buffer), - l2: new cbor.Tagged(ipld.LINK_TAG, addr2.buffer) - } - - expect( - ipld.marshal(src) - ).to.be.eql( - cbor.encode(expected) - ) - }) - - it('/, with properties', () => { - const src = { - data: 'hello world', - size: 11, - secret: { - '/': 'hello-world', - i: 'should not be here' - } - } - - expect( - () => ipld.marshal(src) - ).to.throw( - 'Links must not have siblings' - ) - }) - - it('nested /', () => { - const src = { - data: 'hello world', - size: 11, - l1: {'/': 'hello-world'}, - secret: { - l1: {'/': 'secret-link'} - } - } - - const expected = { - data: 'hello world', - size: 11, - l1: new cbor.Tagged(ipld.LINK_TAG, 'hello-world'), - secret: { - l1: new cbor.Tagged(ipld.LINK_TAG, 'secret-link') - } - } - - expect( - ipld.marshal(src) - ).to.be.eql( - cbor.encode(expected) - ) - }) - - it('does not modify the input', () => { - let src = { - l1: {'/': 'hello'} - } - - ipld.marshal(src) - - expect(src).to.be.eql({ - l1: {'/': 'hello'} - }) - }) - - it('marshals objects with null values', () => { - const src = { - l1: {'/': 'hello'}, - foo: null - } - - const expected = { - l1: new cbor.Tagged(ipld.LINK_TAG, 'hello'), - foo: null - } - - expect( - ipld.marshal(src) - ).to.be.eql( - cbor.encode(expected) - ) - }) -}) - -describe('CBOR -> IPLD', () => { - it('no links', () => { - const src = cbor.encode({ - data: 'hello world', - size: 11 - }) - - const target = { - data: 'hello world', - size: 11 - } - - expect( - ipld.unmarshal(src) - ).to.be.eql( - target - ) - }) - - it('one link, without properties', () => { - const src = cbor.encode({ - data: 'hello world', - size: 11, - nested: new cbor.Tagged(ipld.LINK_TAG, 'hello-world') - }) - - const target = { - data: 'hello world', - size: 11, - nested: { - '/': 'hello-world' - } - } - - expect( - ipld.unmarshal(src) - ).to.be.eql( - target - ) - }) -}) diff --git a/test/multihash.spec.js b/test/multihash.spec.js deleted file mode 100644 index f9c5d2a..0000000 --- a/test/multihash.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -var expect = require('chai').expect -var ipld = require('../src') - -describe('multihash', () => { - it('works on marshalled objects', () => { - var file = ipld.marshal({ - name: 'hello.txt', - size: 11 - }) - - expect( - ipld.multihash(file) - ).to.be.eql( - 'QmQtX5JVbRa25LmQ1LHFChkXWW5GaWrp7JpymN4oPuBSmL' - ) - }) - - it('works on objects that have .marshal function', () => { - class File { - constructor (name, size) { - this.name = name - this.size = size - } - - marshal () { - return ipld.marshal({ - name: this.name, - size: this.size - }) - } - } - - var file = new File('hello.txt', 11) - - expect( - ipld.multihash(file) - ).to.be.eql( - 'QmQtX5JVbRa25LmQ1LHFChkXWW5GaWrp7JpymN4oPuBSmL' - ) - }) -}) diff --git a/test/resolver.spec.js b/test/resolver.spec.js new file mode 100644 index 0000000..4ee03a5 --- /dev/null +++ b/test/resolver.spec.js @@ -0,0 +1,125 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const dagCBOR = require('../src') +const resolver = dagCBOR.resolver +const Block = require('ipfs-block') +const series = require('async/series') + +describe('IPLD format resolver (local)', () => { + let emptyNodeBlock + let nodeBlock + + before((done) => { + const emptyNode = {} + const node = { + name: 'I am a node', + someLink: { '/': 'LINK' }, + nest: { + foo: { + bar: 'baz' + } + }, + array: [ + { a: 'b' }, + 2 + ] + } + + series([ + (cb) => { + dagCBOR.util.serialize(emptyNode, (err, serialized) => { + expect(err).to.not.exist + emptyNodeBlock = new Block(serialized) + cb() + }) + }, + (cb) => { + dagCBOR.util.serialize(node, (err, serialized) => { + expect(err).to.not.exist + nodeBlock = new Block(serialized) + cb() + }) + } + ], done) + }) + + it('multicodec is dag-cbor', () => { + expect(resolver.multicodec).to.equal('dag-cbor') + }) + + describe('empty node', () => { + describe('resolver.resolve', () => { + it('root', (done) => { + resolver.resolve(emptyNodeBlock, '/', (err, result) => { + expect(err).to.not.exist + expect(result.value).to.be.eql({}) + done() + }) + }) + }) + + it('resolver.tree', (done) => { + resolver.tree(emptyNodeBlock, (err, paths) => { + expect(err).to.not.exist + expect(paths).to.eql([]) + done() + }) + }) + }) + + describe('node', () => { + describe('resolver.resolve', () => { + it('path within scope', (done) => { + resolver.resolve(nodeBlock, 'name', (err, result) => { + expect(err).to.not.exist + expect(result.value).to.equal('I am a node') + done() + }) + }) + + it('path within scope, but nested', (done) => { + resolver.resolve(nodeBlock, 'nest/foo/bar', (err, result) => { + expect(err).to.not.exist + expect(result.value).to.equal('baz') + done() + }) + }) + + it('path out of scope', (done) => { + resolver.resolve(nodeBlock, 'someLink/a/b/c', (err, result) => { + expect(err).to.not.exist + expect(result.value).to.eql({ '/': 'LINK' }) + expect(result.remainderPath).to.equal('a/b/c') + done() + }) + }) + }) + + it('resolver.tree', (done) => { + resolver.tree(nodeBlock, (err, paths) => { + expect(err).to.not.exist + expect(paths).to.eql([{ + path: 'name', + value: 'I am a node' + }, { + // TODO: confirm how to represent links in tree + path: 'someLink//', + value: 'LINK' + }, { + path: 'nest/foo/bar', + value: 'baz' + }, { + path: 'array/0/a', + value: 'b' + }, { + path: 'array/1', + value: 2 + }]) + done() + }) + }) + }) +}) diff --git a/test/util.spec.js b/test/util.spec.js new file mode 100644 index 0000000..981ebf7 --- /dev/null +++ b/test/util.spec.js @@ -0,0 +1,35 @@ +/* eslint-env mocha */ +'use strict' + +const expect = require('chai').expect +const dagCBOR = require('../src') + +describe('util', () => { + const obj = { + someKey: 'someValue', + link: { '/': 'aaaaa' } + } + + it('.serialize and .deserialize', (done) => { + dagCBOR.util.serialize(obj, (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(obj).to.eql(deserialized) + done() + }) + }) + }) + + 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 + done() + }) + }) +})