diff --git a/.gitignore b/.gitignore index 254988d..9b8257c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,5 @@ build # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules -lib dist +docs \ No newline at end of file diff --git a/README.md b/README.md index f631ed2..e44d91e 100644 --- a/README.md +++ b/README.md @@ -79,40 +79,7 @@ const cid = new CID(base58Multihash) ## API -### Constructor - -- `new CID(, , )` -- `new CID()` -- `new CID()` -- `new CID()` -- `new CID()` - -### `.codec` - -### `.version` - -### `.multihash` - -### `.buffer` - -### `.prefix` - -### `.toV0()` - -### `.toV1()` - -### `.toBaseEncodedString(base)` - -Defaults to 'base58btc' - -### `.toJSON()` - -returns a buffer with CID version + multicodec + hashAlg + hashLen - -### `CID.isCID(other)` - -### `CID.codecs` - +See https://ipld.github.io/js-cid ## Contribute diff --git a/package.json b/package.json index 09f1f9e..dbb341f 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,12 @@ "test:browser": "aegir-test browser", "build": "aegir-build", "test": "aegir-test", - "release": "aegir-release", - "release-minor": "aegir-release --type minor", - "release-major": "aegir-release --type major", + "release": "aegir-release --docs", + "release-minor": "aegir-release --type minor --docs", + "release-major": "aegir-release --type major --docs", "coverage": "aegir-coverage", - "coverage-publish": "aegir-coverage publish" + "coverage-publish": "aegir-coverage publish", + "docs": "aegir-docs" }, "pre-commit": [ "lint", diff --git a/src/index.js b/src/index.js index de00a1c..6870e85 100644 --- a/src/index.js +++ b/src/index.js @@ -7,10 +7,25 @@ const codecs = require('multicodec/src/base-table') const codecVarints = require('multicodec/src/varint-table') const multihash = require('multihashes') -// CID: +/** + * @typedef {Object} SerializedCID + * @param {string} codec + * @param {number} version + * @param {Buffer} multihash + * + */ +/** + * Class representing a CID `` + * , as defined in [ipld/cid](https://github.com/ipld/cid). + * @class CID + */ class CID { - /* + /** + * Create a new CID. + * + * The algorithm for argument input is roughly: + * ``` * if (str) * if (1st char is on multibase table) -> CID String * else -> bs58 encoded multihash @@ -21,46 +36,74 @@ class CID { * -> construct CID by parts * * ..if only JS had traits.. + * ``` + * + * @param {string|Buffer} version + * @param {string} [codec] + * @param {Buffer} [multihash] + * + * @example + * + * new CID(, , ) + * new CID() + * new CID() + * new CID() + * new CID() + * */ constructor (version, codec, multihash) { if (typeof version === 'string') { if (multibase.isEncoded(version)) { // CID String (encoded with multibase) const cid = multibase.decode(version) - this.version = parseInt(cid.slice(0, 1).toString('hex'), 16) - this.codec = multicodec.getCodec(cid.slice(1)) - this.multihash = multicodec.rmPrefix(cid.slice(1)) + version = parseInt(cid.slice(0, 1).toString('hex'), 16) + codec = multicodec.getCodec(cid.slice(1)) + multihash = multicodec.rmPrefix(cid.slice(1)) } else { // bs58 string encoded multihash - this.codec = 'dag-pb' - this.multihash = mh.fromB58String(version) - this.version = 0 + codec = 'dag-pb' + multihash = mh.fromB58String(version) + version = 0 } } else if (Buffer.isBuffer(version)) { const firstByte = version.slice(0, 1) const v = parseInt(firstByte.toString('hex'), 16) if (v === 0 || v === 1) { // CID const cid = version - this.version = v - this.codec = multicodec.getCodec(cid.slice(1)) - this.multihash = multicodec.rmPrefix(cid.slice(1)) + version = v + codec = multicodec.getCodec(cid.slice(1)) + multihash = multicodec.rmPrefix(cid.slice(1)) } else { // multihash - this.codec = 'dag-pb' - this.multihash = version - this.version = 0 - } - } else if (typeof version === 'number') { - if (typeof codec !== 'string') { - throw new Error('codec must be string') + codec = 'dag-pb' + multihash = version + version = 0 } - if (!(version === 0 || version === 1)) { - throw new Error('version must be a number equal to 0 or 1') - } - mh.validate(multihash) - this.codec = codec - this.version = version - this.multihash = multihash } + + /** + * @type {string} + */ + this.codec = codec + + /** + * @type {number} + */ + this.version = version + + /** + * @type {Buffer} + */ + this.multihash = multihash + + CID.validateCID(this) } + /** + * The CID as a `Buffer` + * + * @return {Buffer} + * @readonly + * + * @memberOf CID + */ get buffer () { switch (this.version) { case 0: @@ -76,6 +119,12 @@ class CID { } } + /** + * Get the prefix of the CID. + * + * @returns {Buffer} + * @readonly + */ get prefix () { return Buffer.concat([ new Buffer(`0${this.version}`, 'hex'), @@ -84,6 +133,11 @@ class CID { ]) } + /** + * Convert to a CID of version `0`. + * + * @returns {CID} + */ toV0 () { if (this.codec !== 'dag-pb') { throw new Error('Cannot convert a non dag-pb CID to CIDv0') @@ -92,11 +146,21 @@ class CID { return new CID(0, this.codec, this.multihash) } + /** + * Convert to a CID of version `1`. + * + * @returns {CID} + */ toV1 () { return new CID(1, this.codec, this.multihash) } - /* defaults to base58btc */ + /** + * Encode the CID into a string. + * + * @param {string} [base='base58btc'] - Base encoding to use. + * @returns {string} + */ toBaseEncodedString (base) { base = base || 'base58btc' @@ -114,6 +178,11 @@ class CID { } } + /** + * Serialize to a plain object. + * + * @returns {SerializedCID} + */ toJSON () { return { codec: this.codec, @@ -122,16 +191,62 @@ class CID { } } + /** + * Compare equality with another CID. + * + * @param {CID} other + * @returns {bool} + */ equals (other) { return this.codec === other.codec && this.version === other.version && this.multihash.equals(other.multihash) } + + /** + * Test if the given input is a CID. + * + * @param {any} other + * @returns {bool} + */ + static isCID (other) { + try { + CID.validateCID(other) + } catch (err) { + return false + } + + return true + } + + /** + * Test if the given input is a valid CID object. + * Throws if it is not. + * + * @param {any} other + * @returns {void} + */ + static validateCID (other) { + if (other == null) { + throw new Error('null values are not valid CIDs') + } + + if (!(other.version === 0 || other.version === 1)) { + throw new Error('Invalid version, must be a number equal to 1 or 0') + } + + if (typeof other.codec !== 'string') { + throw new Error('codec must be string') + } + + if (!Buffer.isBuffer(other.multihash)) { + throw new Error('multihash must be a Buffer') + } + + mh.validate(other.multihash) + } } CID.codecs = codecs -CID.isCID = (other) => { - return other.constructor.name === 'CID' -} module.exports = CID diff --git a/test/index.spec.js b/test/index.spec.js index f14a09d..3f5c40d 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,4 +1,5 @@ /* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ 'use strict' const chai = require('chai') @@ -11,6 +12,18 @@ const multihashing = require('multihashing-async') const CID = require('../src') describe('CID', () => { + let hash + + before((done) => { + multihashing(new Buffer('abc'), 'sha2-256', (err, d) => { + if (err) { + return done(err) + } + hash = d + done() + }) + }) + describe('v0', () => { it('handles B58Str multihash', () => { const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' @@ -40,7 +53,7 @@ describe('CID', () => { }) it('create by parts', () => { - const cid = new CID(0, 'dag-pb', multihash.encode(new Buffer('abc'), 'sha2-256')) + const cid = new CID(0, 'dag-pb', hash) expect(cid).to.have.property('codec', 'dag-pb') expect(cid).to.have.property('version', 0) @@ -60,8 +73,8 @@ describe('CID', () => { }) it('.prefix', () => { - const cid = new CID(0, 'dag-pb', multihash.encode(new Buffer('abc'), 'sha2-256')) - expect(cid.prefix.toString('hex')).to.equal('00701203') + const cid = new CID(0, 'dag-pb', hash) + expect(cid.prefix.toString('hex')).to.equal('00701220') }) }) @@ -92,7 +105,7 @@ describe('CID', () => { }) it('create by parts', () => { - const cid = new CID(1, 'dag-cbor', multihash.encode(new Buffer('xyz'), 'sha2-256')) + const cid = new CID(1, 'dag-cbor', hash) expect(cid).to.have.property('codec', 'dag-cbor') expect(cid).to.have.property('version', 1) @@ -100,7 +113,7 @@ describe('CID', () => { }) it('can roundtrip through cid.toBaseEncodedString()', () => { - const cid1 = new CID(1, 'dag-cbor', multihash.encode(new Buffer('xyz'), 'sha2-256')) + const cid1 = new CID(1, 'dag-cbor', hash) const cid2 = new CID(cid1.toBaseEncodedString()) expect(cid1).to.have.property('codec').that.eql(cid2.codec) @@ -123,8 +136,8 @@ describe('CID', () => { }) it('.prefix', () => { - const cid = new CID(1, 'dag-cbor', multihash.encode(new Buffer('xyz'), 'sha2-256')) - expect(cid.prefix.toString('hex')).to.equal('01711203') + const cid = new CID(1, 'dag-cbor', hash) + expect(cid.prefix.toString('hex')).to.equal('01711220') }) }) @@ -168,4 +181,25 @@ describe('CID', () => { ).to.be.eql(false) }) }) + + describe('throws on invalid inputs', () => { + const invalid = [ + 'hello world', + 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', + new Buffer('hello world'), + new Buffer('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') + ] + + invalid.forEach((i) => it(`new CID(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => new CID(i)).to.throw() + })) + + invalid.forEach((i) => it(`new CID(0, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => new CID(0, 'dag-pb', i)).to.throw() + })) + + invalid.forEach((i) => it(`new CID(1, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => new CID(1, 'dag-pb', i)).to.throw() + })) + }) })