diff --git a/bytes.js b/bytes.js index 0be7e34e..c1883ba8 100644 --- a/bytes.js +++ b/bytes.js @@ -19,24 +19,17 @@ const equals = (aa, bb) => { return true } -const TypedArray = Object.getPrototypeOf(Int8Array) -const isTypedArray = obj => obj instanceof TypedArray - const coerce = o => { if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return o if (o instanceof ArrayBuffer) return new Uint8Array(o) - if (o instanceof DataView || isTypedArray(o)) { + if (ArrayBuffer.isView(o)) { return new Uint8Array(o.buffer, o.byteOffset, o.byteLength) } throw new Error('Unknown type, must be binary type') } -const isBinary = o => { - if (o instanceof DataView) return true - if (o instanceof ArrayBuffer) return true - if (isTypedArray(o)) return true - return false -} +const isBinary = o => + o instanceof ArrayBuffer || ArrayBuffer.isView(o) const fromString = str => (new TextEncoder()).encode(str) const toString = b => (new TextDecoder()).decode(b) diff --git a/cid.js b/cid.js index 7a2f268b..d60ac71f 100644 --- a/cid.js +++ b/cid.js @@ -1,12 +1,11 @@ -import * as bytes from 'multiformats/bytes.js' - -const readonly = (object, key, value) => { - Object.defineProperty(object, key, { - value, - writable: false, - enumerable: true - }) -} +import * as Bytes from 'multiformats/bytes.js' + +const property = (value, { writable = false, enumerable = true, configurable = false } = {}) => ({ + value, + writable, + enumerable, + configurable +}) // ESM does not support importing package.json where this version info // should come from. To workaround it version is copied here. @@ -38,86 +37,180 @@ if (cid) { } ` +/** + * @param {import('./index').Multiformats} multiformats + */ export default multiformats => { const { multibase, varint, multihash } = multiformats - const parse = buff => { - const [code, length] = varint.decode(buff) - return [code, buff.slice(length)] + + /** + * @param {number} version + * @param {number} codec + * @param {Uint8Array} multihash + * @returns {Uint8Array} + */ + const encodeCID = (version, codec, multihash) => { + const versionBytes = varint.encode(version) + const codecBytes = varint.encode(codec) + const bytes = new Uint8Array(versionBytes.byteLength + codecBytes.byteLength + multihash.byteLength) + bytes.set(versionBytes, 0) + bytes.set(codecBytes, versionBytes.byteLength) + bytes.set(multihash, versionBytes.byteLength + codecBytes.byteLength) + return bytes } - const encode = (version, codec, multihash) => { - return Uint8Array.from([ - ...varint.encode(version), - ...varint.encode(codec), - ...multihash - ]) + + /** + * Takes `Uint8Array` representation of `CID` and returns + * `[version, codec, multihash]`. Throws error if bytes passed do not + * correspond to vaild `CID`. + * @param {Uint8Array} bytes + * @returns {[number, number, Uint8Array]} + */ + const decodeCID = (bytes) => { + const [version, offset] = varint.decode(bytes) + switch (version) { + // CIDv0 + case 18: { + return [0, 0x70, bytes] + } + // CIDv1 + case 1: { + const [code, length] = varint.decode(bytes.subarray(offset)) + return [1, code, decodeMultihash(bytes.subarray(offset + length))] + } + default: { + throw new RangeError(`Invalid CID version ${version}`) + } + } } const cidSymbol = Symbol.for('@ipld/js-cid/CID') - class CID { - constructor (cid, ...args) { - Object.defineProperty(this, '_baseCache', { - value: new Map(), - writable: false, - enumerable: false - }) - readonly(this, 'asCID', this) - if (cid != null && cid[cidSymbol] === true) { - readonly(this, 'version', cid.version) - readonly(this, 'multihash', bytes.coerce(cid.multihash)) - readonly(this, 'buffer', bytes.coerce(cid.buffer)) - if (cid.code) readonly(this, 'code', cid.code) - else readonly(this, 'code', multiformats.get(cid.codec).code) - return + /** + * Create CID from the string encoded CID. + * @param {string} string + * @returns {CID} + */ + const fromString = (string) => { + switch (string[0]) { + // V0 + case 'Q': { + const cid = new CID(multibase.get('base58btc').decode(string)) + cid._baseCache.set('base58btc', string) + return cid } - if (args.length > 0) { - if (typeof args[0] !== 'number') throw new Error('String codecs are no longer supported') - readonly(this, 'version', cid) - readonly(this, 'code', args.shift()) - if (this.version === 0 && this.code !== 112) { - throw new Error('Version 0 CID must be 112 codec (dag-cbor)') - } - this._multihash = args.shift() - if (args.length) throw new Error('No longer supported, cannot specify base encoding in instantiation') - if (this.version === 0) readonly(this, 'buffer', this.multihash) - else readonly(this, 'buffer', encode(this.version, this.code, this.multihash)) - return + default: { + // CID v1 + const cid = new CID(multibase.decode(string)) + cid._baseCache.set(multibase.encoding(string).name, string) + return cid } - if (typeof cid === 'string') { - if (cid.startsWith('Q')) { - readonly(this, 'version', 0) - readonly(this, 'code', 0x70) - const { decode } = multibase.get('base58btc') - this._multihash = decode(cid) - readonly(this, 'buffer', this.multihash) - return + } + } + + /** + * Takes a hashCID multihash and validates the digest. Returns it back if + * all good otherwise throws error. + * @param {Uint8Array} hash + * @returns {Uint8Array} + */ + const decodeMultihash = (hash) => { + const { digest, length } = multihash.decode(hash) + if (digest.length !== length) { + throw new Error('Given multihash has incorrect length') + } + + return hash + } + + /** + * @implements {ArrayBufferView} + */ + class CID { + /** + * Creates new CID from the given value that is either CID, string or an + * Uint8Array. + * @param {CID|string|Uint8Array} value + */ + static from (value) { + if (typeof value === 'string') { + return fromString(value) + } else if (value instanceof Uint8Array) { + return new CID(value) + } else { + const cid = CID.asCID(value) + if (cid) { + // If we got the same CID back we create a copy. + if (cid === value) { + return new CID(cid.bytes) + } else { + return cid + } + } else { + throw new TypeError(`Can not create CID from given value ${value}`) } - const { name } = multibase.encoding(cid) - this._baseCache.set(name, cid) - cid = multibase.decode(cid) } - cid = bytes.coerce(cid) - readonly(this, 'buffer', cid) - let code - ;[code, cid] = parse(cid) - if (code === 18) { - // CIDv0 - readonly(this, 'version', 0) - readonly(this, 'code', 0x70) - this._multihash = this.buffer - return + } + + /** + * Creates new CID with a given version, codec and a multihash. + * @param {number} version + * @param {number} code + * @param {Uint8Array} multihash + */ + static create (version, code, multihash) { + if (typeof code !== 'number') { + throw new Error('String codecs are no longer supported') + } + + switch (version) { + case 0: { + if (code !== 112) { + throw new Error('Version 0 CID must be 112 codec (dag-cbor)') + } else { + return new CID(multihash) + } + } + case 1: { + // TODO: Figure out why we check digest here but not in v 0 + return new CID(encodeCID(version, code, decodeMultihash(multihash))) + } + default: { + throw new Error('Invalid version') + } } - if (code > 1) throw new Error(`Invalid CID version ${code}`) - readonly(this, 'version', code) - ;[code, cid] = parse(cid) - readonly(this, 'code', code) - this._multihash = cid } - set _multihash (hash) { - const { length, digest } = multihash.decode(hash) - if (digest.length !== length) throw new Error('Incorrect length') - readonly(this, 'multihash', hash) + /** + * + * @param {ArrayBuffer|Uint8Array} buffer + * @param {number} [byteOffset=0] + * @param {number} [byteLength=buffer.byteLength] + */ + constructor (buffer, byteOffset = 0, byteLength = buffer.byteLength) { + const bytes = buffer instanceof Uint8Array + ? Bytes.coerce(buffer) // Just in case it's a node Buffer + : new Uint8Array(buffer, byteOffset, byteLength) + + const [version, code, multihash] = decodeCID(bytes) + Object.defineProperties(this, { + // ArrayBufferView + buffer: property(bytes.buffer, { enumerable: false }), + byteOffset: property(bytes.byteOffset, { enumerable: false }), + byteLength: property(bytes.byteLength, { enumerable: false }), + + // CID fields + version: property(version), + code: property(code), + multihash: property(multihash), + asCID: property(this), + + // Legacy + bytes: property(bytes, { enumerable: false }), + + // Internal + _baseCache: property(new Map(), { enumerable: false }) + }) } get codec () { @@ -143,11 +236,11 @@ export default multiformats => { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return new CID(0, this.code, this.multihash) + return CID.create(0, this.code, this.multihash) } toV1 () { - return new CID(1, this.code, this.multihash) + return CID.create(1, this.code, this.multihash) } get toBaseEncodedString () { @@ -159,17 +252,25 @@ export default multiformats => { } toString (base) { - if (this.version === 0) { + const { version, bytes } = this + if (version === 0) { if (base && base !== 'base58btc') { throw new Error(`Cannot string encode V0 in ${base} encoding`) } const { encode } = multibase.get('base58btc') - return encode(this.buffer) + return encode(bytes) + } + + base = base || 'base32' + const { _baseCache } = this + const string = _baseCache.get(base) + if (string == null) { + const string = multibase.encode(bytes, base) + _baseCache.set(base, string) + return string + } else { + return string } - if (!base) base = 'base32' - if (this._baseCache.has(base)) return this._baseCache.get(base) - this._baseCache.set(base, multibase.encode(this.buffer, base)) - return this._baseCache.get(base) } toJSON () { @@ -183,17 +284,13 @@ export default multiformats => { equals (other) { return this.code === other.code && this.version === other.version && - bytes.equals(this.multihash, other.multihash) + Bytes.equals(this.multihash, other.multihash) } get [Symbol.toStringTag] () { return 'CID' } - get [cidSymbol] () { - return true - } - /** * Takes any input `value` and returns a `CID` instance if it was * a `CID` otherwise returns `null`. If `value` is instanceof `CID` @@ -217,12 +314,14 @@ export default multiformats => { // API. } else if (value != null && value.asCID === value) { const { version, code, multihash } = value - return new CID(version, code, multihash) + return CID.create(version, code, multihash) // If value is a CID from older implementation that used to be tagged via // symbol we still rebase it to the this `CID` implementation by // delegating that to a constructor. } else if (value != null && value[cidSymbol] === true) { - return new CID(value) + const { version, multihash } = value + const code = value.code || multiformats.get(value.codec).code + return new CID(encodeCID(version, code, multihash)) // Otherwise value is not a CID (or an incompatible version of it) in // which case we return `null`. } else { @@ -232,7 +331,7 @@ export default multiformats => { static isCID (value) { deprecate(/^0\.0/, IS_CID_DEPRECATION) - return !!(value && value[cidSymbol]) + return !!(value && (value[cidSymbol] || value.asCID === value)) } } diff --git a/index.js b/index.js index 30422a97..094c87b6 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,16 @@ import createCID from 'multiformats/cid.js' import * as bytes from 'multiformats/bytes.js' const cache = new Map() + +/** + * @typedef {Object} Varint + * @property {function(Uint8Array):[number, number]} decode + * @property {function(number):Uint8Array} encode + */ + +/** + * @type {Varint} + */ const varint = { decode: data => { const code = varints.decode(data) @@ -16,8 +26,41 @@ const varint = { } } -const createMultihash = multiformats => { - const { get, has, parse, add } = multiformats +/** + * @template Raw,Encoded + * @typedef {(value:Raw) => Encoded} Encode + */ + +/** + * @template Raw,Encoded + * @typedef {Object} Codec + * @property {string} name + * @property {number} code + * @property {Encode} encode + * @property {Encode} decode + */ + +/** + * @typedef {Codec} MultihashCodec + * @typedef {(bytes:Uint8Array) => {name:string, code:number, length:number, digest:Uint8Array}} Multihash$decode + * @typedef {(byte:Uint8Array, base:string|name) => Uint8Array} Multihash$encode + * @typedef {(bytes:Uint8Array, key:string) => Promise} Multihash$hash + * @typedef {Object} Multihash + * @property {Multihash$encode} encode + * @property {Multihash$decode} decode + * @property {Multihash$hash} hash + * @property {function(number|string):boolean} has + * @property {function(number|string):void|MultihashCodec} get + * @property {function(MultihashCodec):void} add + * @property {function(Uint8Array, Uint8Array):Promise} validate + */ + +/** + * @param {MultiformatsUtil & Multicodec} multiformats + * @returns {Multihash} + */ +const createMultihash = ({ get, has, parse, add }) => { + /** @type {Multihash$decode} */ const decode = digest => { const [info, len] = parse(digest) digest = digest.slice(len) @@ -25,6 +68,8 @@ const createMultihash = multiformats => { digest = digest.slice(len2) return { code: info.code, name: info.name, length, digest } } + + /** @type {Multihash$encode} */ const encode = (digest, id) => { let info if (typeof id === 'number') { @@ -36,6 +81,8 @@ const createMultihash = multiformats => { const length = varint.encode(digest.length) return Uint8Array.from([...code, ...length, ...digest]) } + + /** @type {Multihash$hash} */ const hash = async (buff, key) => { buff = bytes.coerce(buff) const info = get(key) @@ -44,6 +91,12 @@ const createMultihash = multiformats => { /* c8 ignore next */ return encode(await info.encode(buff), key) } + + /** + * @param {Uint8Array} _hash + * @param {Uint8Array} buff + * @returns {Promise} + */ const validate = async (_hash, buff) => { _hash = bytes.coerce(_hash) const { length, digest, code } = decode(_hash) @@ -57,9 +110,29 @@ const createMultihash = multiformats => { /* c8 ignore next */ return true } + return { encode, has, decode, hash, validate, add, get } } +/** + * @typedef {Encode} MultibaseDecode + * @typedef {Encode} MultibaseEncode + * @typedef {Object} MultibaseCodec + * @property {string} prefix + * @property {string} name + * @property {MultibaseEncode} encode + * @property {MultibaseDecode} decode + * @typedef {Object} Multibase + * @property {(codec:MultibaseCodec|MultibaseCodec[]) => void} add + * @property {(prefex:string) => MultibaseCodec} get + * @property {(prefex:string) => boolean} has + * @property {(bytes:Uint8Array, prefix:string) => string} encode + * @property {MultibaseDecode} decode + * @property {(text:string) => MultibaseCodec} encoding + * + * + * @returns {Multibase} + */ const createMultibase = () => { const prefixMap = new Map() const nameMap = new Map() @@ -75,6 +148,11 @@ const createMultibase = () => { _add(prefix, name, encode, decode) } } + + /** + * @param {string} id + * @returns {MultibaseCodec} + */ const get = id => { if (id.length === 1) { if (!prefixMap.has(id)) throw new Error(`Missing multibase implementation for "${id}"`) @@ -105,11 +183,37 @@ const createMultibase = () => { const { decode } = get(prefix) return Uint8Array.from(decode(string)) } + /** + * @param {string} string + * @returns {MultibaseCodec} + */ const encoding = string => get(string[0]) return { add, has, get, encode, decode, encoding } } +/** + * @typedef {Object} MultiformatsUtil + * @property {Varint} varint + * @property {function(Uint8Array):[MultihashCodec, number]} parse + * + * @typedef {Object} Multicodec + * @property {function(MultihashCodec):void} add + * @property {function(string|number|Uint8Array):MultihashCodec} get + * @property {function(string):boolean} has + * + * @typedef {Object} MultiformatsExt + * @property {Multicodec} multicodec + * @property {Multibase} multibase + * @property {Multihash} multihash + * + * @typedef {MultiformatsUtil & Multicodec & MultiformatsExt} Multiformats + + * @param {Array<[number, string, Function, Function]>} [table] + * @returns {Multiformats} + */ const create = (table = []) => { + /** @type {Map, Encode]>} + */ const intMap = new Map() const nameMap = new Map() const _add = (code, name, encode, decode) => { @@ -131,6 +235,12 @@ const create = (table = []) => { for (const [code, name, encode, decode] of table) { _add(code, name, encode, decode) } + + /** + * + * @param {Uint8Array} buff + * @returns {[MultihashCodec, number]} + */ const parse = buff => { buff = bytes.coerce(buff) const [code, len] = varint.decode(buff) @@ -140,6 +250,7 @@ const create = (table = []) => { } return [{ code, name, encode, decode }, len] } + const get = obj => { if (typeof obj === 'string') { if (nameMap.has(obj)) { @@ -190,10 +301,12 @@ const create = (table = []) => { } const multiformats = { parse, add, get, has, encode, decode, varint, bytes } + /** @type {Multicodec} */ multiformats.multicodec = { add, get, has, encode, decode } multiformats.multibase = createMultibase() multiformats.multihash = createMultihash(multiformats) multiformats.CID = createCID(multiformats) + return multiformats } export { create, bytes, varint } diff --git a/legacy.js b/legacy.js index 432d0a4a..146305fc 100644 --- a/legacy.js +++ b/legacy.js @@ -5,10 +5,17 @@ import { Buffer } from 'buffer' const legacy = (multiformats, name) => { const toLegacy = obj => { if (CID.isCID(obj)) { - if (!obj.code) return obj - const { name } = multiformats.multicodec.get(obj.code) - return new CID(obj.version, name, Buffer.from(obj.multihash)) + return obj } + + const cid = multiformats.CID.asCID(obj) + if (cid) { + const { version, multihash: { buffer, byteOffset, byteLength } } = cid + const { name } = multiformats.multicodec.get(cid.code) + const multihash = Buffer.from(buffer, byteOffset, byteLength) + return new CID(version, name, Buffer.from(multihash)) + } + if (bytes.isBinary(obj)) return Buffer.from(obj) if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { @@ -18,7 +25,8 @@ const legacy = (multiformats, name) => { return obj } const fromLegacy = obj => { - if (CID.isCID(obj)) return new multiformats.CID(obj) + const cid = multiformats.CID.asCID(obj) + if (cid) return cid if (bytes.isBinary(obj)) return bytes.coerce(obj) if (obj && typeof obj === 'object') { for (const [key, value] of Object.entries(obj)) { diff --git a/test/test-cid.js b/test/test-cid.js index 1352feb0..d80e1091 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -2,7 +2,7 @@ import crypto from 'crypto' import OLDCID from 'cids' import assert from 'assert' -import { toHex } from 'multiformats/bytes.js' +import { toHex, equals } from 'multiformats/bytes.js' import multiformats from 'multiformats/basics.js' import base58 from 'multiformats/bases/base58.js' import base32 from 'multiformats/bases/base32.js' @@ -11,6 +11,8 @@ import util from 'util' const test = it const same = assert.deepStrictEqual +// eslint-disable-next-line no-unused-vars + const testThrow = async (fn, message) => { try { await fn() @@ -52,7 +54,7 @@ describe('CID', () => { describe('v0', () => { test('handles B58Str multihash', () => { const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid = new CID(mhStr) + const cid = CID.from(mhStr) same(cid.code, 112) same(cid.version, 0) @@ -63,7 +65,7 @@ describe('CID', () => { test('create by parts', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(0, 112, hash) + const cid = CID.create(0, 112, hash) same(cid.code, 112) same(cid.version, 0) @@ -74,7 +76,7 @@ describe('CID', () => { test('create from multihash', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(hash) + const cid = CID.from(hash) same(cid.code, 112) same(cid.version, 0) @@ -85,42 +87,42 @@ describe('CID', () => { test('throws on invalid BS58Str multihash ', async () => { const msg = 'Non-base58 character' - testThrow(() => new CID('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), msg) + testThrow(() => CID.from('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), msg) }) test('throws on trying to create a CIDv0 with a codec other than dag-pb', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') const msg = 'Version 0 CID must be 112 codec (dag-cbor)' - testThrow(() => new CID(0, 113, hash), msg) + testThrow(() => CID.create(0, 113, hash), msg) }) test('throws on trying to pass specific base encoding [deprecated]', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') const msg = 'No longer supported, cannot specify base encoding in instantiation' - testThrow(() => new CID(0, 112, hash, 'base32'), msg) + testThrow(() => CID.create(0, 112, hash, 'base32'), msg) }) test('throws on trying to base encode CIDv0 in other base than base58btc', () => { const mhStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid = new CID(mhStr) + const cid = CID.from(mhStr) const msg = 'Cannot string encode V0 in base32 encoding' testThrow(() => cid.toString('base32'), msg) }) - test('.buffer', async () => { + test('.bytes', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') const codec = 112 - const cid = new CID(0, codec, hash) - const buffer = cid.buffer - assert.ok(buffer) - const str = toHex(buffer) + const cid = CID.create(0, codec, hash) + const bytes = cid.bytes + assert.ok(bytes) + const str = toHex(bytes) same(str, '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') }) test('should construct from an old CID', () => { const cidStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const oldCid = new CID(cidStr) - const newCid = new CID(oldCid) + const oldCid = CID.from(cidStr) + const newCid = CID.from(oldCid) same(newCid.toString(), cidStr) }) }) @@ -128,17 +130,17 @@ describe('CID', () => { describe('v1', () => { test('handles CID String (multibase encoded)', () => { const cidStr = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' - const cid = new CID(cidStr) + const cid = CID.from(cidStr) same(cid.code, 112) same(cid.version, 1) assert.ok(cid.multihash) - same(cid.toString(), multibase.encode(cid.buffer, 'base32')) + same(cid.toString(), multibase.encode(cid.bytes, 'base32')) }) test('handles CID (no multibase)', () => { const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' const cidBuf = Buffer.from('017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5', 'hex') - const cid = new CID(cidBuf) + const cid = CID.from(cidBuf) same(cid.code, 112) same(cid.version, 1) same(cid.toString(), cidStr) @@ -146,16 +148,16 @@ describe('CID', () => { test('create by parts', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 0x71, hash) + const cid = CID.create(1, 0x71, hash) same(cid.code, 0x71) same(cid.version, 1) - same(cid.multihash, hash) + assert.ok(equals(cid.multihash, hash)) }) test('can roundtrip through cid.toString()', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid1 = new CID(1, 0x71, hash) - const cid2 = new CID(cid1.toString()) + const cid1 = CID.create(1, 0x71, hash) + const cid2 = CID.from(cid1.toString()) same(cid1.code, cid2.code) same(cid1.version, cid2.version) @@ -166,8 +168,8 @@ describe('CID', () => { test('handles multibyte varint encoded codec codes', () => { const ethBlockHash = Buffer.from('8a8e84c797605fbe75d5b5af107d4220a2db0ad35fd66d9be3d38d87c472b26d', 'hex') const mh = multihash.encode(ethBlockHash, 'keccak-256') - const cid1 = new CID(1, 'eth-block', mh) - const cid2 = new CID(cid1.toBaseEncodedString()) + const cid1 = CID.create(1, 'eth-block', mh) + const cid2 = CID.from(cid1.toBaseEncodedString()) same(cid1.codec, 'eth-block') same(cid1.version, 1) @@ -179,20 +181,20 @@ describe('CID', () => { }) */ - test('.buffer', async () => { + test('.bytes', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') const code = 0x71 - const cid = new CID(1, code, hash) - const buffer = cid.buffer - assert.ok(buffer) - const str = toHex(buffer) + const cid = CID.create(1, code, hash) + const bytes = cid.bytes + assert.ok(bytes) + const str = toHex(bytes) same(str, '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') }) test('should construct from an old CID without a multibaseName', () => { const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' - const oldCid = new CID(cidStr) - const newCid = new CID(oldCid) + const oldCid = CID.from(cidStr) + const newCid = CID.from(oldCid) same(newCid.toString(), cidStr) }) }) @@ -202,71 +204,71 @@ describe('CID', () => { const h2 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1o' test('.equals v0 to v0', () => { - same(new CID(h1).equals(new CID(h1)), true) - same(new CID(h1).equals(new CID(h2)), false) + same(CID.from(h1).equals(CID.from(h1)), true) + same(CID.from(h1).equals(CID.from(h2)), false) }) test('.equals v0 to v1 and vice versa', () => { const cidV1Str = 'zdj7Wd8AMwqnhJGQCbFxBVodGSBG84TM7Hs1rcJuQMwTyfEDS' - const cidV1 = new CID(cidV1Str) + const cidV1 = CID.from(cidV1Str) const cidV0 = cidV1.toV0() same(cidV0.equals(cidV1), false) same(cidV1.equals(cidV0), false) + same(cidV1.multihash, cidV0.multihash) }) test('.isCid', () => { - assert.ok(CID.isCID(new CID(h1))) + assert.ok(CID.isCID(CID.from(h1))) assert.ok(!CID.isCID(false)) assert.ok(!CID.isCID(Buffer.from('hello world'))) - assert.ok(CID.isCID(new CID(h1).toV0())) + assert.ok(CID.isCID(CID.from(h1).toV0())) - assert.ok(CID.isCID(new CID(h1).toV1())) + assert.ok(CID.isCID(CID.from(h1).toV1())) }) test('works with deepEquals', () => { - const ch1 = new CID(h1) + const ch1 = CID.from(h1) ch1._baseCache.set('herp', 'derp') - assert.deepStrictEqual(ch1, new CID(h1)) - assert.notDeepStrictEqual(ch1, new CID(h2)) + assert.deepStrictEqual(ch1, CID.from(h1)) + assert.notDeepStrictEqual(ch1, CID.from(h2)) }) }) describe('throws on invalid inputs', () => { - const invalid = [ + const from = [ 'hello world', 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L', Buffer.from('hello world'), - Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT') + Buffer.from('QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT'), + {} ] - let mapper = i => { - const name = `new CID(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)` - test(name, () => testThrowAny(() => new CID(i))) + for (const i of from) { + const name = `CID.from(${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)` + test(name, () => testThrowAny(() => CID.from(i))) } - invalid.forEach(mapper) - mapper = i => { - const name = `new CID(0, 112, ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)` - test(name, () => testThrowAny(() => new CID(0, 112, i))) - } - invalid.forEach(mapper) + const create = [ + ...from.map(i => [0, 112, i]), + ...from.map(i => [1, 112, i]), + [18, 112, 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L'] + ] - mapper = i => { - const name = `new CID(1, 112, ${Buffer.isBuffer(i) ? 'buffer' : 'string'}<${i.toString()}>)` - test(name, () => testThrowAny(() => new CID(1, 112, i))) + for (const [version, code, hash] of create) { + const name = `CID.create(${version}, ${code}, ${Buffer.isBuffer(hash) ? 'buffer' : 'string'}<${hash.toString()}>)` + test(name, () => testThrowAny(() => CID.create(version, code, hash))) } - invalid.forEach(mapper) }) describe('idempotence', () => { const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' - const cid1 = new CID(h1) - const cid2 = new CID(cid1) + const cid1 = CID.from(h1) + const cid2 = CID.from(cid1) test('constructor accept constructed instance', () => { same(cid1.equals(cid2), true) @@ -277,25 +279,25 @@ describe('CID', () => { describe('conversion v0 <-> v1', () => { test('should convert v0 to v1', async () => { const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') - const cid = (new CID(0, 112, hash)).toV1() + const cid = (CID.create(0, 112, hash)).toV1() same(cid.version, 1) }) test('should convert v1 to v0', async () => { const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') - const cid = (new CID(1, 112, hash)).toV0() + const cid = (CID.create(1, 112, hash)).toV0() same(cid.version, 0) }) test('should not convert v1 to v0 if not dag-pb codec', async () => { const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') - const cid = new CID(1, 0x71, hash) + const cid = CID.create(1, 0x71, hash) await testThrow(() => cid.toV0(), 'Cannot convert a non dag-pb CID to CIDv0') }) test('should not convert v1 to v0 if not sha2-256 multihash', async () => { const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-512') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) await testThrow(() => cid.toV0(), 'Cannot convert non sha2-256 multihash CID to CIDv0') }) }) @@ -303,13 +305,13 @@ describe('CID', () => { describe('caching', () => { test('should cache CID as buffer', async () => { const hash = await multihash.hash(Buffer.from(`TEST${Date.now()}`), 'sha2-256') - const cid = new CID(1, 112, hash) - assert.ok(cid.buffer) - same(cid.buffer, cid.buffer) + const cid = CID.create(1, 112, hash) + assert.ok(cid.bytes) + same(cid.bytes, cid.bytes) }) test('should cache string representation when it matches the multibaseName it was constructed with', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) same(cid._baseCache.size, 0) same(cid.toString('base64'), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') @@ -325,21 +327,24 @@ describe('CID', () => { }) test('should cache string representation when constructed with one', () => { const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' - const cid = new CID(base32String) + const cid = CID.from(base32String) same(cid._baseCache.get('base32'), base32String) }) }) test('toJSON()', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) - same(cid.toJSON(), { code: 112, version: 1, hash }) + const cid = CID.create(1, 112, hash) + const json = cid.toJSON() + + same({ ...json, hash: null }, { code: 112, version: 1, hash: null }) + assert.ok(equals(json.hash, hash)) }) test('isCID', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) - assert.ok(OLDCID.isCID(cid)) + const cid = CID.create(1, 112, hash) + assert.strictEqual(OLDCID.isCID(cid), false) }) test('asCID', async () => { @@ -370,7 +375,7 @@ describe('CID', () => { assert.ok(cid1 instanceof CID) assert.strictEqual(cid1.code, code) assert.strictEqual(cid1.version, version) - assert.strictEqual(cid1.multihash, _multihash) + assert.ok(equals(cid1.multihash, _multihash)) const cid2 = CID.asCID({ version, code, _multihash }) assert.strictEqual(cid2, null) @@ -381,7 +386,7 @@ describe('CID', () => { assert.ok(cid3 instanceof CID) assert.strictEqual(cid3.code, code) assert.strictEqual(cid3.version, version) - assert.strictEqual(cid3.multihash, _multihash) + assert.ok(equals(cid3.multihash, _multihash)) const cid4 = CID.asCID(cid3) assert.strictEqual(cid3, cid4) @@ -389,22 +394,23 @@ describe('CID', () => { const cid5 = CID.asCID(new OLDCID(1, 'raw', Buffer.from(hash))) assert.ok(cid5 instanceof CID) assert.strictEqual(cid5.version, 1) - same(cid5.multihash, hash) + assert.ok(equals(cid5.multihash, hash)) assert.strictEqual(cid5.code, 85) }) test('new CID from old CID', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(new OLDCID(1, 'raw', Buffer.from(hash))) + const cid = CID.from(new OLDCID(1, 'raw', Buffer.from(hash))) same(cid.version, 1) - same(cid.multihash, hash) + + assert.ok(equals(cid.multihash, hash)) same(cid.code, 85) }) if (!process.browser) { test('util.inspect', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) same(util.inspect(cid), 'CID(bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu)') }) } @@ -412,29 +418,29 @@ describe('CID', () => { describe('deprecations', async () => { test('codec', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) await testThrow(() => cid.codec, '"codec" property is deprecated, use integer "code" property instead') - await testThrow(() => new CID(1, 'dag-pb', hash), 'String codecs are no longer supported') + await testThrow(() => CID.create(1, 'dag-pb', hash), 'String codecs are no longer supported') }) test('multibaseName', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) await testThrow(() => cid.multibaseName, '"multibaseName" property is deprecated') }) test('prefix', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) await testThrow(() => cid.prefix, '"prefix" property is deprecated') }) test('toBaseEncodedString()', async () => { const hash = await multihash.hash(Buffer.from('abc'), 'sha2-256') - const cid = new CID(1, 112, hash) + const cid = CID.create(1, 112, hash) await testThrow(() => cid.toBaseEncodedString(), 'Deprecated, use .toString()') }) }) test('invalid CID version', async () => { const encoded = varint.encode(2) - await testThrow(() => new CID(encoded), 'Invalid CID version 2') + await testThrow(() => CID.from(encoded), 'Invalid CID version 2') }) }) diff --git a/test/test-legacy.js b/test/test-legacy.js index 74396548..84bdb251 100644 --- a/test/test-legacy.js +++ b/test/test-legacy.js @@ -38,7 +38,7 @@ describe('multicodec', () => { decode: buff => { const obj = json.util.deserialize(buff) obj.l = link - if (obj.o.link) obj.link = new multiformats.CID(link) + if (obj.o.link) obj.link = multiformats.CID.from(link) return obj } })