From 88e29ea7a8c1a1a284c654311cfb1d67cbfd8e6c Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 19 Jan 2022 08:43:46 +0000 Subject: [PATCH] feat: define Link interface for CID --- .github/workflows/typecheck.yml | 2 +- package.json | 9 + src/bases/base.js | 83 +++----- src/bases/interface.js | 1 + src/bases/interface.ts | 1 + src/block.js | 120 ++++++----- src/block/interface.js | 1 + src/block/interface.ts | 72 +++++++ src/cid.js | 358 +++++++++++++++++++++----------- src/codecs/interface.js | 1 + src/codecs/interface.ts | 9 +- src/hashes/digest.js | 17 +- src/hashes/interface.js | 1 + src/hashes/interface.ts | 4 +- src/index.js | 2 + src/interface.js | 1 + src/interface.ts | 5 + src/link.js | 73 +++++++ src/link/interface.js | 1 + src/link/interface.ts | 55 +++++ src/traversal.js | 15 +- test/test-block.js | 4 +- test/test-bytes.js | 2 +- test/test-cid.js | 358 ++++++++++++++++++++++++-------- test/test-link.js | 121 +++++++++++ test/test-multibase-spec.js | 2 +- test/test-multibase.js | 2 +- test/test-multicodec.js | 2 +- test/test-multihash.js | 2 +- test/test-traversal.js | 2 +- test/tsconfig.json | 49 ----- tsconfig.json | 21 +- vendor/base-x.d.ts | 15 +- vendor/varint.d.ts | 67 +++--- 34 files changed, 1041 insertions(+), 437 deletions(-) create mode 100644 src/bases/interface.js create mode 100644 src/block/interface.js create mode 100644 src/block/interface.ts create mode 100644 src/codecs/interface.js create mode 100644 src/hashes/interface.js create mode 100644 src/interface.js create mode 100644 src/interface.ts create mode 100644 src/link.js create mode 100644 src/link/interface.js create mode 100644 src/link/interface.ts create mode 100644 test/test-link.js delete mode 100644 test/tsconfig.json diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 6b6f96ae..facca92b 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -26,4 +26,4 @@ jobs: - name: Typecheck uses: gozala/typescript-error-reporter-action@v1.0.8 with: - project: test/tsconfig.json + project: tsconfig.json diff --git a/package.json b/package.json index c61b9dae..8c583f77 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "./cid": { "import": "./src/cid.js" }, + "./link": { + "import": "./src/link.js" + }, "./basics": { "import": "./src/basics.js" }, @@ -101,6 +104,12 @@ }, "./codecs/raw": { "import": "./src/codecs/raw.js" + }, + "./interface": { + "import": "./src/interface.js" + }, + "./bytes": { + "import": "./src/bytes.js" } }, "devDependencies": { diff --git a/src/bases/base.js b/src/bases/base.js index 00bc80b9..658da0ab 100644 --- a/src/bases/base.js +++ b/src/bases/base.js @@ -1,20 +1,8 @@ import basex from '../../vendor/base-x.js' import { coerce } from '../bytes.js' - -/** - * @typedef {import('./interface').BaseEncoder} BaseEncoder - * @typedef {import('./interface').BaseDecoder} BaseDecoder - * @typedef {import('./interface').BaseCodec} BaseCodec - */ - -/** - * @template {string} T - * @typedef {import('./interface').Multibase} Multibase - */ -/** - * @template {string} T - * @typedef {import('./interface').MultibaseEncoder} MultibaseEncoder - */ +// Linter can't see that API is used in types. +// eslint-disable-next-line +import * as API from './interface.js' /** * Class represents both BaseEncoder and MultibaseEncoder meaning it @@ -23,8 +11,8 @@ import { coerce } from '../bytes.js' * @class * @template {string} Base * @template {string} Prefix - * @implements {MultibaseEncoder} - * @implements {BaseEncoder} + * @implements {API.MultibaseEncoder} + * @implements {API.BaseEncoder} */ class Encoder { /** @@ -40,7 +28,7 @@ class Encoder { /** * @param {Uint8Array} bytes - * @returns {Multibase} + * @returns {API.Multibase} */ encode (bytes) { if (bytes instanceof Uint8Array) { @@ -51,16 +39,6 @@ class Encoder { } } -/** - * @template {string} Prefix - * @typedef {import('./interface').MultibaseDecoder} MultibaseDecoder - */ - -/** - * @template {string} Prefix - * @typedef {import('./interface').UnibaseDecoder} UnibaseDecoder - */ - /** * @template {string} Prefix */ @@ -68,12 +46,11 @@ class Encoder { * Class represents both BaseDecoder and MultibaseDecoder so it could be used * to decode multibases (with matching prefix) or just base decode strings * with corresponding base encoding. - * @class * @template {string} Base * @template {string} Prefix - * @implements {MultibaseDecoder} - * @implements {UnibaseDecoder} - * @implements {BaseDecoder} + * @implements {API.MultibaseDecoder} + * @implements {API.UnibaseDecoder} + * @implements {API.BaseDecoder} */ class Decoder { /** @@ -109,7 +86,7 @@ class Decoder { /** * @template {string} OtherPrefix - * @param {UnibaseDecoder|ComposedDecoder} decoder + * @param {API.UnibaseDecoder|ComposedDecoder} decoder * @returns {ComposedDecoder} */ or (decoder) { @@ -119,22 +96,17 @@ class Decoder { /** * @template {string} Prefix - * @typedef {import('./interface').CombobaseDecoder} CombobaseDecoder - */ - -/** - * @template {string} Prefix - * @typedef {Record>} Decoders + * @typedef {Record>} Decoders */ /** * @template {string} Prefix - * @implements {MultibaseDecoder} - * @implements {CombobaseDecoder} + * @implements {API.MultibaseDecoder} + * @implements {API.CombobaseDecoder} */ class ComposedDecoder { /** - * @param {Record>} decoders + * @param {Decoders} decoders */ constructor (decoders) { this.decoders = decoders @@ -142,7 +114,7 @@ class ComposedDecoder { /** * @template {string} OtherPrefix - * @param {UnibaseDecoder|ComposedDecoder} decoder + * @param {API.UnibaseDecoder|ComposedDecoder} decoder * @returns {ComposedDecoder} */ or (decoder) { @@ -167,30 +139,25 @@ class ComposedDecoder { /** * @template {string} L * @template {string} R - * @param {UnibaseDecoder|CombobaseDecoder} left - * @param {UnibaseDecoder|CombobaseDecoder} right + * @param {API.UnibaseDecoder|API.CombobaseDecoder} left + * @param {API.UnibaseDecoder|API.CombobaseDecoder} right * @returns {ComposedDecoder} */ export const or = (left, right) => new ComposedDecoder(/** @type {Decoders} */({ - ...(left.decoders || { [/** @type UnibaseDecoder */(left).prefix]: left }), - ...(right.decoders || { [/** @type UnibaseDecoder */(right).prefix]: right }) + ...(left.decoders || { [/** @type API.UnibaseDecoder */(left).prefix]: left }), + ...(right.decoders || { [/** @type API.UnibaseDecoder */(right).prefix]: right }) })) -/** - * @template T - * @typedef {import('./interface').MultibaseCodec} MultibaseCodec - */ - /** * @class * @template {string} Base * @template {string} Prefix - * @implements {MultibaseCodec} - * @implements {MultibaseEncoder} - * @implements {MultibaseDecoder} - * @implements {BaseCodec} - * @implements {BaseEncoder} - * @implements {BaseDecoder} + * @implements {API.MultibaseCodec} + * @implements {API.MultibaseEncoder} + * @implements {API.MultibaseDecoder} + * @implements {API.BaseCodec} + * @implements {API.BaseEncoder} + * @implements {API.BaseDecoder} */ export class Codec { /** diff --git a/src/bases/interface.js b/src/bases/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/bases/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/bases/interface.ts b/src/bases/interface.ts index dd4374e2..e2f05c10 100644 --- a/src/bases/interface.ts +++ b/src/bases/interface.ts @@ -97,3 +97,4 @@ export interface UnibaseDecoder extends MultibaseDecoder< export interface CombobaseDecoder extends MultibaseDecoder { readonly decoders: Record> } + diff --git a/src/block.js b/src/block.js index ce20bafb..23c122e1 100644 --- a/src/block.js +++ b/src/block.js @@ -1,4 +1,7 @@ import { bytes as binary, CID } from './index.js' +// Linter can see that API is used in types. +// eslint-disable-next-line +import * as API from './interface.js' const readonly = ({ enumerable = true, configurable = false } = {}) => ({ enumerable, @@ -71,9 +74,10 @@ const tree = function * (source, base) { * @template T * @param {T} source * @param {string[]} path + * @return {API.BlockCursorView} */ const get = (source, path) => { - let node = /** @type {Record} */ (source) + let node = /** @type {Record} */(source) for (const [index, key] of path.entries()) { node = node[key] if (node == null) { @@ -88,17 +92,21 @@ const get = (source, path) => { } /** - * @template T + * @template {unknown} T - Logical type of the data encoded in the block + * @template {number} C - multicodec code corresponding to codec used to encode the block + * @template {number} A - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template {API.Version} V - CID version + * @implements {API.BlockView} */ class Block { /** * @param {Object} options - * @param {CID} options.cid - * @param {ByteView} options.bytes + * @param {CID} options.cid + * @param {API.ByteView} options.bytes * @param {T} options.value */ constructor ({ cid, bytes, value }) { - if (!cid || !bytes || typeof value === 'undefined') throw new Error('Missing required argument') + if (!cid || !bytes || typeof value === 'undefined') { throw new Error('Missing required argument') } this.cid = cid this.bytes = bytes @@ -123,22 +131,23 @@ class Block { } /** - * @param {string} [path] - */ + * @param {string} [path] + * @return {API.BlockCursorView} + */ get (path = '/') { return get(this.value, path.split('/').filter(Boolean)) } } /** - * @template T - * @template {number} Code - * @template {number} Algorithm + * @template {unknown} T - Logical type of the data encoded in the block + * @template {number} Code - multicodec code corresponding to codec used to encode the block + * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. * @param {Object} options * @param {T} options.value - * @param {BlockEncoder} options.codec - * @param {Hasher} options.hasher - * @returns {Promise>} + * @param {API.BlockEncoder} options.codec + * @param {API.MultihashHasher} options.hasher + * @returns {Promise>} */ const encode = async ({ value, codec, hasher }) => { if (typeof value === 'undefined') throw new Error('Missing required argument "value"') @@ -146,20 +155,25 @@ const encode = async ({ value, codec, hasher }) => { const bytes = codec.encode(value) const hash = await hasher.digest(bytes) - const cid = CID.create(1, codec.code, hash) + /** @type {CID} */ + const cid = CID.create( + 1, + codec.code, + hash + ) return new Block({ value, bytes, cid }) } /** - * @template T - * @template {number} Code - * @template {number} Algorithm + * @template {unknown} T - Logical type of the data encoded in the block + * @template {number} Code - multicodec code corresponding to codec used to encode the block + * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. * @param {Object} options - * @param {ByteView} options.bytes - * @param {BlockDecoder} options.codec - * @param {Hasher} options.hasher - * @returns {Promise>} + * @param {API.ByteView} options.bytes + * @param {API.BlockDecoder} options.codec + * @param {API.MultihashHasher} options.hasher + * @returns {Promise>} */ const decode = async ({ bytes, codec, hasher }) => { if (!bytes) throw new Error('Missing required argument "bytes"') @@ -167,6 +181,7 @@ const decode = async ({ bytes, codec, hasher }) => { const value = codec.decode(bytes) const hash = await hasher.digest(bytes) + /** @type {CID} */ const cid = CID.create(1, codec.code, hash) return new Block({ value, bytes, cid }) @@ -178,10 +193,12 @@ const decode = async ({ bytes, codec, hasher }) => { */ /** - * @template T - * @template {number} Code - * @param {{ cid: CID, value:T, codec?: BlockDecoder, bytes: ByteView }|{cid:CID, bytes:ByteView, value?:void, codec:BlockDecoder}} options - * @returns {Block} + * @template {unknown} T - Logical type of the data encoded in the block + * @template {number} Code - multicodec code corresponding to codec used to encode the block + * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template {API.Version} V - CID version + * @param {{ cid: API.Link, value:T, codec?: API.BlockDecoder, bytes: API.ByteView }|{cid:API.Link, bytes:API.ByteView, value?:void, codec:API.BlockDecoder}} options + * @returns {API.BlockView} */ const createUnsafe = ({ bytes, cid, value: maybeValue, codec }) => { const value = maybeValue !== undefined @@ -190,19 +207,25 @@ const createUnsafe = ({ bytes, cid, value: maybeValue, codec }) => { if (value === undefined) throw new Error('Missing required argument, must either provide "value" or "codec"') - return new Block({ cid, bytes, value }) + return new Block({ + // eslint-disable-next-line object-shorthand + cid: /** @type {CID} */ (cid), + bytes, + value + }) } /** - * @template T - * @template {number} Code - * @template {number} Algorithm + * @template {unknown} T - Logical type of the data encoded in the block + * @template {number} Code - multicodec code corresponding to codec used to encode the block + * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template {API.Version} V - CID version * @param {Object} options - * @param {CID} options.cid - * @param {ByteView} options.bytes - * @param {BlockDecoder} options.codec - * @param {Hasher} options.hasher - * @returns {Promise>} + * @param {API.Link} options.cid + * @param {API.ByteView} options.bytes + * @param {API.BlockDecoder} options.codec + * @param {API.MultihashHasher} options.hasher + * @returns {Promise>} */ const create = async ({ bytes, cid, hasher, codec }) => { if (!bytes) throw new Error('Missing required argument "bytes"') @@ -213,29 +236,12 @@ const create = async ({ bytes, cid, hasher, codec }) => { throw new Error('CID hash does not match bytes') } - return createUnsafe({ bytes, cid, value, codec }) + return createUnsafe({ + bytes, + cid, + value, + codec + }) } export { encode, decode, create, createUnsafe, Block } - -/** - * @template T - * @typedef {import('./codecs/interface').ByteView} ByteView - */ - -/** - * @template {number} Code - * @template T - * @typedef {import('./codecs/interface').BlockEncoder} BlockEncoder - */ - -/** - * @template {number} Code - * @template T - * @typedef {import('./codecs/interface').BlockDecoder} BlockDecoder - */ - -/** - * @template Algorithm - * @typedef {import('./hashes/interface').MultihashHasher} Hasher - */ diff --git a/src/block/interface.js b/src/block/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/block/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/block/interface.ts b/src/block/interface.ts new file mode 100644 index 00000000..4809907b --- /dev/null +++ b/src/block/interface.ts @@ -0,0 +1,72 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable no-use-before-define */ +import { Link, Version } from '../link/interface.js' +import { CID } from "../cid.js" + +/** + * A byte-encoded representation of some type of `Data`. + * + * A `ByteView` is essentially a `Uint8Array` that's been "tagged" with + * a `Data` type parameter indicating the type of encoded data. + * + * For example, a `ByteView<{ hello: "world" }>` is a `Uint8Array` containing a + * binary representation of a `{hello: "world"}. + */ +export interface ByteView extends Uint8Array, Phantom {} + +/** + * A utility type to retain an unused type parameter `T`. + * Similar to [phantom type parameters in Rust](https://doc.rust-lang.org/rust-by-example/generics/phantom.html). + * + * Capturing unused type parameters allows us to define "nominal types," which + * TypeScript does not natively support. Nominal types in turn allow us to capture + * semantics not represented in the actual type structure, without requring us to define + * new classes or pay additional runtime costs. + * + * For a concrete example, see {@link ByteView}, which extends the `Uint8Array` type to capture + * type information about the structure of the data encoded into the array. + */ +export interface Phantom { + // This field can not be represented because field name is nonexistent + // unique symbol. But given that field is optional any object will valid + // type contstraint. + [Marker]?: T +} +declare const Marker: unique symbol + +/** + * Represents an IPLD block (including its CID) that can be decoded to data of + * type `T`. + * + * @template T - Logical type of the data encoded in the block + * @template C - multicodec code corresponding to codec used to encode the block + * @template A - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template V - CID version + */ +export interface Block< + T = unknown, + C extends number = number, + A extends number = number, + V extends Version = 1 +> { + bytes: ByteView + cid: Link +} + +export type BlockCursorView = + | { value: T, remaining?: undefined } + | { value: CID, remaining: string } + +export interface BlockView< + T = unknown, + C extends number = number, + A extends number = number, + V extends Version = 1 +> extends Block { + cid: CID + value: T + + links(): Iterable<[string, CID]> + tree(): Iterable + get(path:string): BlockCursorView +} diff --git a/src/cid.js b/src/cid.js index 403c3104..ffab5750 100644 --- a/src/cid.js +++ b/src/cid.js @@ -3,48 +3,90 @@ import * as Digest from './hashes/digest.js' import { base58btc } from './bases/base58.js' import { base32 } from './bases/base32.js' import { coerce } from './bytes.js' +// Linter can see that API is used in types. +// eslint-disable-next-line +import * as API from "./link/interface.js" + +// This way TS will also expose all the types from module +export * from './link/interface.js' /** - * @typedef {import('./hashes/interface').MultihashDigest} MultihashDigest - * @typedef {0 | 1} CIDVersion + * @template {API.Link} T + * @template {string} Prefix + * @param {T} link + * @param {API.MultibaseEncoder} [base] + * @returns {API.ToString} */ +export const format = (link, base) => { + const { bytes, version } = link + switch (version) { + case 0: + return toStringV0( + bytes, + baseCache(link), + /** @type {API.MultibaseEncoder<"z">} */ (base) || base58btc.encoder + ) + default: + return toStringV1( + bytes, + baseCache(link), + /** @type {API.MultibaseEncoder} */ (base || base32.encoder) + ) + } +} + +/** @type {WeakMap>} */ +const cache = new WeakMap() /** - * @template Prefix - * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder + * @param {API.UnknownLink} cid + * @returns {Map} */ +const baseCache = cid => { + const baseCache = cache.get(cid) + if (baseCache == null) { + const baseCache = new Map() + cache.set(cid, baseCache) + return baseCache + } + return baseCache +} /** - * @template Prefix - * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder + * @template {unknown} [Data=unknown] + * @template {number} [Format=number] + * @template {number} [Alg=number] + * @template {API.Version} [Version=API.Version] + * @implements {API.Link} */ export class CID { /** - * @param {CIDVersion} version - * @param {number} code - multicodec code, see https://github.com/multiformats/multicodec/blob/master/table.csv - * @param {MultihashDigest} multihash + * @param {Version} version - Version of the CID + * @param {Format} code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv + * @param {API.MultihashDigest} multihash - (Multi)hash of the of the content. * @param {Uint8Array} bytes * */ constructor (version, code, multihash, bytes) { + /** @readonly */ this.code = code + /** @readonly */ this.version = version + /** @readonly */ this.multihash = multihash + /** @readonly */ this.bytes = bytes // ArrayBufferView + /** @readonly */ this.byteOffset = bytes.byteOffset + /** @readonly */ this.byteLength = bytes.byteLength // Circular reference - /** @private */ + /** @readonly */ this.asCID = this - /** - * @type {Map} - * @private - */ - this._baseCache = new Map() // Configure private properties Object.defineProperties(this, { @@ -56,20 +98,19 @@ export class CID { multihash: readonly, bytes: readonly, - _baseCache: hidden, asCID: hidden }) } /** - * @returns {CID} + * @returns {CID} */ toV0 () { switch (this.version) { case 0: { - return this + return /** @type {CID} */ (this) } - default: { + case 1: { const { code, multihash } = this if (code !== DAG_PB_CODE) { @@ -81,53 +122,79 @@ export class CID { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return CID.createV0(multihash) + return /** @type {CID} */ ( + CID.createV0( + /** @type {API.MultihashDigest} */ (multihash) + ) + ) + } + default: { + throw Error( + `Can not convert CID version ${this.version} to version 0. This is a bug please report` + ) } } } /** - * @returns {CID} + * @returns {CID} */ toV1 () { switch (this.version) { case 0: { const { code, digest } = this.multihash const multihash = Digest.create(code, digest) - return CID.createV1(this.code, multihash) + return /** @type {CID} */ ( + CID.createV1(this.code, multihash) + ) } case 1: { - return this + return /** @type {CID} */ (this) } - /* c8 ignore next 3 */ default: { - throw Error(`Can not convert CID version ${this.version} to version 0. This is a bug please report`) + throw Error( + `Can not convert CID version ${this.version} to version 1. This is a bug please report` + ) } } } /** - * @param {any} other + * @param {unknown} other + * @returns {other is CID} */ equals (other) { - return other && - this.code === other.code && - this.version === other.version && - Digest.equals(this.multihash, other.multihash) + return CID.equals(this, other) + } + + /** + * @template {unknown} Data + * @template {number} Format + * @template {number} Alg + * @template {API.Version} Version + * @param {API.Link} self + * @param {unknown} other + * @returns {other is cid} + */ + static equals (self, other) { + const unknown = + /** @type {{code?:unknown, version?:unknown, multihash?:unknown}} */ ( + other + ) + return ( + unknown && + self.code === unknown.code && + self.version === unknown.version && + Digest.equals(self.multihash, unknown.multihash) + ) } /** - * @param {MultibaseEncoder} [base] + * @param {API.MultibaseEncoder} [base] * @returns {string} */ toString (base) { - const { bytes, version, _baseCache } = this - switch (version) { - case 0: - return toStringV0(bytes, _baseCache, base || base58btc.encoder) - default: - return toStringV1(bytes, _baseCache, base || base32.encoder) - } + return format(this, base) } toJSON () { @@ -138,6 +205,10 @@ export class CID { } } + link () { + return this + } + get [Symbol.toStringTag] () { return 'CID' } @@ -145,7 +216,7 @@ export class CID { // Legacy [Symbol.for('nodejs.util.inspect.custom')] () { - return 'CID(' + this.toString() + ')' + return `CID(${this.toString()})` } // Deprecated @@ -164,11 +235,15 @@ export class CID { } get codec () { - throw new Error('"codec" property is deprecated, use integer "code" property instead') + throw new Error( + '"codec" property is deprecated, use integer "code" property instead' + ) } get buffer () { - throw new Error('Deprecated .buffer property, use .bytes to get Uint8Array instead') + throw new Error( + 'Deprecated .buffer property, use .bytes to get Uint8Array instead' + ) } get multibaseName () { @@ -180,50 +255,58 @@ export class CID { } /** - * Takes any input `value` and returns a `CID` instance if it was - * a `CID` otherwise returns `null`. If `value` is instanceof `CID` - * it will return value back. If `value` is not instance of this CID - * class, but is compatible CID it will return new instance of this - * `CID` class. Otherwise returs null. - * - * This allows two different incompatible versions of CID library to - * co-exist and interop as long as binary interface is compatible. - * @param {any} value - * @returns {CID|null} + * @template {unknown} Data + * @template {number} Format + * @template {number} Alg + * @template {API.Version} Version + * @template {unknown} U + * @param {API.Link|U} input + * @returns {CID|null} */ - static asCID (value) { + static asCID (input) { + const value = /** @type {any} */ (input) if (value instanceof CID) { - // If value is instance of CID then we're all set. + // If value is instance of CID then we're all set. return value } else if (value != null && value.asCID === value) { - // If value isn't instance of this CID class but `this.asCID === this` is - // true it is CID instance coming from a different implementation (diff - // version or duplicate). In that case we rebase it to this `CID` - // implementation so caller is guaranteed to get instance with expected - // API. + // If value isn't instance of this CID class but `this.asCID === this` is + // true it is CID instance coming from a different implementation (diff + // version or duplicate). In that case we rebase it to this `CID` + // implementation so caller is guaranteed to get instance with expected + // API. const { version, code, multihash, bytes } = value - return new CID(version, code, multihash, bytes || encodeCID(version, code, multihash.bytes)) + return new CID( + version, + code, + /** @type {API.MultihashDigest} */ (multihash), + bytes || encodeCID(version, code, multihash.bytes) + ) } else if (value != null && value[cidSymbol] === true) { - // If value is a CID from older implementation that used to be tagged via - // symbol we still rebase it to this `CID` implementation by - // delegating that to a constructor. + // 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. const { version, multihash, code } = value - const digest = Digest.decode(multihash) + const digest = + /** @type {API.MultihashDigest} */ + (Digest.decode(multihash)) return CID.create(version, code, digest) } else { - // Otherwise value is not a CID (or an incompatible version of it) in - // which case we return `null`. + // Otherwise value is not a CID (or an incompatible version of it) in + // which case we return `null`. return null } } /** - * - * @param {CIDVersion} version - Version of the CID - * @param {number} code - Code of the codec content is encoded in. - * @param {MultihashDigest} digest - (Multi)hash of the of the content. - * @returns {CID} - */ + * @template {unknown} Data + * @template {number} Format + * @template {number} Alg + * @template {API.Version} Version + * @param {Version} version - Version of the CID + * @param {Format} code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv + * @param {API.MultihashDigest} digest - (Multi)hash of the of the content. + * @returns {CID} + */ static create (version, code, digest) { if (typeof code !== 'number') { throw new Error('String codecs are no longer supported') @@ -232,7 +315,9 @@ export class CID { switch (version) { case 0: { if (code !== DAG_PB_CODE) { - throw new Error(`Version 0 CID must use dag-pb (code: ${DAG_PB_CODE}) block encoding`) + throw new Error( + `Version 0 CID must use dag-pb (code: ${DAG_PB_CODE}) block encoding` + ) } else { return new CID(version, code, digest, digest.bytes) } @@ -249,19 +334,23 @@ export class CID { /** * Simplified version of `create` for CIDv0. - * @param {MultihashDigest} digest - Multihash. + * @template {unknown} [T=unknown] + * @param {API.MultihashDigest} digest - Multihash. + * @returns {CID} */ static createV0 (digest) { return CID.create(0, DAG_PB_CODE, digest) } /** - * Simplified version of `create` for CIDv1. - * @template {number} Code - * @param {Code} code - Content encoding format code. - * @param {MultihashDigest} digest - Miltihash of the content. - * @returns {CID} - */ + * Simplified version of `create` for CIDv1. + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @param {Code} code - Content encoding format code. + * @param {API.MultihashDigest} digest - Miltihash of the content. + * @returns {CID} + */ static createV1 (code, digest) { return CID.create(1, code, digest) } @@ -272,9 +361,12 @@ export class CID { * * An error will be thrown if the bytes provided do not contain a valid * binary representation of a CID. - * - * @param {Uint8Array} bytes - * @returns {CID} + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @template {API.Version} Ver + * @param {API.ByteView>} bytes + * @returns {CID} */ static decode (bytes) { const [cid, remainder] = CID.decodeFirst(bytes) @@ -293,20 +385,36 @@ export class CID { * will be a zero-length byte array if the provided bytes only contained a * binary CID representation. * - * @param {Uint8Array} bytes - * @returns {[CID, Uint8Array]} + * @template {unknown} T + * @template {number} C + * @template {number} A + * @template {API.Version} V + * @param {API.ByteView>} bytes + * @returns {[CID, Uint8Array]} */ static decodeFirst (bytes) { const specs = CID.inspectBytes(bytes) const prefixSize = specs.size - specs.multihashSize - const multihashBytes = coerce(bytes.subarray(prefixSize, prefixSize + specs.multihashSize)) + const multihashBytes = coerce( + bytes.subarray(prefixSize, prefixSize + specs.multihashSize) + ) if (multihashBytes.byteLength !== specs.multihashSize) { throw new Error('Incorrect length') } - const digestBytes = multihashBytes.subarray(specs.multihashSize - specs.digestSize) - const digest = new Digest.Digest(specs.multihashCode, specs.digestSize, digestBytes, multihashBytes) - const cid = specs.version === 0 ? CID.createV0(digest) : CID.createV1(specs.codec, digest) - return [cid, bytes.subarray(specs.size)] + const digestBytes = multihashBytes.subarray( + specs.multihashSize - specs.digestSize + ) + const digest = new Digest.Digest( + specs.multihashCode, + specs.digestSize, + digestBytes, + multihashBytes + ) + const cid = + specs.version === 0 + ? CID.createV0(/** @type {API.MultihashDigest} */ (digest)) + : CID.createV1(specs.codec, digest) + return [/** @type {CID} */(cid), bytes.subarray(specs.size)] } /** @@ -318,8 +426,12 @@ export class CID { * 10 bytes be made available in the `initialBytes` argument for a complete * inspection. * - * @param {Uint8Array} initialBytes - * @returns {{ version:CIDVersion, codec:number, multihashCode:number, digestSize:number, multihashSize:number, size:number }} + * @template {unknown} T + * @template {number} C + * @template {number} A + * @template {API.Version} V + * @param {API.ByteView>} initialBytes + * @returns {{ version:V, codec:C, multihashCode:A, digestSize:number, multihashSize:number, size:number }} */ static inspectBytes (initialBytes) { let offset = 0 @@ -329,13 +441,14 @@ export class CID { return i } - let version = next() - let codec = DAG_PB_CODE - if (version === 18) { // CIDv0 - version = 0 + let version = /** @type {V} */ (next()) + let codec = /** @type {C} */ (DAG_PB_CODE) + if (/** @type {number} */(version) === 18) { + // CIDv0 + version = /** @type {V} */ (0) offset = 0 - } else if (version === 1) { - codec = next() + } else { + codec = /** @type {C} */ (next()) } if (version !== 0 && version !== 1) { @@ -343,7 +456,7 @@ export class CID { } const prefixSize = offset - const multihashCode = next() // multihash code + const multihashCode = /** @type {A} */ (next()) // multihash code const digestSize = next() // multihash length const size = offset + digestSize const multihashSize = size - prefixSize @@ -358,16 +471,21 @@ export class CID { * a default decoder). * * @template {string} Prefix - * @param {string} source - * @param {MultibaseDecoder} [base] + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @template {API.Version} Ver + * @param {API.ToString, Prefix>} source + * @param {API.MultibaseDecoder} [base] + * @returns {CID} */ static parse (source, base) { const [prefix, bytes] = parseCIDtoBytes(source, base) const cid = CID.decode(bytes) + // Cache string representation to avoid computing it on `this.toString()` - // @ts-ignore - Can't access private - cid._baseCache.set(prefix, source) + baseCache(cid).set(prefix, source) return cid } @@ -375,30 +493,39 @@ export class CID { /** * @template {string} Prefix - * @param {string} source - * @param {MultibaseDecoder} [base] - * @returns {[string, Uint8Array]} + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @template {API.Version} Ver + * @param {API.ToString, Prefix>} source + * @param {API.MultibaseDecoder} [base] + * @returns {[Prefix, API.ByteView>]} */ const parseCIDtoBytes = (source, base) => { switch (source[0]) { // CIDv0 is parsed differently case 'Q': { const decoder = base || base58btc - return [base58btc.prefix, decoder.decode(`${base58btc.prefix}${source}`)] + return [ + /** @type {Prefix} */ (base58btc.prefix), + decoder.decode(`${base58btc.prefix}${source}`) + ] } case base58btc.prefix: { const decoder = base || base58btc - return [base58btc.prefix, decoder.decode(source)] + return [/** @type {Prefix} */(base58btc.prefix), decoder.decode(source)] } case base32.prefix: { const decoder = base || base32 - return [base32.prefix, decoder.decode(source)] + return [/** @type {Prefix} */(base32.prefix), decoder.decode(source)] } default: { if (base == null) { - throw Error('To parse non base32 or base58btc encoded CID multibase decoder must be provided') + throw Error( + 'To parse non base32 or base58btc encoded CID multibase decoder must be provided' + ) } - return [source[0], base.decode(source)] + return [/** @type {Prefix} */(source[0]), base.decode(source)] } } } @@ -407,7 +534,7 @@ const parseCIDtoBytes = (source, base) => { * * @param {Uint8Array} bytes * @param {Map} cache - * @param {MultibaseEncoder<'z'>} base + * @param {API.MultibaseEncoder<'z'>} base */ const toStringV0 = (bytes, cache, base) => { const { prefix } = base @@ -429,7 +556,7 @@ const toStringV0 = (bytes, cache, base) => { * @template {string} Prefix * @param {Uint8Array} bytes * @param {Map} cache - * @param {MultibaseEncoder} base + * @param {API.MultibaseEncoder} base */ const toStringV1 = (bytes, cache, base) => { const { prefix } = base @@ -447,7 +574,7 @@ const DAG_PB_CODE = 0x70 const SHA_256_CODE = 0x12 /** - * @param {CIDVersion} version + * @param {API.Version} version * @param {number} code * @param {Uint8Array} multihash * @returns {Uint8Array} @@ -478,14 +605,13 @@ const version = '0.0.0-dev' const deprecate = (range, message) => { if (range.test(version)) { console.warn(message) - /* c8 ignore next 3 */ + /* c8 ignore next 3 */ } else { throw new Error(message) } } -const IS_CID_DEPRECATION = -`CID.isCID(v) is deprecated and will be removed in the next major release. +const IS_CID_DEPRECATION = `CID.isCID(v) is deprecated and will be removed in the next major release. Following code pattern: if (CID.isCID(value)) { diff --git a/src/codecs/interface.js b/src/codecs/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/codecs/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/codecs/interface.ts b/src/codecs/interface.ts index dc8527c1..af2c9762 100644 --- a/src/codecs/interface.ts +++ b/src/codecs/interface.ts @@ -1,3 +1,5 @@ +import type { ByteView } from '../block/interface' + /** * IPLD encoder part of the codec. */ @@ -20,9 +22,4 @@ export interface BlockDecoder { */ export interface BlockCodec extends BlockEncoder, BlockDecoder {} -// This just a hack to retain type information about the data that -// is encoded `T` Because it's a union `data` field is never going -// to be usable anyway. -export type ByteView = - | Uint8Array - | Uint8Array & { data: T } +export type { ByteView } diff --git a/src/hashes/digest.js b/src/hashes/digest.js index 206e891b..7eebbb65 100644 --- a/src/hashes/digest.js +++ b/src/hashes/digest.js @@ -25,7 +25,7 @@ export const create = (code, digest) => { * @param {Uint8Array} multihash * @returns {MultihashDigest} */ -export const decode = (multihash) => { +export const decode = multihash => { const bytes = coerce(multihash) const [code, sizeOffset] = varint.decode(bytes) const [size, digestOffset] = varint.decode(bytes.subarray(sizeOffset)) @@ -40,14 +40,23 @@ export const decode = (multihash) => { /** * @param {MultihashDigest} a - * @param {MultihashDigest} b - * @returns {boolean} + * @param {unknown} b + * @returns {b is MultihashDigest} */ export const equals = (a, b) => { if (a === b) { return true } else { - return a.code === b.code && a.size === b.size && equalBytes(a.bytes, b.bytes) + const data = /** @type {{code?:unknown, size?:unknown, bytes?:unknown}} */ ( + b + ) + + return ( + a.code === data.code && + a.size === data.size && + data.bytes instanceof Uint8Array && + equalBytes(a.bytes, data.bytes) + ) } } diff --git a/src/hashes/interface.js b/src/hashes/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/hashes/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/hashes/interface.ts b/src/hashes/interface.ts index 09afdfe0..a8003aaa 100644 --- a/src/hashes/interface.ts +++ b/src/hashes/interface.ts @@ -44,7 +44,7 @@ export interface MultihashHasher { * * @param {Uint8Array} input */ - digest(input: Uint8Array): Promise | MultihashDigest + digest(input: Uint8Array): Promise> | MultihashDigest /** * Name of the multihash @@ -68,5 +68,5 @@ export interface MultihashHasher { * impractical e.g. implementation of Hash Array Mapped Trie (HAMT). */ export interface SyncMultihashHasher extends MultihashHasher { - digest(input: Uint8Array): MultihashDigest + digest(input: Uint8Array): MultihashDigest } diff --git a/src/index.js b/src/index.js index aed6be18..377a9771 100644 --- a/src/index.js +++ b/src/index.js @@ -3,5 +3,7 @@ import * as varint from './varint.js' import * as bytes from './bytes.js' import * as hasher from './hashes/hasher.js' import * as digest from './hashes/digest.js' +// This way TS will also expose all the types from module +export * from './interface.js' export { CID, hasher, digest, varint, bytes } diff --git a/src/interface.js b/src/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 00000000..5d082780 --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,5 @@ +export * from './bases/interface' +export * from './hashes/interface' +export * from './codecs/interface' +export * from './link/interface' +export * from './block/interface' diff --git a/src/link.js b/src/link.js new file mode 100644 index 00000000..4d7bc990 --- /dev/null +++ b/src/link.js @@ -0,0 +1,73 @@ +// Linter can see that API is used in types. +// eslint-disable-next-line +import * as API from "./link/interface.js" +import { CID, format } from './cid.js' +// This way TS will also expose all the types from module +export * from './link/interface.js' + +const DAG_PB_CODE = 0x70 +// eslint-disable-next-line +const SHA_256_CODE = 0x12 + +/** + * Simplified version of `create` for CIDv0. + * @param {API.MultihashDigest} digest - Multihash. + * @returns {API.LegacyLink} + */ +export const createLegacy = digest => CID.create(0, DAG_PB_CODE, digest) + +/** + * Simplified version of `create` for CIDv1. + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @param {Code} code - Content encoding format code. + * @param {API.MultihashDigest} digest - Miltihash of the content. + * @returns {API.Link} + */ +export const create = (code, digest) => CID.create(1, code, digest) + +/** + * Type predicate returns true if value is the link. + * + * @template {API.Link} L + * @param {unknown|L} value + * @returns {value is L & CID} + */ +export const isLink = value => + value != null && /** @type {{asCID: unknown}} */ (value).asCID === value + +/** + * Takes cid in a string representation and creates an instance. If `base` + * decoder is not provided will use a default from the configuration. It will + * throw an error if encoding of the CID is not compatible with supplied (or + * a default decoder). + * + * @template {string} Prefix + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @template {API.Version} Ver + * @param {API.ToString, Prefix>} source + * @param {API.MultibaseDecoder} [base] + * @returns {API.Link} + */ +export const parse = (source, base) => CID.parse(source, base) + +export { format } + +/** + * Decoded a CID from its binary representation. The byte array must contain + * only the CID with no additional bytes. + * + * An error will be thrown if the bytes provided do not contain a valid + * binary representation of a CID. + * + * @template {unknown} Data + * @template {number} Code + * @template {number} Alg + * @template {API.Version} Ver + * @param {API.ByteView>} bytes + * @returns {API.Link} + */ +export const decode = bytes => CID.decode(bytes) diff --git a/src/link/interface.js b/src/link/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/link/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/link/interface.ts b/src/link/interface.ts new file mode 100644 index 00000000..60cb0263 --- /dev/null +++ b/src/link/interface.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable no-use-before-define */ +import type { MultihashDigest } from '../hashes/interface' +import type { MultibaseEncoder, MultibaseDecoder, Multibase } from '../bases/interface' +import type { Phantom, ByteView } from '../block/interface' + + +export type { MultihashDigest, MultibaseEncoder, MultibaseDecoder } +export type Version = 0 | 1 + +export type DAG_PB = 0x70 +export type SHA_256 = 0x12 + +/** + * Represents an IPLD link to a specific data of type `T`. + * + * @template T - Logical type of the data being linked to. + * @template C - multicodec code corresponding to a codec linked data is encoded with + * @template A - multicodec code corresponding to the hashing algorithm of the CID + * @template V - CID version + */ +export interface Link< + Data extends unknown = unknown, + Format extends number = number, + Alg extends number = number, + V extends Version = 1 + > extends Phantom { + readonly version: V + readonly code: Format + readonly multihash: MultihashDigest + + readonly byteOffset: number + readonly byteLength: number + readonly bytes: ByteView> + + + equals(other: unknown): other is Link + + toString(base?: MultibaseEncoder): ToString, Prefix> + toJSON(): { version: V, code: Format, hash: Uint8Array } + link(): Link + + toV1(): Link +} + +export interface LegacyLink extends Link { +} + +export type UnknownLink = + | LegacyLink + | Link + +export type ToString = Multibase & Phantom + +export type { ByteView } diff --git a/src/traversal.js b/src/traversal.js index a13d1d5c..2e35c020 100644 --- a/src/traversal.js +++ b/src/traversal.js @@ -1,19 +1,24 @@ import { base58btc } from './bases/base58.js' /** - * @typedef {import('./cid.js').CID} CID + * @template [C=number] - multicodec code corresponding to codec used to encode the block + * @template [A=number] - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template [V=0|1] - CID version + * @typedef {import('./cid').CID} CID */ /** - * @template T - * @typedef {import('./block.js').Block} Block + * @template [T=unknown] - Logical type of the data encoded in the block + * @template [C=number] - multicodec code corresponding to codec used to encode the block + * @template [A=number] - multicodec code corresponding to the hashing algorithm used in CID creation. + * @template [V=0|1] - CID version + * @typedef {import('./block/interface').BlockView} BlockView */ /** - * @template T * @param {Object} options * @param {CID} options.cid - * @param {(cid: CID) => Promise|null>} options.load + * @param {(cid: CID) => Promise} options.load * @param {Set} [options.seen] */ const walk = async ({ cid, load, seen }) => { diff --git a/test/test-block.js b/test/test-block.js index c744e72f..8f6d6fa9 100644 --- a/test/test-block.js +++ b/test/test-block.js @@ -60,9 +60,9 @@ describe('block', () => { it('get', () => { let ret = block.get('link/test') assert.deepStrictEqual(ret.remaining, 'test') - assert.deepStrictEqual(ret.value.toString(), link.toString()) + assert.deepStrictEqual(String(ret.value), link.toString()) ret = block.get('nope') - // @ts-expect-error - 'string' is not expected + assert.deepStrictEqual(ret, { value: 'skip' }) }) diff --git a/test/test-bytes.js b/test/test-bytes.js index a764ded6..c420fa9b 100644 --- a/test/test-bytes.js +++ b/test/test-bytes.js @@ -1,5 +1,5 @@ /* globals describe, it */ -import * as bytes from '../src/bytes.js' +import * as bytes from 'multiformats/bytes' import { assert } from 'chai' describe('bytes', () => { diff --git a/test/test-cid.js b/test/test-cid.js index 905d4644..9bc19594 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -1,7 +1,7 @@ /* globals describe, it */ import OLDCID from 'cids' -import { fromHex, toHex, equals } from '../src/bytes.js' +import { fromHex, toHex, equals } from 'multiformats/bytes' import { varint, CID } from 'multiformats' import { base58btc } from 'multiformats/bases/base58' import { base32 } from 'multiformats/bases/base32' @@ -10,6 +10,9 @@ import { sha256, sha512 } from 'multiformats/hashes/sha2' import invalidMultihash from './fixtures/invalid-multihash.js' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' +// Linter can see that API is used in types. +// eslint-disable-next-line +import * as API from 'multiformats' chai.use(chaiAsPromised) const { assert } = chai @@ -39,6 +42,16 @@ describe('CID', () => { assert.deepStrictEqual(cid.toString(), base58btc.baseEncode(hash.bytes)) }) + it('CID.createV0', async () => { + const hash = await sha256.digest(textEncoder.encode('abc')) + const cid = CID.createV0(hash) + + assert.deepStrictEqual(cid.code, 112) + assert.deepStrictEqual(cid.version, 0) + assert.deepStrictEqual(cid.multihash, hash) + assert.deepStrictEqual(cid.toString(), base58btc.baseEncode(hash.bytes)) + }) + it('create from multihash', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) @@ -47,14 +60,20 @@ describe('CID', () => { assert.deepStrictEqual(cid.code, 112) assert.deepStrictEqual(cid.version, 0) assert.deepStrictEqual(cid.multihash.digest, hash.digest) - assert.deepStrictEqual({ ...cid.multihash, digest: null }, { ...hash, digest: null }) + assert.deepStrictEqual( + { ...cid.multihash, digest: null }, + { ...hash, digest: null } + ) cid.toString() assert.deepStrictEqual(cid.toString(), base58btc.baseEncode(hash.bytes)) }) it('throws on invalid BS58Str multihash ', async () => { const msg = 'Non-base58btc character' - assert.throws(() => CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), msg) + assert.throws( + () => CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zIII'), + msg + ) }) it('throws on trying to create a CIDv0 with a codec other than dag-pb', async () => { @@ -85,41 +104,59 @@ describe('CID', () => { const bytes = cid.bytes assert.ok(bytes) const str = toHex(bytes) - assert.deepStrictEqual(str, '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + assert.deepStrictEqual( + str, + '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) }) it('should construct from an old CID', () => { const cidStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' const oldCid = CID.parse(cidStr) - const newCid = CID.asCID(oldCid) - assert.deepStrictEqual(/** @type {CID} */(newCid).toString(), cidStr) + const newCid = /** @type {CID} */ (CID.asCID(oldCid)) + assert.deepStrictEqual(newCid.toString(), cidStr) }) it('inspect bytes', () => { - const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + const byts = fromHex( + '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) const inspected = CID.inspectBytes(byts.subarray(0, 10)) // should only need the first few bytes - assert.deepStrictEqual({ - version: 0, - codec: 0x70, - multihashCode: 0x12, - multihashSize: 34, - digestSize: 32, - size: 34 - }, inspected) + assert.deepStrictEqual( + { + version: 0, + codec: 0x70, + multihashCode: 0x12, + multihashSize: 34, + digestSize: 32, + size: 34 + }, + inspected + ) }) describe('decodeFirst', () => { it('no remainder', () => { - const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + const byts = fromHex( + '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) const [cid, remainder] = CID.decodeFirst(byts) - assert.deepStrictEqual(cid.toString(), 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY') + assert.deepStrictEqual( + cid.toString(), + 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY' + ) assert.deepStrictEqual(remainder.byteLength, 0) }) it('remainder', () => { - const byts = fromHex('1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405') + const byts = fromHex( + '1220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405' + ) const [cid, remainder] = CID.decodeFirst(byts) - assert.deepStrictEqual(cid.toString(), 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY') + assert.deepStrictEqual( + cid.toString(), + 'QmatYkNGZnELf8cAGdyJpUca2PyY4szai3RHyyWofNY1pY' + ) assert.deepStrictEqual(toHex(remainder), '0102030405') }) }) @@ -136,8 +173,11 @@ describe('CID', () => { }) it('handles CID (no multibase)', () => { - const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' - const cidBuf = fromHex('017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5') + const cidStr = + 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' + const cidBuf = fromHex( + '017012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5' + ) const cid = CID.decode(cidBuf) assert.deepStrictEqual(cid.code, 112) assert.deepStrictEqual(cid.version, 1) @@ -152,6 +192,15 @@ describe('CID', () => { equalDigest(cid.multihash, hash) }) + it('CID.createV1', async () => { + const hash = await sha256.digest(textEncoder.encode('abc')) + const cid = CID.createV1(0x71, hash) + + assert.deepStrictEqual(cid.code, 0x71) + assert.deepStrictEqual(cid.version, 1) + equalDigest(cid.multihash, hash) + }) + it('can roundtrip through cid.toString()', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid1 = CID.create(1, 0x71, hash) @@ -162,7 +211,10 @@ describe('CID', () => { assert.deepStrictEqual(cid1.multihash.digest, cid2.multihash.digest) assert.deepStrictEqual(cid1.multihash.bytes, cid2.multihash.bytes) const clear = { digest: null, bytes: null } - assert.deepStrictEqual({ ...cid1.multihash, ...clear }, { ...cid2.multihash, ...clear }) + assert.deepStrictEqual( + { ...cid1.multihash, ...clear }, + { ...cid2.multihash, ...clear } + ) }) /* TODO: after i have a keccak hash for the new interface @@ -189,14 +241,23 @@ describe('CID', () => { const bytes = cid.bytes assert.ok(bytes) const str = toHex(bytes) - assert.deepStrictEqual(str, '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + assert.deepStrictEqual( + str, + '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) }) it('should construct from an old CID without a multibaseName', () => { - const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' + const cidStr = + 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' const oldCid = CID.parse(cidStr) - const newCid = CID.asCID(oldCid) - assert.deepStrictEqual(/** @type {CID} */(newCid).toString(), cidStr) + const newCid = /** @type {CID} */ (CID.asCID(oldCid)) + assert.deepStrictEqual(newCid.toString(), cidStr) + }) + + it('.link() should return this CID', () => { + const cid = CID.parse('bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u') + assert.equal(cid, cid.link()) }) }) @@ -208,11 +269,17 @@ describe('CID', () => { it('.equals v0 to v0', () => { const cid1 = CID.parse(h1) assert.deepStrictEqual(cid1.equals(CID.parse(h1)), true) - assert.deepStrictEqual(cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), true) + assert.deepStrictEqual( + cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), + true + ) const cid2 = CID.parse(h2) assert.deepStrictEqual(cid1.equals(CID.parse(h2)), false) - assert.deepStrictEqual(cid1.equals(CID.create(cid2.version, cid2.code, cid2.multihash)), false) + assert.deepStrictEqual( + cid1.equals(CID.create(cid2.version, cid2.code, cid2.multihash)), + false + ) }) it('.equals v0 to v1 and vice versa', () => { @@ -230,7 +297,10 @@ describe('CID', () => { const cid1 = CID.parse(h3) assert.deepStrictEqual(cid1.equals(CID.parse(h3)), true) - assert.deepStrictEqual(cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), true) + assert.deepStrictEqual( + cid1.equals(CID.create(cid1.version, cid1.code, cid1.multihash)), + true + ) }) it('.isCid', () => { @@ -247,8 +317,6 @@ describe('CID', () => { it('works with deepEquals', () => { const ch1 = CID.parse(h1) - // @ts-expect-error - '_baseCache' is private - ch1._baseCache.set('herp', 'derp') assert.deepStrictEqual(ch1, CID.parse(h1)) assert.notDeepEqual(ch1, CID.parse(h2)) }) @@ -271,19 +339,22 @@ describe('CID', () => { ] for (const i of decode) { - const name = `CID.decode(textEncoder.encode(${JSON.stringify(i.toString())}))` + const name = `CID.decode(textEncoder.encode(${JSON.stringify( + i.toString() + )}))` it(name, async () => assert.throws(() => CID.decode(i))) } const create = [ - ...[...parse, ...decode].map(i => [0, 112, i]), - ...[...parse, ...decode].map(i => [1, 112, i]), + ...[...parse, ...decode].map((i) => [0, 112, i]), + ...[...parse, ...decode].map((i) => [1, 112, i]), [18, 112, 'QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L'] ] for (const [version, code, hash] of create) { const form = JSON.stringify(hash.toString()) - const mh = hash instanceof Uint8Array ? `textEncoder.encode(${form})` : form + const mh = + hash instanceof Uint8Array ? `textEncoder.encode(${form})` : form const name = `CID.create(${version}, ${code}, ${mh})` // @ts-expect-error - version issn't always 0|1 it(name, async () => assert.throws(() => CID.create(version, code, hash))) @@ -310,26 +381,32 @@ describe('CID', () => { describe('conversion v0 <-> v1', () => { it('should convert v0 to v1', async () => { const hash = await sha256.digest(textEncoder.encode(`TEST${Date.now()}`)) - const cid = (CID.create(0, 112, hash)).toV1() + const cid = CID.create(0, 112, hash).toV1() assert.deepStrictEqual(cid.version, 1) }) it('should convert v1 to v0', async () => { const hash = await sha256.digest(textEncoder.encode(`TEST${Date.now()}`)) - const cid = (CID.create(1, 112, hash)).toV0() + const cid = CID.create(1, 112, hash).toV0() assert.deepStrictEqual(cid.version, 0) }) it('should not convert v1 to v0 if not dag-pb codec', async () => { const hash = await sha256.digest(textEncoder.encode(`TEST${Date.now()}`)) const cid = CID.create(1, 0x71, hash) - assert.throws(() => cid.toV0(), 'Cannot convert a non dag-pb CID to CIDv0') + assert.throws( + () => cid.toV0(), + 'Cannot convert a non dag-pb CID to CIDv0' + ) }) it('should not convert v1 to v0 if not sha2-256 multihash', async () => { const hash = await sha512.digest(textEncoder.encode(`TEST${Date.now()}`)) const cid = CID.create(1, 112, hash) - assert.throws(() => cid.toV0(), 'Cannot convert non sha2-256 multihash CID to CIDv0') + assert.throws( + () => cid.toV0(), + 'Cannot convert non sha2-256 multihash CID to CIDv0' + ) }) it('should return assert.deepStrictEqual instance when converting v1 to v1', async () => { @@ -344,6 +421,31 @@ describe('CID', () => { const cid = CID.create(0, 112, hash) assert.deepStrictEqual(cid.toV0() === cid, true) }) + + it('should fail to convert unknown version', async () => { + const hash = await sha256.digest(textEncoder.encode(`TEST${Date.now()}`)) + const cid = CID.create(0, 112, hash) + const cid1 = Object.assign(Object.create(Object.getPrototypeOf(cid)), { + ...cid + }) + const cid2 = Object.assign(Object.create(Object.getPrototypeOf(cid)), { + ...cid, + version: 3 + }) + + assert.deepStrictEqual(cid1.toV0().version, 0) + assert.deepStrictEqual(cid1.toV1().version, 1) + assert.equal(cid2.version, 3) + + assert.throws( + () => cid2.toV1(), + /Can not convert CID version 3 to version 1/ + ) + assert.throws( + () => cid2.toV0(), + /Can not convert CID version 3 to version 0/ + ) + }) }) describe('caching', () => { @@ -358,30 +460,63 @@ describe('CID', () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.size, 0) - - assert.deepStrictEqual(cid.toString(base64), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') - - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.get(base64.prefix), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') - - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.has(base32.prefix), false) + const b32 = { + ...base32, + callCount: 0, + /** + * @param {Uint8Array} bytes + */ + encode (bytes) { + this.callCount += 1 + return base32.encode(bytes) + '!' + } + } - const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' - assert.deepStrictEqual(cid.toString(), base32String) + const b64 = { + ...base64, + callCount: 0, + /** + * @param {Uint8Array} bytes + */ + encode (bytes) { + this.callCount += 1 + return base64.encode(bytes) + } + } - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.get(base32.prefix), base32String) - assert.deepStrictEqual(cid.toString(base64), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') + const base32String = + 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' + assert.deepEqual(cid.toString(b32), `${base32String}!`) + assert.deepEqual(b32.callCount, 1) + assert.deepEqual(cid.toString(), `${base32String}!`) + assert.deepEqual(b32.callCount, 1) + + assert.deepStrictEqual( + cid.toString(b64), + 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt' + ) + assert.equal(b64.callCount, 1) + assert.deepStrictEqual( + cid.toString(b64), + 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt' + ) + assert.equal(b64.callCount, 1) }) it('should cache string representation when constructed with one', () => { - const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' + const base32String = + 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' const cid = CID.parse(base32String) - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.get(base32.prefix), base32String) + + assert.deepStrictEqual( + cid.toString({ + ...base32, + encode () { + throw Error('Should not call decode') + } + }), + base32String + ) }) }) @@ -390,7 +525,10 @@ describe('CID', () => { const cid = CID.create(1, 112, hash) const json = cid.toJSON() - assert.deepStrictEqual({ ...json, hash: null }, { code: 112, version: 1, hash: null }) + assert.deepStrictEqual( + { ...json, hash: null }, + { code: 112, version: 1, hash: null } + ) assert.ok(equals(json.hash, hash.bytes)) }) @@ -429,7 +567,7 @@ describe('CID', () => { // @ts-expect-error - no such method assert.strictEqual(typeof incompatibleCID.toV0, 'undefined') - const cid1 = /** @type {CID} */(CID.asCID(incompatibleCID)) + const cid1 = /** @type {CID} */ (CID.asCID(incompatibleCID)) assert.ok(cid1 instanceof CID) assert.strictEqual(cid1.code, code) assert.strictEqual(cid1.version, version) @@ -450,7 +588,9 @@ describe('CID', () => { const cid4 = CID.asCID(cid3) assert.strictEqual(cid3, cid4) - const cid5 = /** @type {CID} */(CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes)))) + const cid5 = /** @type {CID} */ ( + CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) + ) assert.ok(cid5 instanceof CID) assert.strictEqual(cid5.version, 1) assert.ok(equals(cid5.multihash.bytes, hash.bytes)) @@ -458,8 +598,8 @@ describe('CID', () => { }) /** - * @param {CID} x - * @param {CID} y + * @param {API.CID} x + * @param {API.CID} y */ const digestsame = (x, y) => { // @ts-ignore - not sure what this supposed to be @@ -496,7 +636,7 @@ describe('CID', () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - const parsed = /** @type {CID} */(CID.parse(cid.toString(base58btc))) + const parsed = CID.parse(cid.toString(base58btc)) digestsame(cid, parsed) }) @@ -511,7 +651,8 @@ describe('CID', () => { it('fails to parse base64 encoded CIDv1', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - const msg = 'To parse non base32 or base58btc encoded CID multibase decoder must be provided' + const msg = + 'To parse non base32 or base58btc encoded CID multibase decoder must be provided' assert.throws(() => CID.parse(cid.toString(base64)), msg) }) @@ -526,29 +667,44 @@ describe('CID', () => { }) it('inspect bytes', () => { - const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + const byts = fromHex( + '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) const inspected = CID.inspectBytes(byts.subarray(0, 10)) // should only need the first few bytes - assert.deepStrictEqual({ - version: 1, - codec: 0x71, - multihashCode: 0x12, - multihashSize: 34, - digestSize: 32, - size: 36 - }, inspected) + assert.deepStrictEqual( + { + version: 1, + codec: 0x71, + multihashCode: 0x12, + multihashSize: 34, + digestSize: 32, + size: 36 + }, + inspected + ) describe('decodeFirst', () => { it('no remainder', () => { - const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') + const byts = fromHex( + '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ) const [cid, remainder] = CID.decodeFirst(byts) - assert.deepStrictEqual(cid.toString(), 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu') + assert.deepStrictEqual( + cid.toString(), + 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' + ) assert.deepStrictEqual(remainder.byteLength, 0) }) it('remainder', () => { - const byts = fromHex('01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405') + const byts = fromHex( + '01711220ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad0102030405' + ) const [cid, remainder] = CID.decodeFirst(byts) - assert.deepStrictEqual(cid.toString(), 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu') + assert.deepStrictEqual( + cid.toString(), + 'bafyreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' + ) assert.deepStrictEqual(toHex(remainder), '0102030405') }) }) @@ -556,7 +712,9 @@ describe('CID', () => { it('new CID from old CID', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) - const cid = /** @type {CID} */ (CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes)))) + const cid = /** @type {CID} */ ( + CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) + ) assert.deepStrictEqual(cid.version, 1) equalDigest(cid.multihash, hash) @@ -566,25 +724,41 @@ describe('CID', () => { it('util.inspect', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - // @ts-expect-error - no such method is known - assert.deepStrictEqual(typeof cid[Symbol.for('nodejs.util.inspect.custom')], 'function') - // @ts-expect-error - no such method is known - assert.deepStrictEqual(cid[Symbol.for('nodejs.util.inspect.custom')](), 'CID(bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu)') + assert.deepStrictEqual( + // @ts-expect-error - no such method is known + typeof cid[Symbol.for('nodejs.util.inspect.custom')], + 'function' + ) + assert.deepStrictEqual( + // @ts-expect-error - no such method is known + cid[Symbol.for('nodejs.util.inspect.custom')](), + 'CID(bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu)' + ) }) describe('deprecations', async () => { it('codec', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - assert.throws(() => cid.codec, '"codec" property is deprecated, use integer "code" property instead') - // @ts-expect-error - 'string' is not assignable to parameter of type 'number' - assert.throws(() => CID.create(1, 'dag-pb', hash), 'String codecs are no longer supported') + + assert.throws( + () => cid.codec, + '"codec" property is deprecated, use integer "code" property instead' + ) + assert.throws( + // @ts-expect-error - 'string' is not assignable to parameter of type 'number' + () => CID.create(1, 'dag-pb', hash), + 'String codecs are no longer supported' + ) }) it('multibaseName', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - assert.throws(() => cid.multibaseName, '"multibaseName" property is deprecated') + assert.throws( + () => cid.multibaseName, + '"multibaseName" property is deprecated' + ) }) it('prefix', async () => { @@ -596,8 +770,11 @@ describe('CID', () => { it('toBaseEncodedString()', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - // @ts-expect-error - deprecated - assert.throws(() => cid.toBaseEncodedString(), 'Deprecated, use .toString()') + assert.throws( + // @ts-expect-error - deprecated + () => cid.toBaseEncodedString(), + 'Deprecated, use .toString()' + ) }) }) @@ -609,6 +786,9 @@ describe('CID', () => { it('buffer', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) const cid = CID.create(1, 112, hash) - assert.throws(() => cid.buffer, 'Deprecated .buffer property, use .bytes to get Uint8Array instead') + assert.throws( + () => cid.buffer, + 'Deprecated .buffer property, use .bytes to get Uint8Array instead' + ) }) }) diff --git a/test/test-link.js b/test/test-link.js new file mode 100644 index 00000000..bb555526 --- /dev/null +++ b/test/test-link.js @@ -0,0 +1,121 @@ +/* globals describe, it */ + +import * as Link from 'multiformats/link' +import { CID } from 'multiformats' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { sha256 } from 'multiformats/hashes/sha2' + +chai.use(chaiAsPromised) +const { assert } = chai +const utf8 = new TextEncoder() + +const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' +const h4 = 'bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae' +const CBOR = 0x71 +// eslint-disable-next-line +const SHA256 = sha256.code + +const sh1 = /** @type {Link.MultihashDigest} */ ( + Link.parse(h4).multihash +) + +describe('Link', () => { + it('isLink', () => { + assert.equal(Link.isLink(0), false) + assert.equal(Link.isLink(false), false) + assert.equal(CID.isCID(CID.parse(h1)), true) + assert.equal(CID.isCID(CID.parse(h1).toV0()), true) + + assert.equal(CID.isCID(CID.parse(h1).toV1()), true) + }) + + describe('create', () => { + it('create v1', async () => { + const hash = await sha256.digest(utf8.encode('abc')) + const link = Link.create(0x71, hash) + /** @type {0x71} */ + const code = link.code + assert.deepStrictEqual(code, 0x71) + + /** @type {1} */ + const version = link.version + assert.deepEqual(version, 1) + + /** @type {Link.MultihashDigest}> */ + const multihash = link.multihash + assert.deepStrictEqual(multihash, hash) + }) + + it('create v0', async () => { + const hash = await sha256.digest(utf8.encode('abc')) + const link = Link.createLegacy(hash) + + /** @type {0x70} */ + const code = link.code + assert.deepStrictEqual(code, 0x70) + + /** @type {0} */ + const version = link.version + assert.deepEqual(version, 0) + + /** @type {Link.MultihashDigest}> */ + const multihash = link.multihash + assert.deepStrictEqual(multihash, hash) + }) + }) + + describe('parse', () => { + it('can parse any string', () => { + const link = Link.parse(h1) + + /** @type {Link.Link} */ + // @ts-expect-error - types can not be inferred + const t1 = link + assert.ok(t1) + + // it is possible to manually cast + const t2 = /** @type {Link.LegacyLink} */ (link) + assert.ok(t2) + }) + + it('parse retains type info', () => { + const original = Link.create(CBOR, sh1) + const source = Link.format(original) + const link = Link.parse(source) + assert.equal(original.equals(link), true, 'format -> parse roundtrips') + + // ensure that type info is retained + /** @type {Link.Link} */ + const t1 = link + assert.ok(t1) + + // ensurate that you can't cast incorrectly + const t2 = + // @ts-expect-error - version is 1 not 0 + /** @type {Link.Link} */ (link) + assert.ok(t2) + }) + }) +}) + +describe('decode', () => { + it('decode', async () => { + const hash = await sha256.digest(utf8.encode('abc')) + const { bytes } = Link.create(0x71, hash) + + const link = Link.decode(bytes) + + /** @type {0x71} */ + const code = link.code + assert.deepStrictEqual(code, 0x71) + + /** @type {1} */ + const version = link.version + assert.deepEqual(version, 1) + + /** @type {Link.MultihashDigest}> */ + const multihash = link.multihash + assert.deepStrictEqual(multihash, hash) + }) +}) diff --git a/test/test-multibase-spec.js b/test/test-multibase-spec.js index d49040a6..e06d3a9c 100644 --- a/test/test-multibase-spec.js +++ b/test/test-multibase-spec.js @@ -2,7 +2,7 @@ 'use strict' import { bases } from 'multiformats/basics' -import { fromString } from '../src/bytes.js' +import { fromString } from 'multiformats/bytes' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' diff --git a/test/test-multibase.js b/test/test-multibase.js index db4d37b0..1c63ce1c 100644 --- a/test/test-multibase.js +++ b/test/test-multibase.js @@ -1,5 +1,5 @@ /* globals describe, it */ -import * as bytes from '../src/bytes.js' +import * as bytes from 'multiformats/bytes' import * as b2 from 'multiformats/bases/base2' import * as b8 from 'multiformats/bases/base8' import * as b10 from 'multiformats/bases/base10' diff --git a/test/test-multicodec.js b/test/test-multicodec.js index b12d5d17..ee449de8 100644 --- a/test/test-multicodec.js +++ b/test/test-multicodec.js @@ -1,5 +1,5 @@ /* globals describe, it */ -import * as bytes from '../src/bytes.js' +import * as bytes from 'multiformats/bytes' import * as raw from 'multiformats/codecs/raw' import * as json from 'multiformats/codecs/json' import chai from 'chai' diff --git a/test/test-multihash.js b/test/test-multihash.js index d65f392d..85d48245 100644 --- a/test/test-multihash.js +++ b/test/test-multihash.js @@ -1,5 +1,5 @@ /* globals describe, it */ -import { fromHex, fromString } from '../src/bytes.js' +import { fromHex, fromString } from 'multiformats/bytes' import { hash as slSha256 } from '@stablelib/sha256' import { hash as slSha512 } from '@stablelib/sha512' import valid from './fixtures/valid-multihash.js' diff --git a/test/test-traversal.js b/test/test-traversal.js index b7b191f0..dc411a01 100644 --- a/test/test-traversal.js +++ b/test/test-traversal.js @@ -5,7 +5,7 @@ import { sha256 as hasher } from 'multiformats/hashes/sha2' import * as main from 'multiformats/block' import { walk } from 'multiformats/traversal' import { assert } from 'chai' -import { fromString } from '../src/bytes.js' +import { fromString } from 'multiformats/bytes' const { createLink, createNode } = dagPB diff --git a/test/tsconfig.json b/test/tsconfig.json deleted file mode 100644 index 8068304b..00000000 --- a/test/tsconfig.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "references": [ - { - "path": "../" - } - ], - "compilerOptions": { - "composite": true, - "allowJs": true, - "checkJs": true, - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": false, - "noImplicitAny": true, - "noImplicitThis": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "strictFunctionTypes": false, - "strictNullChecks": true, - "strictPropertyInitialization": true, - "strictBindCallApply": true, - "strict": true, - "alwaysStrict": true, - "esModuleInterop": true, - "target": "ES2020", - "moduleResolution": "node", - "declaration": true, - "declarationMap": true, - "skipLibCheck": true, - "stripInternal": true, - "resolveJsonModule": true, - "noEmit": true, - "paths": { - "multiformats": [ - "../types/src/index" - ], - "multiformats/*": [ - "../types/src/*" - ] - } - }, - "include": [ - "." - ], - "exclude": [ - "ts-use/", - "node_modules" - ] -} diff --git a/tsconfig.json b/tsconfig.json index 43a6feb3..f85de4e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,13 +26,30 @@ "resolveJsonModule": true, "emitDeclarationOnly": true, "baseUrl": ".", - "composite": true + "composite": true, + "paths": { + "multiformats": [ + "./src/index.js" + ], + "multiformats/interface": [ + "./src/interface" + ], + "multiformats/hashes/interface": [ + "./src/hashes/interface" + ], + "multiformats/*": [ + "./src/*.js" + ], + + } }, "include": [ - "src" + "src", + "test" ], "exclude": [ "vendor", + "test/ts-use", "node_modules" ], "compileOnSave": false diff --git a/vendor/base-x.d.ts b/vendor/base-x.d.ts index 789d1e8f..a8665dc9 100644 --- a/vendor/base-x.d.ts +++ b/vendor/base-x.d.ts @@ -1,9 +1,8 @@ -declare function base(ALPHABET: string, name: string): base.BaseConverter; -export = base; -declare namespace base { - interface BaseConverter { - encode(buffer: Uint8Array | number[]): string; - decodeUnsafe(string: string): Uint8Array | undefined; - decode(string: string): Uint8Array; - } + +export interface BaseConverter { + encode(buffer: Uint8Array | number[]): string; + decodeUnsafe(string: string): Uint8Array | undefined; + decode(string: string): Uint8Array; } + +export default function base(ALPHABET: string, name: string): BaseConverter diff --git a/vendor/varint.d.ts b/vendor/varint.d.ts index 11e2e2d7..f5a8a021 100644 --- a/vendor/varint.d.ts +++ b/vendor/varint.d.ts @@ -3,44 +3,47 @@ // Definitions by: David Brockman Smoliansky // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/// +interface Varint { + encode: { + /** + * Encodes `num` into `buffer` starting at `offset`. returns `buffer`, with the encoded varint written into it. + * `varint.encode.bytes` will now be set to the number of bytes modified. + */ + (num: number, buffer: Uint8Array, offset?: number): Buffer; -export const encode: { - /** - * Encodes `num` into `buffer` starting at `offset`. returns `buffer`, with the encoded varint written into it. - * `varint.encode.bytes` will now be set to the number of bytes modified. - */ - (num: number, buffer: Uint8Array, offset?: number): Buffer; + /** + * Encodes `num` into `array` starting at `offset`. returns `array`, with the encoded varint written into it. + * If `array` is not provided, it will default to a new array. + * `varint.encode.bytes` will now be set to the number of bytes modified. + */ + (num: number, array?: number[], offset?: number): number[] - /** - * Encodes `num` into `array` starting at `offset`. returns `array`, with the encoded varint written into it. - * If `array` is not provided, it will default to a new array. - * `varint.encode.bytes` will now be set to the number of bytes modified. - */ - (num: number, array?: number[], offset?: number): number[] + /** + * Similar to `decode.bytes` when encoding a number it can be useful to know how many bytes where written (especially if you pass an output array). + * You can access this via `varint.encode.bytes` which holds the number of bytes written in the last encode. + */ + bytes: number + }, - /** - * Similar to `decode.bytes` when encoding a number it can be useful to know how many bytes where written (especially if you pass an output array). - * You can access this via `varint.encode.bytes` which holds the number of bytes written in the last encode. - */ - bytes: number -} + decode: { + /** + * Decodes `data`, which can be either a buffer or array of integers, from position `offset` or default 0 and returns the decoded original integer. + * Throws a `RangeError` when `data` does not represent a valid encoding. + */ + (buf: Uint8Array | number[], offset?: number): number -export const decode: { - /** - * Decodes `data`, which can be either a buffer or array of integers, from position `offset` or default 0 and returns the decoded original integer. - * Throws a `RangeError` when `data` does not represent a valid encoding. - */ - (buf: Uint8Array | number[], offset?: number): number + /** + * If you also require the length (number of bytes) that were required to decode the integer you can access it via `varint.decode.bytes`. + * This is an integer property that will tell you the number of bytes that the last .decode() call had to use to decode. + */ + bytes: number + }, /** - * If you also require the length (number of bytes) that were required to decode the integer you can access it via `varint.decode.bytes`. - * This is an integer property that will tell you the number of bytes that the last .decode() call had to use to decode. + * returns the number of bytes this number will be encoded as, up to a maximum of 8. */ - bytes: number + encodingLength(num: number): number } -/** - * returns the number of bytes this number will be encoded as, up to a maximum of 8. - */ -export function encodingLength(num: number): number +declare const varint:Varint +export default varint