From b2b5d2f7d107a295ea9aef13f07483ce23131039 Mon Sep 17 00:00:00 2001 From: Harry Y Date: Sun, 4 Mar 2018 18:17:10 -0800 Subject: [PATCH 1/7] fix: #39 - Improve constructor performance Updated the isCID() implementation to use a (private) intermediate method to validate CID without relying on the error being thrown during validation. The validateCID() method also uses this intermediate method, and its behavior has been preserved (e.g., with regards to throwing an error for invalid CIDs). --- package.json | 4 +- src/index.js | 29 +++- test/profiling/alt-impl/cid1.js | 260 ++++++++++++++++++++++++++++++ test/profiling/alt-impl/cid2.js | 275 ++++++++++++++++++++++++++++++++ test/profiling/cidperf-a.js | 67 ++++++++ 5 files changed, 627 insertions(+), 8 deletions(-) create mode 100644 test/profiling/alt-impl/cid1.js create mode 100644 test/profiling/alt-impl/cid2.js create mode 100644 test/profiling/cidperf-a.js diff --git a/package.json b/package.json index 3981648..cee200b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cids", - "version": "0.5.2", + "version": "0.5.3", "description": "CID Implementation in JavaScript", "main": "src/index.js", "scripts": { @@ -43,6 +43,8 @@ "aegir": "^12.0.8", "chai": "^4.1.2", "dirty-chai": "^2.0.1", + "mocha": "^5.0.1", + "multihashing": "^0.3.2", "multihashing-async": "~0.4.6", "pre-commit": "^1.2.2" }, diff --git a/src/index.js b/src/index.js index 7bcfb9f..ea906e6 100644 --- a/src/index.js +++ b/src/index.js @@ -219,12 +219,11 @@ class CID { */ static isCID (other) { try { - CID.validateCID(other) + let errorMsg = CID._checkCIDComponents(other) + return !(errorMsg) } catch (err) { return false } - - return true } /** @@ -235,23 +234,39 @@ class CID { * @returns {void} */ static validateCID (other) { + let errorMsg = CID._checkCIDComponents(other) + if (errorMsg) { + throw new Error(errorMsg) + } + } + + /** + * Test if the given input is a valid CID object. + * Returns an error message if it is not, or + * Throws an error its multihash is invalid. + * + * @param {any} other + * @returns {string} + */ + static _checkCIDComponents (other) { if (other == null) { - throw new Error('null values are not valid CIDs') + return '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') + return 'Invalid version, must be a number equal to 1 or 0' } if (typeof other.codec !== 'string') { - throw new Error('codec must be string') + return 'codec must be string' } if (!Buffer.isBuffer(other.multihash)) { - throw new Error('multihash must be a Buffer') + return 'multihash must be a Buffer' } mh.validate(other.multihash) + return null } } diff --git a/test/profiling/alt-impl/cid1.js b/test/profiling/alt-impl/cid1.js new file mode 100644 index 0000000..a97fbaf --- /dev/null +++ b/test/profiling/alt-impl/cid1.js @@ -0,0 +1,260 @@ +'use strict' + +const mh = require('multihashes') +const multibase = require('multibase') +const multicodec = require('multicodec') +const codecs = require('multicodec/src/base-table') +const codecVarints = require('multicodec/src/varint-table') +const multihash = require('multihashes') + +/** + * @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 CID1 { + /** + * 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 + * else if (Buffer) + * if (0 or 1) -> CID + * else -> multihash + * else if (Number) + * -> 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() + * new CID() + * + */ + constructor (version, codec, multihash) { + if (CID1.isCID(version)) { + let cid = version + this.version = cid.version + this.codec = cid.codec + this.multihash = Buffer.from(cid.multihash) + return + } + if (typeof version === 'string') { + if (multibase.isEncoded(version)) { // CID String (encoded with multibase) + const cid = multibase.decode(version) + 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 + 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 + version = v + codec = multicodec.getCodec(cid.slice(1)) + multihash = multicodec.rmPrefix(cid.slice(1)) + } else { // multihash + codec = 'dag-pb' + multihash = version + version = 0 + } + } + + /** + * @type {string} + */ + this.codec = codec + + /** + * @type {number} + */ + this.version = version + + /** + * @type {Buffer} + */ + this.multihash = multihash + + CID1.validateCID(this) + } + + /** + * The CID as a `Buffer` + * + * @return {Buffer} + * @readonly + * + * @memberOf CID + */ + get buffer () { + switch (this.version) { + case 0: + return this.multihash + case 1: + return Buffer.concat([ + Buffer.from('01', 'hex'), + Buffer.from(codecVarints[this.codec]), + this.multihash + ]) + default: + throw new Error('unsupported version') + } + } + + /** + * Get the prefix of the CID. + * + * @returns {Buffer} + * @readonly + */ + get prefix () { + return Buffer.concat([ + Buffer.from(`0${this.version}`, 'hex'), + codecVarints[this.codec], + multihash.prefix(this.multihash) + ]) + } + + /** + * Convert to a CID of version `0`. + * + * @returns {CID1} + */ + toV0 () { + if (this.codec !== 'dag-pb') { + throw new Error('Cannot convert a non dag-pb CID to CIDv0') + } + + return new CID1(0, this.codec, this.multihash) + } + + /** + * Convert to a CID of version `1`. + * + * @returns {CID1} + */ + toV1 () { + return new CID1(1, this.codec, this.multihash) + } + + /** + * Encode the CID into a string. + * + * @param {string} [base='base58btc'] - Base encoding to use. + * @returns {string} + */ + toBaseEncodedString (base) { + base = base || 'base58btc' + + switch (this.version) { + case 0: { + if (base !== 'base58btc') { + throw new Error('not supported with CIDv0, to support different bases, please migrate the instance do CIDv1, you can do that through cid.toV1()') + } + return mh.toB58String(this.multihash) + } + case 1: + return multibase.encode(base, this.buffer).toString() + default: + throw new Error('Unsupported version') + } + } + + /** + * Serialize to a plain object. + * + * @returns {SerializedCID} + */ + toJSON () { + return { + codec: this.codec, + version: this.version, + hash: this.multihash + } + } + + /** + * Compare equality with another CID. + * + * @param {CID1} 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 { + CID1.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) + } +} + +CID1.codecs = codecs + +module.exports = CID1 diff --git a/test/profiling/alt-impl/cid2.js b/test/profiling/alt-impl/cid2.js new file mode 100644 index 0000000..0d314e8 --- /dev/null +++ b/test/profiling/alt-impl/cid2.js @@ -0,0 +1,275 @@ +'use strict' + +const mh = require('multihashes') +const multibase = require('multibase') +const multicodec = require('multicodec') +const codecs = require('multicodec/src/base-table') +const codecVarints = require('multicodec/src/varint-table') +const multihash = require('multihashes') + +/** + * @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 CID2 { + /** + * 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 + * else if (Buffer) + * if (0 or 1) -> CID + * else -> multihash + * else if (Number) + * -> 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() + * new CID() + * + */ + constructor (version, codec, multihash) { + if (CID2.isCID(version)) { + let cid = version + this.version = cid.version + this.codec = cid.codec + this.multihash = Buffer.from(cid.multihash) + return + } + if (typeof version === 'string') { + if (multibase.isEncoded(version)) { // CID String (encoded with multibase) + const cid = multibase.decode(version) + 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 + 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 + version = v + codec = multicodec.getCodec(cid.slice(1)) + multihash = multicodec.rmPrefix(cid.slice(1)) + } else { // multihash + codec = 'dag-pb' + multihash = version + version = 0 + } + } + + /** + * @type {string} + */ + this.codec = codec + + /** + * @type {number} + */ + this.version = version + + /** + * @type {Buffer} + */ + this.multihash = multihash + + CID2.validateCID(this) + } + + /** + * The CID as a `Buffer` + * + * @return {Buffer} + * @readonly + * + * @memberOf CID + */ + get buffer () { + switch (this.version) { + case 0: + return this.multihash + case 1: + return Buffer.concat([ + Buffer.from('01', 'hex'), + Buffer.from(codecVarints[this.codec]), + this.multihash + ]) + default: + throw new Error('unsupported version') + } + } + + /** + * Get the prefix of the CID. + * + * @returns {Buffer} + * @readonly + */ + get prefix () { + return Buffer.concat([ + Buffer.from(`0${this.version}`, 'hex'), + codecVarints[this.codec], + multihash.prefix(this.multihash) + ]) + } + + /** + * Convert to a CID of version `0`. + * + * @returns {CID2} + */ + toV0 () { + if (this.codec !== 'dag-pb') { + throw new Error('Cannot convert a non dag-pb CID to CIDv0') + } + + return new CID2(0, this.codec, this.multihash) + } + + /** + * Convert to a CID of version `1`. + * + * @returns {CID2} + */ + toV1 () { + return new CID2(1, this.codec, this.multihash) + } + + /** + * Encode the CID into a string. + * + * @param {string} [base='base58btc'] - Base encoding to use. + * @returns {string} + */ + toBaseEncodedString (base) { + base = base || 'base58btc' + + switch (this.version) { + case 0: { + if (base !== 'base58btc') { + throw new Error('not supported with CIDv0, to support different bases, please migrate the instance do CIDv1, you can do that through cid.toV1()') + } + return mh.toB58String(this.multihash) + } + case 1: + return multibase.encode(base, this.buffer).toString() + default: + throw new Error('Unsupported version') + } + } + + /** + * Serialize to a plain object. + * + * @returns {SerializedCID} + */ + toJSON () { + return { + codec: this.codec, + version: this.version, + hash: this.multihash + } + } + + /** + * Compare equality with another CID. + * + * @param {CID2} 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 { + let errorMsg = CID2._checkCIDComponents(other) + return !(errorMsg) + } catch (err) { + return false + } + } + + /** + * Test if the given input is a valid CID object. + * Throws if it is not. + * + * @param {any} other + * @returns {void} + */ + static validateCID (other) { + let errorMsg = CID2._checkCIDComponents(other) + if (errorMsg) { + throw new Error(errorMsg) + } + } + + /** + * Test if the given input is a valid CID object. + * Returns an error message if it is not, or + * Throws an error its multihash is invalid. + * + * @param {any} other + * @returns {string} + */ + static _checkCIDComponents (other) { + if (other == null) { + return 'null values are not valid CIDs' + } + + if (!(other.version === 0 || other.version === 1)) { + return 'Invalid version, must be a number equal to 1 or 0' + } + + if (typeof other.codec !== 'string') { + return 'codec must be string' + } + + if (!Buffer.isBuffer(other.multihash)) { + return 'multihash must be a Buffer' + } + + mh.validate(other.multihash) + return null + } +} + +CID2.codecs = codecs + +module.exports = CID2 diff --git a/test/profiling/cidperf-a.js b/test/profiling/cidperf-a.js new file mode 100644 index 0000000..01c00b9 --- /dev/null +++ b/test/profiling/cidperf-a.js @@ -0,0 +1,67 @@ +'use strict' + +const codecs = require('multicodec/src/base-table') +const multihashing = require('multihashing') +// const CID1 = require('../../src') +const CID1 = require('./alt-impl/cid1') // Original/existing implementation. +const CID2 = require('./alt-impl/cid2') // New/proposed implementation. + +// Used to delay the testing for a few seconds. +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// Simple test class. +// The purpose of this class is +// to simply test the CID ctor (and, primarily, CID.isCID() method). +class CIDPerfA { + constructor () { + this.version = 1 + this.codec = codecs.base8.toString() + // console.log(codecs.base10) + this.mh = multihashing(Buffer.from('oh, hey!'), 'sha2-256') + } + + // i: Running-counter. + // print: If true, it'll print/dump the CID data. + run1 (i, print) { + const cid = new CID1(this.version, this.codec, this.mh) + if (print === true) { + console.log('i=' + i, cid) + } + // const cidStr = cid.toString('hex') + // console.log('cidStr (hex) = ' + cidStr) + } + + run2 (i, print) { + const cid = new CID2(this.version, this.codec, this.mh) + if (print === true) { + console.log('i=' + i, cid) + } + } +} + +// ///////////////////////// +// Main test routine. +// ///////////////////////// + +const reps = 10000 +const cidPerf = new CIDPerfA() + +// We just give ~1 second for the JS engine to start and 'rest', etc. +// before starting new tests. +sleep(1000).then(() => { + // [1] For original impl. + console.time('run1'); [...Array(reps).keys()].map(i => { + cidPerf.run1(i) + }) + console.timeEnd('run1') + + sleep(1000).then(() => { + // [2] For alternative impl. + console.time('run2'); [...Array(reps).keys()].map(i => { + cidPerf.run2(i, false) + }) + console.timeEnd('run2') + }) +}) From 7eaaa0259dcf10d60dfacd1eef8bdbd5b9cc70df Mon Sep 17 00:00:00 2001 From: Harry Y Date: Sun, 4 Mar 2018 18:42:05 -0800 Subject: [PATCH 2/7] Added dev dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cee200b..76debfd 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "aegir": "^12.0.8", "chai": "^4.1.2", "dirty-chai": "^2.0.1", + "karma-mocha": "^1.3.0", "mocha": "^5.0.1", "multihashing": "^0.3.2", "multihashing-async": "~0.4.6", From fd656935ca74f48eaa635c49dc24c3390e2a65d3 Mon Sep 17 00:00:00 2001 From: Harry Y Date: Sun, 4 Mar 2018 19:00:28 -0800 Subject: [PATCH 3/7] Updated dev dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 76debfd..4ed5e98 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "chai": "^4.1.2", "dirty-chai": "^2.0.1", "karma-mocha": "^1.3.0", + "karma-mocha-webworker": "^1.3.0", "mocha": "^5.0.1", "multihashing": "^0.3.2", "multihashing-async": "~0.4.6", From 4d423a76a1adfa10088c044482a37051779d97a6 Mon Sep 17 00:00:00 2001 From: Harry Y Date: Mon, 5 Mar 2018 10:08:51 -0800 Subject: [PATCH 4/7] Updated based on the feedback --- package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/package.json b/package.json index 4ed5e98..8632747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cids", - "version": "0.5.3", + "version": "0.5.2", "description": "CID Implementation in JavaScript", "main": "src/index.js", "scripts": { @@ -43,11 +43,7 @@ "aegir": "^12.0.8", "chai": "^4.1.2", "dirty-chai": "^2.0.1", - "karma-mocha": "^1.3.0", - "karma-mocha-webworker": "^1.3.0", - "mocha": "^5.0.1", "multihashing": "^0.3.2", - "multihashing-async": "~0.4.6", "pre-commit": "^1.2.2" }, "engines": { From bdf34040a2a173a2dbe4a5c2aa312f0974169e5c Mon Sep 17 00:00:00 2001 From: Harry Y Date: Mon, 5 Mar 2018 11:26:25 -0800 Subject: [PATCH 5/7] Fixed checkin error --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8632747..0c541bd 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "chai": "^4.1.2", "dirty-chai": "^2.0.1", "multihashing": "^0.3.2", + "multihashing-async": "^0.4.8", "pre-commit": "^1.2.2" }, "engines": { From c2cb1c72952482919a73b79baa5538a129fd1043 Mon Sep 17 00:00:00 2001 From: Harry Y Date: Tue, 6 Mar 2018 11:27:10 -0800 Subject: [PATCH 6/7] Updated per review/feedback (1) Put checkCIDComponents() function in a separate file. (2) Added a test spec for the new module. (3) Simplify the perf test runner. (Now it tests one CID implementation at a time.) --- src/cid-util.js | 36 +++++ src/index.js | 34 +--- test/cid-util.spec.js | 41 +++++ test/profiling/alt-impl/cid1.js | 260 ------------------------------ test/profiling/alt-impl/cid2.js | 275 -------------------------------- test/profiling/cidperf-a.js | 67 -------- test/profiling/cidperf-x.js | 51 ++++++ 7 files changed, 131 insertions(+), 633 deletions(-) create mode 100644 src/cid-util.js create mode 100644 test/cid-util.spec.js delete mode 100644 test/profiling/alt-impl/cid1.js delete mode 100644 test/profiling/alt-impl/cid2.js delete mode 100644 test/profiling/cidperf-a.js create mode 100644 test/profiling/cidperf-x.js diff --git a/src/cid-util.js b/src/cid-util.js new file mode 100644 index 0000000..ced928f --- /dev/null +++ b/src/cid-util.js @@ -0,0 +1,36 @@ +'use strict' + +const mh = require('multihashes') + +var CIDUtil = { + /** + * Test if the given input is a valid CID object. + * Returns an error message if it is not, or + * Throws an error its multihash is invalid. + * + * @param {any} other + * @returns {string} + */ + checkCIDComponents: function (other) { + if (other == null) { + return 'null values are not valid CIDs' + } + + if (!(other.version === 0 || other.version === 1)) { + return 'Invalid version, must be a number equal to 1 or 0' + } + + if (typeof other.codec !== 'string') { + return 'codec must be string' + } + + if (!Buffer.isBuffer(other.multihash)) { + throw new Error('multihash must be a Buffer') + } + + mh.validate(other.multihash) + return null + } +} + +module.exports = CIDUtil diff --git a/src/index.js b/src/index.js index ea906e6..ea67340 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ const multicodec = require('multicodec') const codecs = require('multicodec/src/base-table') const codecVarints = require('multicodec/src/varint-table') const multihash = require('multihashes') +const CIDUtil = require('./cid-util') /** * @typedef {Object} SerializedCID @@ -219,7 +220,7 @@ class CID { */ static isCID (other) { try { - let errorMsg = CID._checkCIDComponents(other) + let errorMsg = CIDUtil.checkCIDComponents(other) return !(errorMsg) } catch (err) { return false @@ -234,40 +235,11 @@ class CID { * @returns {void} */ static validateCID (other) { - let errorMsg = CID._checkCIDComponents(other) + let errorMsg = CIDUtil.checkCIDComponents(other) if (errorMsg) { throw new Error(errorMsg) } } - - /** - * Test if the given input is a valid CID object. - * Returns an error message if it is not, or - * Throws an error its multihash is invalid. - * - * @param {any} other - * @returns {string} - */ - static _checkCIDComponents (other) { - if (other == null) { - return 'null values are not valid CIDs' - } - - if (!(other.version === 0 || other.version === 1)) { - return 'Invalid version, must be a number equal to 1 or 0' - } - - if (typeof other.codec !== 'string') { - return 'codec must be string' - } - - if (!Buffer.isBuffer(other.multihash)) { - return 'multihash must be a Buffer' - } - - mh.validate(other.multihash) - return null - } } CID.codecs = codecs diff --git a/test/cid-util.spec.js b/test/cid-util.spec.js new file mode 100644 index 0000000..cf5889b --- /dev/null +++ b/test/cid-util.spec.js @@ -0,0 +1,41 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) +const CIDUtil = require('../src/cid-util') + +describe('CIDUtil', () => { + describe('returns error on invalid inputs', () => { + const invalid = [ + 'hello world', + 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', + Buffer.from('hello world'), + Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') + ] + + invalid.forEach((i) => it(`new CID(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + let errMsg = CIDUtil.checkCIDComponents(i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + + invalid.forEach((i) => it(`new CID(0, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + let errMsg = CIDUtil.checkCIDComponents(0, 'dag-pb', i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + + invalid.forEach((i) => it(`new CID(1, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + let errMsg = CIDUtil.checkCIDComponents(1, 'dag-pb', i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + }) +}) diff --git a/test/profiling/alt-impl/cid1.js b/test/profiling/alt-impl/cid1.js deleted file mode 100644 index a97fbaf..0000000 --- a/test/profiling/alt-impl/cid1.js +++ /dev/null @@ -1,260 +0,0 @@ -'use strict' - -const mh = require('multihashes') -const multibase = require('multibase') -const multicodec = require('multicodec') -const codecs = require('multicodec/src/base-table') -const codecVarints = require('multicodec/src/varint-table') -const multihash = require('multihashes') - -/** - * @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 CID1 { - /** - * 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 - * else if (Buffer) - * if (0 or 1) -> CID - * else -> multihash - * else if (Number) - * -> 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() - * new CID() - * - */ - constructor (version, codec, multihash) { - if (CID1.isCID(version)) { - let cid = version - this.version = cid.version - this.codec = cid.codec - this.multihash = Buffer.from(cid.multihash) - return - } - if (typeof version === 'string') { - if (multibase.isEncoded(version)) { // CID String (encoded with multibase) - const cid = multibase.decode(version) - 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 - 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 - version = v - codec = multicodec.getCodec(cid.slice(1)) - multihash = multicodec.rmPrefix(cid.slice(1)) - } else { // multihash - codec = 'dag-pb' - multihash = version - version = 0 - } - } - - /** - * @type {string} - */ - this.codec = codec - - /** - * @type {number} - */ - this.version = version - - /** - * @type {Buffer} - */ - this.multihash = multihash - - CID1.validateCID(this) - } - - /** - * The CID as a `Buffer` - * - * @return {Buffer} - * @readonly - * - * @memberOf CID - */ - get buffer () { - switch (this.version) { - case 0: - return this.multihash - case 1: - return Buffer.concat([ - Buffer.from('01', 'hex'), - Buffer.from(codecVarints[this.codec]), - this.multihash - ]) - default: - throw new Error('unsupported version') - } - } - - /** - * Get the prefix of the CID. - * - * @returns {Buffer} - * @readonly - */ - get prefix () { - return Buffer.concat([ - Buffer.from(`0${this.version}`, 'hex'), - codecVarints[this.codec], - multihash.prefix(this.multihash) - ]) - } - - /** - * Convert to a CID of version `0`. - * - * @returns {CID1} - */ - toV0 () { - if (this.codec !== 'dag-pb') { - throw new Error('Cannot convert a non dag-pb CID to CIDv0') - } - - return new CID1(0, this.codec, this.multihash) - } - - /** - * Convert to a CID of version `1`. - * - * @returns {CID1} - */ - toV1 () { - return new CID1(1, this.codec, this.multihash) - } - - /** - * Encode the CID into a string. - * - * @param {string} [base='base58btc'] - Base encoding to use. - * @returns {string} - */ - toBaseEncodedString (base) { - base = base || 'base58btc' - - switch (this.version) { - case 0: { - if (base !== 'base58btc') { - throw new Error('not supported with CIDv0, to support different bases, please migrate the instance do CIDv1, you can do that through cid.toV1()') - } - return mh.toB58String(this.multihash) - } - case 1: - return multibase.encode(base, this.buffer).toString() - default: - throw new Error('Unsupported version') - } - } - - /** - * Serialize to a plain object. - * - * @returns {SerializedCID} - */ - toJSON () { - return { - codec: this.codec, - version: this.version, - hash: this.multihash - } - } - - /** - * Compare equality with another CID. - * - * @param {CID1} 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 { - CID1.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) - } -} - -CID1.codecs = codecs - -module.exports = CID1 diff --git a/test/profiling/alt-impl/cid2.js b/test/profiling/alt-impl/cid2.js deleted file mode 100644 index 0d314e8..0000000 --- a/test/profiling/alt-impl/cid2.js +++ /dev/null @@ -1,275 +0,0 @@ -'use strict' - -const mh = require('multihashes') -const multibase = require('multibase') -const multicodec = require('multicodec') -const codecs = require('multicodec/src/base-table') -const codecVarints = require('multicodec/src/varint-table') -const multihash = require('multihashes') - -/** - * @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 CID2 { - /** - * 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 - * else if (Buffer) - * if (0 or 1) -> CID - * else -> multihash - * else if (Number) - * -> 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() - * new CID() - * - */ - constructor (version, codec, multihash) { - if (CID2.isCID(version)) { - let cid = version - this.version = cid.version - this.codec = cid.codec - this.multihash = Buffer.from(cid.multihash) - return - } - if (typeof version === 'string') { - if (multibase.isEncoded(version)) { // CID String (encoded with multibase) - const cid = multibase.decode(version) - 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 - 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 - version = v - codec = multicodec.getCodec(cid.slice(1)) - multihash = multicodec.rmPrefix(cid.slice(1)) - } else { // multihash - codec = 'dag-pb' - multihash = version - version = 0 - } - } - - /** - * @type {string} - */ - this.codec = codec - - /** - * @type {number} - */ - this.version = version - - /** - * @type {Buffer} - */ - this.multihash = multihash - - CID2.validateCID(this) - } - - /** - * The CID as a `Buffer` - * - * @return {Buffer} - * @readonly - * - * @memberOf CID - */ - get buffer () { - switch (this.version) { - case 0: - return this.multihash - case 1: - return Buffer.concat([ - Buffer.from('01', 'hex'), - Buffer.from(codecVarints[this.codec]), - this.multihash - ]) - default: - throw new Error('unsupported version') - } - } - - /** - * Get the prefix of the CID. - * - * @returns {Buffer} - * @readonly - */ - get prefix () { - return Buffer.concat([ - Buffer.from(`0${this.version}`, 'hex'), - codecVarints[this.codec], - multihash.prefix(this.multihash) - ]) - } - - /** - * Convert to a CID of version `0`. - * - * @returns {CID2} - */ - toV0 () { - if (this.codec !== 'dag-pb') { - throw new Error('Cannot convert a non dag-pb CID to CIDv0') - } - - return new CID2(0, this.codec, this.multihash) - } - - /** - * Convert to a CID of version `1`. - * - * @returns {CID2} - */ - toV1 () { - return new CID2(1, this.codec, this.multihash) - } - - /** - * Encode the CID into a string. - * - * @param {string} [base='base58btc'] - Base encoding to use. - * @returns {string} - */ - toBaseEncodedString (base) { - base = base || 'base58btc' - - switch (this.version) { - case 0: { - if (base !== 'base58btc') { - throw new Error('not supported with CIDv0, to support different bases, please migrate the instance do CIDv1, you can do that through cid.toV1()') - } - return mh.toB58String(this.multihash) - } - case 1: - return multibase.encode(base, this.buffer).toString() - default: - throw new Error('Unsupported version') - } - } - - /** - * Serialize to a plain object. - * - * @returns {SerializedCID} - */ - toJSON () { - return { - codec: this.codec, - version: this.version, - hash: this.multihash - } - } - - /** - * Compare equality with another CID. - * - * @param {CID2} 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 { - let errorMsg = CID2._checkCIDComponents(other) - return !(errorMsg) - } catch (err) { - return false - } - } - - /** - * Test if the given input is a valid CID object. - * Throws if it is not. - * - * @param {any} other - * @returns {void} - */ - static validateCID (other) { - let errorMsg = CID2._checkCIDComponents(other) - if (errorMsg) { - throw new Error(errorMsg) - } - } - - /** - * Test if the given input is a valid CID object. - * Returns an error message if it is not, or - * Throws an error its multihash is invalid. - * - * @param {any} other - * @returns {string} - */ - static _checkCIDComponents (other) { - if (other == null) { - return 'null values are not valid CIDs' - } - - if (!(other.version === 0 || other.version === 1)) { - return 'Invalid version, must be a number equal to 1 or 0' - } - - if (typeof other.codec !== 'string') { - return 'codec must be string' - } - - if (!Buffer.isBuffer(other.multihash)) { - return 'multihash must be a Buffer' - } - - mh.validate(other.multihash) - return null - } -} - -CID2.codecs = codecs - -module.exports = CID2 diff --git a/test/profiling/cidperf-a.js b/test/profiling/cidperf-a.js deleted file mode 100644 index 01c00b9..0000000 --- a/test/profiling/cidperf-a.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' - -const codecs = require('multicodec/src/base-table') -const multihashing = require('multihashing') -// const CID1 = require('../../src') -const CID1 = require('./alt-impl/cid1') // Original/existing implementation. -const CID2 = require('./alt-impl/cid2') // New/proposed implementation. - -// Used to delay the testing for a few seconds. -function sleep (ms) { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -// Simple test class. -// The purpose of this class is -// to simply test the CID ctor (and, primarily, CID.isCID() method). -class CIDPerfA { - constructor () { - this.version = 1 - this.codec = codecs.base8.toString() - // console.log(codecs.base10) - this.mh = multihashing(Buffer.from('oh, hey!'), 'sha2-256') - } - - // i: Running-counter. - // print: If true, it'll print/dump the CID data. - run1 (i, print) { - const cid = new CID1(this.version, this.codec, this.mh) - if (print === true) { - console.log('i=' + i, cid) - } - // const cidStr = cid.toString('hex') - // console.log('cidStr (hex) = ' + cidStr) - } - - run2 (i, print) { - const cid = new CID2(this.version, this.codec, this.mh) - if (print === true) { - console.log('i=' + i, cid) - } - } -} - -// ///////////////////////// -// Main test routine. -// ///////////////////////// - -const reps = 10000 -const cidPerf = new CIDPerfA() - -// We just give ~1 second for the JS engine to start and 'rest', etc. -// before starting new tests. -sleep(1000).then(() => { - // [1] For original impl. - console.time('run1'); [...Array(reps).keys()].map(i => { - cidPerf.run1(i) - }) - console.timeEnd('run1') - - sleep(1000).then(() => { - // [2] For alternative impl. - console.time('run2'); [...Array(reps).keys()].map(i => { - cidPerf.run2(i, false) - }) - console.timeEnd('run2') - }) -}) diff --git a/test/profiling/cidperf-x.js b/test/profiling/cidperf-x.js new file mode 100644 index 0000000..ddcada7 --- /dev/null +++ b/test/profiling/cidperf-x.js @@ -0,0 +1,51 @@ +'use strict' + +const multihashing = require('multihashing') +// [1] Original/existing implementation. +// const CID = require('cids') +// [2] New/proposed implementation. +const CID = require('../../src') + +// Used to delay the testing for a few seconds. +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// Simple test class. +// The purpose of this class is +// to simply test the CID ctor (and, primarily, CID.isCID() method). +class CIDPerfX { + constructor () { + this.version = 1 + this.codec = 'dag-pb' + this.mh = multihashing(Buffer.from('oh, hey!'), 'sha2-256') + } + + // i: Running-counter. + // print: If true, it'll print/dump the CID data. + run (i, print) { + const cid = new CID(this.version, this.codec, this.mh) + if (print === true) { + console.log('i=' + i, cid) + } + } +} + +// ///////////////////////// +// Main test routine. +// Note: You will need to run the test separately +// for each "alternative" impls and compare their results. +// ///////////////////////// + +const reps = 10000 +const cidPerf = new CIDPerfX() + +console.log(`Starting a test: Will run "new CID()" ${reps} times.`) +// We just give ~1 second for the JS engine to start and 'rest', etc. +// before starting new tests. +sleep(1000).then(() => { + console.time('run'); [...Array(reps).keys()].map(i => { + cidPerf.run(i) + }) + console.timeEnd('run') +}) From 0f11f0c1bd5c7a9c6b6f59b53fb3f447a8439149 Mon Sep 17 00:00:00 2001 From: Harry Y Date: Wed, 7 Mar 2018 00:00:25 -0800 Subject: [PATCH 7/7] checkCIDComponents() method now does not throw error. --- src/cid-util.js | 17 ++++--- src/index.js | 7 +-- test/cid-util.spec.js | 90 +++++++++++++++++++++++++------------ test/profiling/cidperf-x.js | 3 +- 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/cid-util.js b/src/cid-util.js index ced928f..4f5c2db 100644 --- a/src/cid-util.js +++ b/src/cid-util.js @@ -5,8 +5,8 @@ const mh = require('multihashes') var CIDUtil = { /** * Test if the given input is a valid CID object. - * Returns an error message if it is not, or - * Throws an error its multihash is invalid. + * Returns an error message if it is not. + * Returns undefined if it is a valid CID. * * @param {any} other * @returns {string} @@ -25,11 +25,18 @@ var CIDUtil = { } if (!Buffer.isBuffer(other.multihash)) { - throw new Error('multihash must be a Buffer') + return 'multihash must be a Buffer' } - mh.validate(other.multihash) - return null + try { + mh.validate(other.multihash) + } catch (err) { + let errorMsg = err.message + if (!errorMsg) { // Just in case mh.validate() throws an error with empty error message + errorMsg = 'Multihash validation failed' + } + return errorMsg + } } } diff --git a/src/index.js b/src/index.js index ea67340..dc34d78 100644 --- a/src/index.js +++ b/src/index.js @@ -219,12 +219,7 @@ class CID { * @returns {bool} */ static isCID (other) { - try { - let errorMsg = CIDUtil.checkCIDComponents(other) - return !(errorMsg) - } catch (err) { - return false - } + return !(CIDUtil.checkCIDComponents(other)) } /** diff --git a/test/cid-util.spec.js b/test/cid-util.spec.js index cf5889b..df0b191 100644 --- a/test/cid-util.spec.js +++ b/test/cid-util.spec.js @@ -6,36 +6,70 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +const multihashing = require('multihashing-async') +const CID = require('../src') const CIDUtil = require('../src/cid-util') describe('CIDUtil', () => { - describe('returns error on invalid inputs', () => { - const invalid = [ - 'hello world', - 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', - Buffer.from('hello world'), - Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') - ] - - invalid.forEach((i) => it(`new CID(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { - expect(() => { - let errMsg = CIDUtil.checkCIDComponents(i) - expect(errMsg).to.exist() - }).to.not.throw() - })) - - invalid.forEach((i) => it(`new CID(0, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { - expect(() => { - let errMsg = CIDUtil.checkCIDComponents(0, 'dag-pb', i) - expect(errMsg).to.exist() - }).to.not.throw() - })) - - invalid.forEach((i) => it(`new CID(1, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { - expect(() => { - let errMsg = CIDUtil.checkCIDComponents(1, 'dag-pb', i) - expect(errMsg).to.exist() - }).to.not.throw() - })) + let hash + + before((done) => { + multihashing(Buffer.from('abc'), 'sha2-256', (err, d) => { + if (err) { + return done(err) + } + hash = d + done() + }) + }) + + describe('checkCIDComponents()', () => { + describe('returns undefined on valid CID', () => { + it('create from B58Str multihash', () => { + expect(() => { + const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' + const cid = new CID(mhStr) + const errMsg = CIDUtil.checkCIDComponents(cid) + expect(errMsg).to.not.exist() + }).to.not.throw() + }) + it('create by parts', () => { + expect(() => { + const cid = new CID(1, 'dag-cbor', hash) + const errMsg = CIDUtil.checkCIDComponents(cid) + expect(errMsg).to.not.exist() + }).to.not.throw() + }) + }) + + describe('returns non-null error message on invalid inputs', () => { + const invalid = [ + 'hello world', + 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', + Buffer.from('hello world'), + Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') + ] + + invalid.forEach((i) => it(`new CID(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + const errMsg = CIDUtil.checkCIDComponents(i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + + invalid.forEach((i) => it(`new CID(0, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + const errMsg = CIDUtil.checkCIDComponents(0, 'dag-pb', i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + + invalid.forEach((i) => it(`new CID(1, 'dag-pb', ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)`, () => { + expect(() => { + const errMsg = CIDUtil.checkCIDComponents(1, 'dag-pb', i) + expect(errMsg).to.exist() + }).to.not.throw() + })) + }) }) }) diff --git a/test/profiling/cidperf-x.js b/test/profiling/cidperf-x.js index ddcada7..bafc48f 100644 --- a/test/profiling/cidperf-x.js +++ b/test/profiling/cidperf-x.js @@ -40,10 +40,11 @@ class CIDPerfX { const reps = 10000 const cidPerf = new CIDPerfX() -console.log(`Starting a test: Will run "new CID()" ${reps} times.`) +console.log(`Test: Will run "new CID()" ${reps} times.`) // We just give ~1 second for the JS engine to start and 'rest', etc. // before starting new tests. sleep(1000).then(() => { + console.log(`Starting a test...`) console.time('run'); [...Array(reps).keys()].map(i => { cidPerf.run(i) })