diff --git a/README.md b/README.md index f4200be..a00da26 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,8 @@ const zlib = require('zlib') // `gitObject` is a Buffer containing a git object inflatedObject = zlib.inflateSync(gitObject) -IpldGit.util.deserialize(inflatedObject, (err, dagNode) => { - if (err) throw err - console.log(dagNode) -}) - +const dagNode = IpldGit.util.deserialize(inflatedObject) +console.log(dagNode) ``` ## Contribute diff --git a/package.json b/package.json index c9bfa6f..a080329 100644 --- a/package.json +++ b/package.json @@ -36,21 +36,18 @@ }, "homepage": "https://github.com/ipld/js-ipld-git", "dependencies": { - "async": "^2.6.2", "cids": "~0.6.0", "multicodec": "~0.5.0", "multihashes": "~0.4.14", - "multihashing-async": "~0.6.0", + "multihashing-async": "~0.7.0", "smart-buffer": "^4.0.2", - "traverse": "~0.6.6", "strftime": "~0.10.0" }, "devDependencies": { "aegir": "^18.2.1", "chai": "^4.2.0", - "deep-freeze": "0.0.1", - "dirty-chai": "^2.0.1", - "garbage": "0.0.0" + "chai-as-promised": "^7.1.1", + "dirty-chai": "^2.0.1" }, "contributors": [ "Alan Shaw ", 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 b44f0cb..cdca84c 100644 --- a/src/resolver.js +++ b/src/resolver.js @@ -1,153 +1,70 @@ 'use strict' -const util = require('./util') -const traverse = require('traverse') - -exports = module.exports - -exports.multicodec = 'git-raw' -exports.defaultHashAlg = 'sha1' +const CID = require('cids') -const personInfoPaths = [ - 'original', - 'name', - 'email', - 'date' -] - -exports.resolve = (binaryBlob, path, callback) => { - if (typeof path === 'function') { - callback = path - path = undefined - } - - util.deserialize(binaryBlob, (err, node) => { - if (err) { - return callback(err) - } +const util = require('./util') - if (!path || path === '/') { - return callback(null, { - value: node, - remainderPath: '' - }) +/** + * Resolves a path within a Git 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 Git 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) => { + 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}'`) } - if (Buffer.isBuffer(node)) { // git blob - return callback(null, { + node = node[key] + if (CID.isCID(node)) { + return { value: node, - remainderPath: path - }) - } - - const parts = path.split('/') - const val = traverse(node).get(parts) - - if (val) { - return callback(null, { - value: val, - remainderPath: '' - }) - } - - let value - let len = parts.length - - for (let i = 0; i < len; i++) { - const partialPath = parts.shift() - - if (Array.isArray(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 } - }) -} - -exports.tree = (binaryBlob, options, callback) => { - if (typeof options === 'function') { - callback = options - options = undefined } - options = options || {} - - util.deserialize(binaryBlob, (err, node) => { - if (err) { - return callback(err) - } - - if (Buffer.isBuffer(node)) { // git blob - return callback(null, []) - } - - let paths = [] - switch (node.gitType) { - case 'commit': - paths = [ - 'message', - 'tree' - ] - - paths = paths.concat(personInfoPaths.map((e) => 'author/' + e)) - paths = paths.concat(personInfoPaths.map((e) => 'committer/' + e)) - paths = paths.concat(node.parents.map((_, e) => 'parents/' + e)) - - if (node.encoding) { - paths.push('encoding') - } - break - case 'tag': - paths = [ - 'object', - 'type', - 'tag', - 'message' - ] - - if (node.tagger) { - paths = paths.concat(personInfoPaths.map((e) => 'tagger/' + e)) - } - - break - default: // tree - Object.keys(node).forEach(dir => { - paths.push(dir) - paths.push(dir + '/hash') - paths.push(dir + '/mode') - }) - } - callback(null, paths) - }) + return { + value: node, + remainderPath: '' + } } -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')) - } +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) + } +} - if (typeof result.value === 'object' && result.value['/']) { - callback(null, result.value) - } else { - callback(null, false) - } - }) +/** + * Return all available paths of a block. + * + * @generator + * @param {Buffer} binaryBlob - Binary representation of a Bitcoin block + * @yields {string} - A single path + */ +exports.tree = function * (binaryBlob) { + const node = util.deserialize(binaryBlob) + + yield * traverse(node) } diff --git a/src/util.js b/src/util.js index b153569..0771b9f 100644 --- a/src/util.js +++ b/src/util.js @@ -1,11 +1,9 @@ 'use strict' -const setImmediate = require('async/setImmediate') -const waterfall = require('async/waterfall') const multihashing = require('multihashing-async') const CID = require('cids') +const multicodec = require('multicodec') -const resolver = require('./resolver') const gitUtil = require('./util/util') const commit = require('./util/commit') @@ -14,87 +12,83 @@ const tree = require('./util/tree') exports = module.exports -exports.serialize = (dagNode, callback) => { +exports.codec = multicodec.GIT_RAW +exports.defaultHashAlg = multicodec.SHA1 + +/** + * Serialize internal representation into a binary Git block. + * + * @param {GitBlock} dagNode - Internal representation of a Git block + * @returns {Buffer} + */ +exports.serialize = (dagNode) => { if (dagNode === null) { - setImmediate(() => callback(new Error('dagNode passed to serialize was null'), null)) - return + throw new Error('dagNode passed to serialize was null') } if (Buffer.isBuffer(dagNode)) { if (dagNode.slice(0, 4).toString() === 'blob') { - setImmediate(() => callback(null, dagNode)) + return dagNode } else { - setImmediate(() => callback(new Error('unexpected dagNode passed to serialize'), null)) + throw new Error('unexpected dagNode passed to serialize') } - return } switch (dagNode.gitType) { case 'commit': - commit.serialize(dagNode, callback) - break + return commit.serialize(dagNode) case 'tag': - tag.serialize(dagNode, callback) - break + return tag.serialize(dagNode) default: // assume tree as a file named 'type' is legal - tree.serialize(dagNode, callback) + return tree.serialize(dagNode) } } -exports.deserialize = (data, callback) => { +/** + * Deserialize Git block into the internal representation. + * + * @param {Buffer} data - Binary representation of a Git block. + * @returns {BitcoinBlock} + */ +exports.deserialize = (data) => { let headLen = gitUtil.find(data, 0) let head = data.slice(0, headLen).toString() let typeLen = head.match(/([^ ]+) (\d+)/) if (!typeLen) { - setImmediate(() => callback(new Error('invalid object header'), null)) - return + throw new Error('invalid object header') } switch (typeLen[1]) { case 'blob': - callback(null, data) - break + return data case 'commit': - commit.deserialize(data.slice(headLen + 1), callback) - break + return commit.deserialize(data.slice(headLen + 1)) case 'tag': - tag.deserialize(data.slice(headLen + 1), callback) - break + return tag.deserialize(data.slice(headLen + 1)) case 'tree': - tree.deserialize(data.slice(headLen + 1), callback) - break + return tree.deserialize(data.slice(headLen + 1)) default: - setImmediate(() => callback(new Error('unknown object type ' + typeLen[1]), null)) + throw new Error('unknown object type ' + typeLen[1]) } } /** - * @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='sha1'] - Hashing algorithm - * @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 version = typeof options.version === 'undefined' ? 1 : options.version - waterfall([ - (cb) => exports.serialize(dagNode, cb), - (serialized, cb) => multihashing(serialized, hashAlg, cb), - (mh, cb) => cb(null, new CID(version, resolver.multicodec, mh)) - ], callback) +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/src/util/commit.js b/src/util/commit.js index 29fd44b..48655dc 100644 --- a/src/util/commit.js +++ b/src/util/commit.js @@ -1,16 +1,15 @@ 'use strict' -const setImmediate = require('async/setImmediate') const SmartBuffer = require('smart-buffer').SmartBuffer const gitUtil = require('./util') exports = module.exports -exports.serialize = (dagNode, callback) => { +exports.serialize = (dagNode) => { let lines = [] - lines.push('tree ' + gitUtil.cidToSha(dagNode.tree['/']).toString('hex')) + lines.push('tree ' + gitUtil.cidToSha(dagNode.tree).toString('hex')) dagNode.parents.forEach((parent) => { - lines.push('parent ' + gitUtil.cidToSha(parent['/']).toString('hex')) + lines.push('parent ' + gitUtil.cidToSha(parent).toString('hex')) }) lines.push('author ' + gitUtil.serializePersonLine(dagNode.author)) lines.push('committer ' + gitUtil.serializePersonLine(dagNode.committer)) @@ -19,7 +18,7 @@ exports.serialize = (dagNode, callback) => { } if (dagNode.mergetag) { dagNode.mergetag.forEach(tag => { - lines.push('mergetag object ' + gitUtil.cidToSha(tag.object['/']).toString('hex')) + lines.push('mergetag object ' + gitUtil.cidToSha(tag.object).toString('hex')) lines.push(tag.text) }) } @@ -37,10 +36,10 @@ exports.serialize = (dagNode, callback) => { outBuf.writeString(data.length.toString()) outBuf.writeUInt8(0) outBuf.writeString(data) - setImmediate(() => callback(null, outBuf.toBuffer())) + return outBuf.toBuffer() } -exports.deserialize = (data, callback) => { +exports.deserialize = (data) => { let lines = data.toString().split('\n') let res = { gitType: 'commit', parents: [] } @@ -48,7 +47,7 @@ exports.deserialize = (data, callback) => { let m = lines[line].match(/^([^ ]+) (.+)$/) if (!m) { if (lines[line] !== '') { - setImmediate(() => callback(new Error('Invalid commit line ' + line))) + throw new Error('Invalid commit line ' + line) } res.message = lines.slice(line + 1).join('\n') break @@ -58,7 +57,7 @@ exports.deserialize = (data, callback) => { let value = m[2] switch (key) { case 'tree': - res.tree = { '/': gitUtil.shaToCid(Buffer.from(value, 'hex')) } + res.tree = gitUtil.shaToCid(Buffer.from(value, 'hex')) break case 'committer': res.committer = gitUtil.parsePersonLine(value) @@ -67,11 +66,11 @@ exports.deserialize = (data, callback) => { res.author = gitUtil.parsePersonLine(value) break case 'parent': - res.parents.push({ '/': gitUtil.shaToCid(Buffer.from(value, 'hex')) }) + res.parents.push(gitUtil.shaToCid(Buffer.from(value, 'hex'))) break case 'gpgsig': { if (value !== '-----BEGIN PGP SIGNATURE-----') { - setImmediate(() => callback(new Error('Invalid commit line ' + line))) + throw new Error('Invalid commit line ' + line) } res.signature = {} @@ -87,12 +86,10 @@ exports.deserialize = (data, callback) => { case 'mergetag': { let mt = value.match(/^object ([0-9a-f]{40})$/) if (!mt) { - setImmediate(() => callback(new Error('Invalid commit line ' + line))) + throw new Error('Invalid commit line ' + line) } - let tag = { object: - { '/': gitUtil.shaToCid(Buffer.from(mt[1], 'hex')) } - } + let tag = { object: gitUtil.shaToCid(Buffer.from(mt[1], 'hex')) } let startLine = line for (; line < lines.length - 1; line++) { @@ -115,5 +112,7 @@ exports.deserialize = (data, callback) => { } } - setImmediate(() => callback(null, res)) + Object.defineProperty(res, 'gitType', { enumerable: false }) + + return res } diff --git a/src/util/tag.js b/src/util/tag.js index c084cfe..2311248 100644 --- a/src/util/tag.js +++ b/src/util/tag.js @@ -1,14 +1,13 @@ 'use strict' -const setImmediate = require('async/setImmediate') const SmartBuffer = require('smart-buffer').SmartBuffer const gitUtil = require('./util') exports = module.exports -exports.serialize = (dagNode, callback) => { +exports.serialize = (dagNode) => { let lines = [] - lines.push('object ' + gitUtil.cidToSha(dagNode.object['/']).toString('hex')) + lines.push('object ' + gitUtil.cidToSha(dagNode.object).toString('hex')) lines.push('type ' + dagNode.type) lines.push('tag ' + dagNode.tag) if (dagNode.tagger !== null) { @@ -24,10 +23,10 @@ exports.serialize = (dagNode, callback) => { outBuf.writeString(data.length.toString()) outBuf.writeUInt8(0) outBuf.writeString(data) - setImmediate(() => callback(null, outBuf.toBuffer())) + return outBuf.toBuffer() } -exports.deserialize = (data, callback) => { +exports.deserialize = (data) => { let lines = data.toString().split('\n') let res = { gitType: 'tag' } @@ -35,7 +34,7 @@ exports.deserialize = (data, callback) => { let m = lines[line].match(/^([^ ]+) (.+)$/) if (m === null) { if (lines[line] !== '') { - setImmediate(() => callback(new Error('Invalid tag line ' + line))) + throw new Error('Invalid tag line ' + line) } res.message = lines.slice(line + 1).join('\n') break @@ -45,7 +44,7 @@ exports.deserialize = (data, callback) => { let value = m[2] switch (key) { case 'object': - res.object = { '/': gitUtil.shaToCid(Buffer.from(value, 'hex')) } + res.object = gitUtil.shaToCid(Buffer.from(value, 'hex')) break case 'tagger': res.tagger = gitUtil.parsePersonLine(value) @@ -61,5 +60,7 @@ exports.deserialize = (data, callback) => { } } - setImmediate(() => callback(null, res)) + Object.defineProperty(res, 'gitType', { enumerable: false }) + + return res } diff --git a/src/util/tree.js b/src/util/tree.js index d7b38fa..6f7610e 100644 --- a/src/util/tree.js +++ b/src/util/tree.js @@ -1,12 +1,11 @@ 'use strict' -const setImmediate = require('async/setImmediate') const SmartBuffer = require('smart-buffer').SmartBuffer const gitUtil = require('./util') exports = module.exports -exports.serialize = (dagNode, callback) => { +exports.serialize = (dagNode) => { let entries = [] Object.keys(dagNode).forEach((name) => { entries.push([name, dagNode[name]]) @@ -19,7 +18,7 @@ exports.serialize = (dagNode, callback) => { let buf = new SmartBuffer() entries.forEach((entry) => { buf.writeStringNT(entry[1].mode + ' ' + entry[0]) - buf.writeBuffer(gitUtil.cidToSha(entry[1].hash['/'])) + buf.writeBuffer(gitUtil.cidToSha(entry[1].hash)) }) let outBuf = new SmartBuffer() @@ -27,10 +26,10 @@ exports.serialize = (dagNode, callback) => { outBuf.writeString(buf.length.toString()) outBuf.writeUInt8(0) outBuf.writeBuffer(buf.toBuffer()) - setImmediate(() => callback(null, outBuf.toBuffer())) + return outBuf.toBuffer() } -exports.deserialize = (data, callback) => { +exports.deserialize = (data) => { let res = {} let buf = SmartBuffer.fromBuffer(data, 'utf8') @@ -43,18 +42,18 @@ exports.deserialize = (data, callback) => { let hash = buf.readBuffer(gitUtil.SHA1_LENGTH) let modNameMatched = modeName.match(/^(\d+) (.+)$/) if (!modNameMatched) { - setImmediate(() => callback(new Error('invalid file mode/name'))) + throw new Error('invalid file mode/name') } if (res[modNameMatched[2]]) { - setImmediate(() => callback(new Error('duplicate file in tree'))) + throw new Error('duplicate file in tree') } res[modNameMatched[2]] = { mode: modNameMatched[1], - hash: { '/': gitUtil.shaToCid(hash) } + hash: gitUtil.shaToCid(hash) } } - setImmediate(() => callback(null, res)) + return res } diff --git a/src/util/util.js b/src/util/util.js index ad96dc0..73e6fc6 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -1,8 +1,6 @@ 'use strict' -const SmartBuffer = require('smart-buffer').SmartBuffer const multihashes = require('multihashes/src/constants') -const multicodecs = require('multicodec/src/base-table') const multihash = require('multihashes') const CID = require('cids') const strftime = require('strftime') @@ -58,17 +56,12 @@ exports.serializePersonLine = (node) => { } exports.shaToCid = (buf) => { - let mhashBuf = new SmartBuffer() - mhashBuf.writeUInt8(1) - mhashBuf.writeBuffer(multicodecs['git-raw']) - mhashBuf.writeUInt8(multihashes.names.sha1) - mhashBuf.writeUInt8(exports.SHA1_LENGTH) - mhashBuf.writeBuffer(buf) - return mhashBuf.toBuffer() + const mh = multihash.encode(buf, 'sha1') + return new CID(1, 'git-raw', mh) } -exports.cidToSha = (cidBuf) => { - let mh = multihash.decode(new CID(cidBuf).multihash) +exports.cidToSha = (cid) => { + let mh = multihash.decode(cid.multihash) if (mh.name !== 'sha1') { return null } diff --git a/test/mod.spec.js b/test/mod.spec.js new file mode 100644 index 0000000..9f49c47 --- /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 git-raw', () => { + expect(mod.codec).to.equal(multicodec.GIT_RAW) + }) + + it('defaultHashAlg is sha1', () => { + expect(mod.defaultHashAlg).to.equal(multicodec.SHA1) + }) +}) diff --git a/test/parse.spec.js b/test/parse.spec.js index 163fe10..36f4372 100644 --- a/test/parse.spec.js +++ b/test/parse.spec.js @@ -11,7 +11,6 @@ const loadFixture = require('aegir/fixtures') const zlib = require('zlib') const ipldGit = require('../src') const util = require('../src/util/util') -const waterfall = require('async/waterfall') const testObjectsJSON = require('./fixtures/objects.json') @@ -74,35 +73,16 @@ describe('utils', () => { }) describe('git object parsing', () => { - let objects + const objects = testObjectsJSON.map( + (o) => [o, zlib.inflateSync(loadFixture('test/fixtures/' + o))] + ) - before(function (done) { - this.timeout(5000) - objects = testObjectsJSON.map(o => [o, zlib.inflateSync(loadFixture('test/fixtures/' + o))]) - done() - }) - - it('is parsing and serializing properly', (done) => { - waterfall(objects.map((object) => { - return (cb) => { - ipldGit.util.deserialize(object[1], (err, node) => { - expect(err).to.not.exist() - expect(node).to.exist() - - let expCid = util.shaToCid(Buffer.from(object[0], 'hex')) - - ipldGit.util.cid(node, (err, cid) => { - expect(err).to.not.exist() - expect(cid).to.exist() - - expect(cid.buffer.toString('hex')).to.equal(expCid.toString('hex'), 'expected ' + - object[0] + ', got ' + cid.toBaseEncodedString('base16') + ', objtype ' + - node._objtype + ', blob:' + Buffer.isBuffer(node)) + it('is parsing and serializing properly', async () => { + for (const object of objects) { + const expCid = util.shaToCid(Buffer.from(object[0], 'hex')) - cb(null) - }) - }) - } - }), done) + const cid = await ipldGit.util.cid(object[1]) + expect(cid.equals(expCid)).to.be.true() + } }) }) diff --git a/test/resolver.spec.js b/test/resolver.spec.js index 654657b..b1bab0d 100644 --- a/test/resolver.spec.js +++ b/test/resolver.spec.js @@ -7,8 +7,6 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) -const waterfall = require('async/waterfall') -const parallel = require('async/parallel') const CID = require('cids') const ipldGit = require('../src') @@ -23,9 +21,9 @@ describe('IPLD format resolver (local)', () => { before((done) => { const commitNode = { gitType: 'commit', - tree: { '/': new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr').buffer }, + tree: new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr'), parents: [ - { '/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer } + new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH') ], author: { name: 'John Doe', @@ -43,7 +41,7 @@ describe('IPLD format resolver (local)', () => { const tagNode = { gitType: 'tag', - object: { '/': new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e').buffer }, + object: new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e'), type: 'commit', tag: 'v0.0.0', tagger: { @@ -56,270 +54,155 @@ describe('IPLD format resolver (local)', () => { const treeNode = { somefile: { - hash: { '/': new CID('z8mWaJNVTadD7oum3m7f1dmarHvYhFV5b').buffer }, + hash: new CID('z8mWaJNVTadD7oum3m7f1dmarHvYhFV5b'), mode: '100644' }, somedir: { - hash: { '/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer }, + hash: new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH'), mode: '40000' } } treeNode['somedir.notactuallyadir'] = { - hash: { '/': new CID('z8mWaJNVTadD7oum3m7f1dmarHvYhFV5b').buffer }, + hash: new CID('z8mWaJNVTadD7oum3m7f1dmarHvYhFV5b'), mode: '100644' } treeNode['somefile.notactuallyafile'] = { - hash: { '/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer }, + hash: new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH'), mode: '40000' } const blobNode = Buffer.from('626c6f62203800736f6d6564617461', 'hex') // blob 8\0somedata - waterfall([ - (cb) => parallel([ - (cb) => ipldGit.util.serialize(commitNode, cb), - (cb) => ipldGit.util.serialize(tagNode, cb), - (cb) => ipldGit.util.serialize(treeNode, cb), - (cb) => ipldGit.util.serialize(blobNode, cb) - ], cb), - (blocks, cb) => { - commitBlob = blocks[0] - tagBlob = blocks[1] - treeBlob = blocks[2] - blobBlob = blocks[3] - cb() - } - ], done) + commitBlob = ipldGit.util.serialize(commitNode) + tagBlob = ipldGit.util.serialize(tagNode) + treeBlob = ipldGit.util.serialize(treeNode) + blobBlob = ipldGit.util.serialize(blobNode) + done() }) describe('commit', () => { - it('resolver.tree', (done) => { - resolver.tree(commitBlob, (err, paths) => { - expect(err).to.not.exist() - - expect(paths).to.eql([ - 'message', - 'tree', - 'author/original', - 'author/name', - 'author/email', - 'author/date', - 'committer/original', - 'committer/name', - 'committer/email', - 'committer/date', - 'parents/0', - 'encoding' - ]) - - done() - }) - }) - - it('resolver.isLink with valid Link', (done) => { - resolver.isLink(commitBlob, 'tree', (err, link) => { - expect(err).to.not.exist() - const linkCID = new CID(link['/']) - expect(CID.isCID(linkCID)).to.equal(true) - done() - }) - }) - - it('resolver.isLink with invalid Link', (done) => { - resolver.isLink(commitBlob, '', (err, link) => { - expect(err).to.not.exist() - expect(link).to.equal(false) - done() - }) + it('resolver.tree', () => { + const tree = resolver.tree(commitBlob) + const paths = [...tree] + + expect(paths).to.have.members([ + 'message', + 'tree', + 'author', + 'author/name', + 'author/email', + 'author/date', + 'committer', + 'committer/name', + 'committer/email', + 'committer/date', + 'parents', + 'parents/0', + 'encoding' + ]) }) describe('resolver.resolve', () => { - it('path within scope', (done) => { - resolver.resolve(commitBlob, 'message', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('Encoded\n') - done() - }) + it('path within scope', () => { + const result = resolver.resolve(commitBlob, 'message') + expect(result.value).to.equal('Encoded\n') }) - it('path within scope, but nested', (done) => { - resolver.resolve(commitBlob, 'author/name', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('John Doe') - done() - }) + it('path within scope, but nested', () => { + const result = resolver.resolve(commitBlob, 'author/name') + expect(result.value).to.equal('John Doe') }) - it('path out of scope', (done) => { - resolver.resolve(commitBlob, 'tree/foo/hash/bar/mode', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.eql({ - '/': new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr').buffer - }) - expect(result.remainderPath).to.equal('foo/hash/bar/mode') - done() - }) + it('path out of scope', () => { + const result = resolver.resolve(commitBlob, 'tree/foo/hash/bar/mode') + expect(result.value.equals( + new CID('z8mWaJ1dZ9fH5EetPuRsj8jj26pXsgpsr')) + ).to.be.true() + expect(result.remainderPath).to.equal('foo/hash/bar/mode') }) }) }) describe('tag', () => { - it('resolver.tree', (done) => { - resolver.tree(tagBlob, (err, paths) => { - expect(err).to.not.exist() - - expect(paths).to.eql([ - 'object', - 'type', - 'tag', - 'message', - 'tagger/original', - 'tagger/name', - 'tagger/email', - 'tagger/date' - ]) - - done() - }) - }) - - it('resolver.isLink with valid Link', (done) => { - resolver.isLink(tagBlob, 'object', (err, link) => { - expect(err).to.not.exist() - const linkCID = new CID(link['/']) - expect(CID.isCID(linkCID)).to.equal(true) - done() - }) - }) - - it('resolver.isLink with invalid Link', (done) => { - resolver.isLink(tagBlob, '', (err, link) => { - expect(err).to.not.exist() - expect(link).to.equal(false) - done() - }) + it('resolver.tree', () => { + const tree = resolver.tree(tagBlob) + const paths = [...tree] + + expect(paths).to.have.members([ + 'object', + 'type', + 'tag', + 'message', + 'tagger', + 'tagger/name', + 'tagger/email', + 'tagger/date' + ]) }) describe('resolver.resolve', () => { - it('path within scope', (done) => { - resolver.resolve(tagBlob, 'message', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('A message\n') - done() - }) + it('path within scope', () => { + const result = resolver.resolve(tagBlob, 'message') + expect(result.value).to.equal('A message\n') }) - it('path within scope, but nested', (done) => { - resolver.resolve(tagBlob, 'tagger/name', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('John Doe') - done() - }) + it('path within scope, but nested', () => { + const result = resolver.resolve(tagBlob, 'tagger/name') + expect(result.value).to.equal('John Doe') }) - it('path out of scope', (done) => { - resolver.resolve(tagBlob, 'object/tree/foo/mode', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.eql({ - '/': new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e').buffer - }) - expect(result.remainderPath).to.equal('tree/foo/mode') - done() - }) + it('path out of scope', () => { + const result = resolver.resolve(tagBlob, 'object/tree/foo/mode') + expect(result.value.equals( + new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e') + )).to.be.true() + expect(result.remainderPath).to.equal('tree/foo/mode') }) }) }) describe('tree', () => { - it('resolver.tree', (done) => { - resolver.tree(treeBlob, (err, paths) => { - expect(err).to.not.exist() - - expect(paths).to.eql([ - 'somedir.notactuallyadir', - 'somedir.notactuallyadir/hash', - 'somedir.notactuallyadir/mode', - 'somedir', - 'somedir/hash', - 'somedir/mode', - 'somefile', - 'somefile/hash', - 'somefile/mode', - 'somefile.notactuallyafile', - 'somefile.notactuallyafile/hash', - 'somefile.notactuallyafile/mode' - ]) - - done() - }) - }) - - it('resolver.isLink with valid Link', (done) => { - resolver.isLink(treeBlob, 'somefile/hash', (err, link) => { - expect(err).to.not.exist() - const linkCID = new CID(link['/']) - expect(CID.isCID(linkCID)).to.equal(true) - done() - }) - }) - - it('resolver.isLink with invalid Link', (done) => { - resolver.isLink(treeBlob, '', (err, link) => { - expect(err).to.not.exist() - expect(link).to.equal(false) - done() - }) + it('resolver.tree', () => { + const tree = resolver.tree(treeBlob) + const paths = [...tree] + + expect(paths).to.have.members([ + 'somedir.notactuallyadir', + 'somedir.notactuallyadir/hash', + 'somedir.notactuallyadir/mode', + 'somedir', + 'somedir/hash', + 'somedir/mode', + 'somefile', + 'somefile/hash', + 'somefile/mode', + 'somefile.notactuallyafile', + 'somefile.notactuallyafile/hash', + 'somefile.notactuallyafile/mode' + ]) }) describe('resolver.resolve', () => { - it('path within scope, nested', (done) => { - resolver.resolve(treeBlob, 'somedir/mode', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.equal('40000') - done() - }) + it('path within scope, nested', () => { + const result = resolver.resolve(treeBlob, 'somedir/mode') + expect(result.value).to.equal('40000') }) - it('path out of scope', (done) => { - resolver.resolve(treeBlob, 'somedir/hash/subfile/mode', (err, result) => { - expect(err).to.not.exist() - expect(result.value).to.eql({ - '/': new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH').buffer - }) - expect(result.remainderPath).to.equal('subfile/mode') - done() - }) + it('path out of scope', () => { + const result = resolver.resolve(treeBlob, 'somedir/hash/subfile/mode') + expect(result.value.equals( + new CID('z8mWaFY1zpiZSXTBrz8i6A3o9vNvAs2CH') + )).to.be.true() + expect(result.remainderPath).to.equal('subfile/mode') }) }) }) describe('blob', () => { - it('resolver.tree', (done) => { - resolver.tree(blobBlob, (err, paths) => { - expect(err).to.not.exist() - expect(paths).to.eql([]) - done() - }) + it('resolver.tree', () => { + const paths = resolver.tree(blobBlob).next() + expect(paths.value).to.be.undefined() + expect(paths.done).to.be.true() }) - - it('resolver.isLink with invalid Link', (done) => { - resolver.isLink(treeBlob, '', (err, link) => { - expect(err).to.not.exist() - expect(link).to.equal(false) - done() - }) - }) - }) -}) - -describe('IPLD format resolver API properties', () => { - it('should have `multicodec` defined correctly', (done) => { - expect(resolver.multicodec).to.equal('git-raw') - done() - }) - - it('should have `defaultHashAlg` defined correctly', (done) => { - expect(resolver.defaultHashAlg).to.equal('sha1') - done() }) }) diff --git a/test/util.spec.js b/test/util.spec.js index 759cdcb..86df6c0 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -2,17 +2,20 @@ 'use strict' const chai = require('chai') +const chaiAsProised = require('chai-as-promised') const dirtyChai = require('dirty-chai') const expect = chai.expect +chai.use(chaiAsProised) chai.use(dirtyChai) const ipldGit = require('../src') +const multicodec = require('multicodec') const multihash = require('multihashes') const CID = require('cids') describe('IPLD format util', () => { const tagNode = { gitType: 'tag', - object: { '/': new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e').buffer }, + object: new CID('z8mWaHQaEAKd5KMRNU3npB3saSZmhFh3e'), type: 'commit', tag: 'v0.0.0', tagger: { @@ -22,47 +25,42 @@ describe('IPLD format util', () => { }, message: 'A message\n' } + const tagBlob = ipldGit.util.serialize(tagNode) - it('.serialize and .deserialize', (done) => { - ipldGit.util.serialize(tagNode, (err, serialized) => { - expect(err).to.not.exist() - expect(Buffer.isBuffer(serialized)).to.equal(true) - ipldGit.util.deserialize(serialized, (err, deserialized) => { - expect(err).to.not.exist() - expect(deserialized).to.eql(tagNode) - done() - }) - }) + it('.serialize and .deserialize', () => { + expect(Buffer.isBuffer(tagBlob)).to.be.true() + const deserialized = ipldGit.util.deserialize(tagBlob) + + // The `gitType` is not enumerable, hence `eql()` would find it. Thus + // remove that property so that that check passes + const expected = Object.assign({}, tagNode) + delete expected.gitType + expect(deserialized).to.eql(expected) }) - it('.cid', (done) => { - ipldGit.util.cid(tagNode, (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('git-raw') - expect(cid.multihash).to.exist() - const mh = multihash.decode(cid.multihash) - expect(mh.name).to.equal('sha1') - done() - }) + it('.cid', async () => { + const cid = await ipldGit.util.cid(tagBlob) + expect(cid.version).to.equal(1) + expect(cid.codec).to.equal('git-raw') + expect(cid.multihash).to.exist() + const mh = multihash.decode(cid.multihash) + expect(mh.name).to.equal('sha1') }) - it('.cid with options', (done) => { - ipldGit.util.cid(tagNode, { hashAlg: 'sha3-512' }, (err, cid) => { - expect(err).to.not.exist() - expect(cid.version).to.equal(1) - expect(cid.codec).to.equal('git-raw') - expect(cid.multihash).to.exist() - const mh = multihash.decode(cid.multihash) - expect(mh.name).to.equal('sha3-512') - done() + it('.cid with options', async () => { + const cid = await ipldGit.util.cid(tagBlob, { + hashAlg: multicodec.SHA3_512 }) + expect(cid.version).to.equal(1) + expect(cid.codec).to.equal('git-raw') + expect(cid.multihash).to.exist() + const mh = multihash.decode(cid.multihash) + expect(mh.name).to.equal('sha3-512') }) - it('.cid errors unknown hashAlg', (done) => { - ipldGit.util.cid(tagNode, { hashAlg: 'unknown' }, (err, cid) => { - expect(err).to.exist() - done() - }) + it('.cid errors unknown hashAlg', async () => { + await expect(ipldGit.util.cid(tagNode, { + hashAlg: 0xffffff } + )).to.be.rejectedWith('Unrecognized function code: 16777215') }) })