Skip to content
This repository has been archived by the owner on Aug 24, 2021. It is now read-only.

Commit

Permalink
feat: new IPLD Format API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vmx committed May 8, 2019
1 parent 784c464 commit e39a7d9
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 525 deletions.
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <alan.shaw@protocol.ai>",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

exports.util = require('./util.js')
exports.resolver = require('./resolver.js')
exports.codec = exports.util.codec
exports.defaultHashAlg = exports.util.defaultHashAlg
193 changes: 55 additions & 138 deletions src/resolver.js
Original file line number Diff line number Diff line change
@@ -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)
}
96 changes: 45 additions & 51 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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.<CID>}
*/
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
}
Loading

0 comments on commit e39a7d9

Please sign in to comment.