diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index a18a1b6e..21dd9b5b 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 ec061f30..bc3bb40c 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,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 680b0187..6de7a645 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 { /** @@ -107,7 +84,7 @@ class Decoder { /** * @template {string} OtherPrefix - * @param {UnibaseDecoder|ComposedDecoder} decoder + * @param {API.UnibaseDecoder|ComposedDecoder} decoder * @returns {ComposedDecoder} */ or (decoder) { @@ -117,22 +94,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 @@ -140,7 +112,7 @@ class ComposedDecoder { /** * @template {string} OtherPrefix - * @param {UnibaseDecoder|ComposedDecoder} decoder + * @param {API.UnibaseDecoder|ComposedDecoder} decoder * @returns {ComposedDecoder} */ or (decoder) { @@ -165,30 +137,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/block.js b/src/block.js index 75bed063..3a53b766 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, @@ -10,7 +13,7 @@ const readonly = ({ enumerable = true, configurable = false } = {}) => ({ * @template T * @param {T} source * @param {Array} base - * @returns {Iterable<[string, CID]>} + * @returns {Iterable<[string, API.CIDView]>} */ const links = function * (source, base) { if (source == null) return @@ -71,6 +74,7 @@ const tree = function * (source, base) { * @template T * @param {T} source * @param {string[]} path + * @return {API.BlockCursorView} */ const get = (source, path) => { /** @type {Record} */ @@ -89,17 +93,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.CIDVersion} V - CID version + * @implements {API.BlockView} */ class Block { /** * @param {Object} options - * @param {CID} options.cid - * @param {ByteView} options.bytes + * @param {API.CIDView} 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 @@ -124,22 +132,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"') @@ -147,20 +156,24 @@ 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) + 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"') @@ -175,14 +188,16 @@ const decode = async ({ bytes, codec, hasher }) => { /** * @typedef {Object} RequiredCreateOptions - * @property {CID} options.cid + * @property {API.CID} options.cid */ /** - * @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.CIDVersion} 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 @@ -191,19 +206,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 {API.CIDView} */ (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.CIDVersion} 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.CID} 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"') @@ -214,29 +235,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..a30b2352 --- /dev/null +++ b/src/block/interface.ts @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable no-use-before-define */ +import { Link, CIDView, CIDVersion } from '../cid/interface.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 non-existings + // 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 CIDVersion = 1 +> { + bytes: ByteView + cid: Link +} + +export type BlockCursorView = + | { value: T, remaining?: undefined } + | { value: CIDView, remaining: string } + +export interface BlockView< + T = unknown, + C extends number = number, + A extends number = number, + V extends CIDVersion = 1 +> extends Block { + cid: CIDView & Link + value: T + + links(): Iterable<[string, CIDView]> + tree(): Iterable + get(path:string): BlockCursorView +} diff --git a/src/cid.js b/src/cid.js index 403c3104..02891f8a 100644 --- a/src/cid.js +++ b/src/cid.js @@ -3,48 +3,310 @@ 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 './cid/interface.js' + +// This way TS will also expose all the types from module +export * from './cid/interface.js' + +/** + * @template {number} Format + * @template {number} Alg + * @template {API.CIDVersion} Version + * @template {unknown} U + * @param {API.CID|U} input + * @returns {API.CID|null} + */ +export const asCID = (input) => { + const value = /** @type {any} */(input) + if (value instanceof CID) { + // 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. + const { version, code, multihash, bytes } = value + 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 the this `CID` implementation by + // delegating that to a constructor. + const { version, multihash, code } = value + const digest = + /** @type {API.MultihashDigest} */ + (Digest.decode(multihash)) + return create(version, code, digest) + } else { + // Otherwise value is not a CID (or an incompatible version of it) in + // which case we return `null`. + return null + } +} + +/** + * @template {number} Format + * @template {number} Alg + * @template {API.CIDVersion} Version + * @param {Version} version - Version of the CID + * @param {Format} code - Code of the codec content is encoded in. + * @param {API.MultihashDigest} digest - (Multi)hash of the of the content. + * @returns {API.CID} + */ +export const create = (version, code, digest) => { + if (typeof code !== 'number') { + throw new Error('String codecs are no longer supported') + } + + 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`) + } else { + return new CID(version, code, digest, digest.bytes) + } + } + case 1: { + const bytes = encodeCID(version, code, digest.bytes) + return new CID(version, code, digest, bytes) + } + default: { + throw new Error('Invalid version') + } + } +} /** - * @typedef {import('./hashes/interface').MultihashDigest} MultihashDigest - * @typedef {0 | 1} CIDVersion + * Simplified version of `create` for CIDv0. + * @param {API.MultihashDigest} digest - Multihash. + * @returns {API.CIDv0} */ +export const createV0 = (digest) => create(0, DAG_PB_CODE, digest) /** - * @template Prefix - * @typedef {import('./bases/interface').MultibaseEncoder} MultibaseEncoder + * Simplified version of `create` for CIDv1. + * @template {number} Code + * @template {number} Alg + * @param {Code} code - Content encoding format code. + * @param {API.MultihashDigest} digest - Miltihash of the content. + * @returns {API.CIDv1} */ +export const createV1 = (code, digest) => create(1, code, digest) /** - * @template Prefix - * @typedef {import('./bases/interface').MultibaseDecoder} MultibaseDecoder + * 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. + * + * @param {Uint8Array} bytes + * @returns {API.CID} + */ +export const decode = (bytes) => { + const [cid, remainder] = decodeFirst(bytes) + if (remainder.length) { + throw new Error('Incorrect length') + } + return cid +} + +/** + * Decoded a CID from its binary representation at the beginning of a byte + * array. + * + * Returns an array with the first element containing the CID and the second + * element containing the remainder of the original byte array. The remainder + * will be a zero-length byte array if the provided bytes only contained a + * binary CID representation. + * + * @param {Uint8Array} bytes + * @returns {[API.CID, Uint8Array]} + */ +export const decodeFirst = (bytes) => { + const specs = inspectBytes(bytes) + const prefixSize = specs.size - 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 + ? createV0(/** @type {API.MultihashDigest} */(digest)) + : createV1(specs.codec, digest) + return [cid, bytes.subarray(specs.size)] +} + +/** + * Inspect the initial bytes of a CID to determine its properties. + * + * Involves decoding up to 4 varints. Typically this will require only 4 to 6 + * bytes but for larger multicodec code values and larger multihash digest + * lengths these varints can be quite large. It is recommended that at least + * 10 bytes be made available in the `initialBytes` argument for a complete + * inspection. + * + * @param {Uint8Array} initialBytes + * @returns {{ version:API.CIDVersion, codec:number, multihashCode:number, digestSize:number, multihashSize:number, size:number }} + */ +export const inspectBytes = (initialBytes) => { + let offset = 0 + const next = () => { + const [i, length] = varint.decode(initialBytes.subarray(offset)) + offset += length + return i + } + + let version = next() + let codec = DAG_PB_CODE + if (version === 18) { + // CIDv0 + version = 0 + offset = 0 + } else if (version === 1) { + codec = next() + } + + if (version !== 0 && version !== 1) { + throw new RangeError(`Invalid CID version ${version}`) + } + + const prefixSize = offset + const multihashCode = next() // multihash code + const digestSize = next() // multihash length + const size = offset + digestSize + const multihashSize = size - prefixSize + + return { version, codec, multihashCode, digestSize, multihashSize, size } +} + +/** + * 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 + * @param {string} source + * @param {API.MultibaseDecoder} [base] + * @returns {API.CID} + */ +export const parse = (source, base) => { + const [prefix, bytes] = parseCIDtoBytes(source, base) + + const cid = decode(bytes) + + // Cache string representation to avoid computing it on `this.toString()` + baseCache(cid).set(prefix, source) + + return cid +} + +/** + * @template {number} Format + * @template {number} Alg + * @template {API.CIDVersion} Version + * @param {API.CID} cid + * @param {any} other + * @returns {other is cid} + */ +export const equals = (cid, other) => { + return ( + other && + cid.code === other.code && + cid.version === other.version && + Digest.equals(cid.multihash, other.multihash) + ) +} + +/** + * @param {API.CID} cid + * @param {API.MultibaseEncoder} [base] + * @returns {string} + */ +export const toString = (cid, base) => { + const { bytes, version } = cid + switch (version) { + case 0: + return toStringV0( + bytes, + baseCache(cid), + /** @type {API.MultibaseEncoder<"z">} */ (base) || base58btc.encoder + ) + default: + return toStringV1(bytes, baseCache(cid), base || base32.encoder) + } +} + +/** @type {WeakMap>} */ +const cache = new WeakMap() + +/** + * @param {API.CID} 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 {number} [Format=number] + * @template {number} [Alg=number] + * @template {API.CIDVersion} [Version=API.CIDVersion] + * @implements {API.CID} */ 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 + * @param {Format} code - multicodec code, see https://github.com/multiformats/multicodec/blob/master/table.csv + * @param {API.MultihashDigest} multihash * @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 +318,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 +342,55 @@ export class CID { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return CID.createV0(multihash) + return /** @type {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} */ ( + 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 equals(this, other) } /** - * @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 toString(this, base) } toJSON () { @@ -145,7 +408,7 @@ export class CID { // Legacy [Symbol.for('nodejs.util.inspect.custom')] () { - return 'CID(' + this.toString() + ')' + return `CID(${this.toString()})` } // Deprecated @@ -164,11 +427,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,203 +447,86 @@ 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 {number} C + * @template {number} A + * @template {API.CIDVersion} V + * @template {unknown} U + * @param {API.CID|U} value + * @returns {CID|null} */ static asCID (value) { - if (value instanceof CID) { - // 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. - const { version, code, multihash, bytes } = value - return new CID(version, code, 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. - const { version, multihash, code } = value - const digest = 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`. - return null - } + return /** @type {CID|null} */ (asCID(value)) } /** - * - * @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 {number} Format + * @template {number} Alg + * @template {API.CIDVersion} Version + * @param {Version} version - Version of the CID + * @param {Format} code - Code of the codec content is encoded in. + * @param {API.MultihashDigest} digest - (Multi)hash of the of the content. + */ static create (version, code, digest) { - if (typeof code !== 'number') { - throw new Error('String codecs are no longer supported') - } - - 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`) - } else { - return new CID(version, code, digest, digest.bytes) - } - } - case 1: { - const bytes = encodeCID(version, code, digest.bytes) - return new CID(version, code, digest, bytes) - } - default: { - throw new Error('Invalid version') - } - } + return /** @type {CID} */ ( + create(version, code, digest) + ) } /** * Simplified version of `create` for CIDv0. - * @param {MultihashDigest} digest - Multihash. + * @param {API.MultihashDigest} digest - Multihash. */ 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 {number} Code + * @template {number} Alg + * @param {Code} code - Content encoding format code. + * @param {API.MultihashDigest} digest - Miltihash of the content. + */ static createV1 (code, digest) { return CID.create(1, code, digest) } /** - * 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. - * * @param {Uint8Array} bytes - * @returns {CID} */ + static decode (bytes) { - const [cid, remainder] = CID.decodeFirst(bytes) - if (remainder.length) { - throw new Error('Incorrect length') - } - return cid + return /** @type {CID} */ (decode(bytes)) } /** - * Decoded a CID from its binary representation at the beginning of a byte - * array. - * - * Returns an array with the first element containing the CID and the second - * element containing the remainder of the original byte array. The remainder - * will be a zero-length byte array if the provided bytes only contained a - * binary CID representation. - * * @param {Uint8Array} 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)) - 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)] + return /** @type {[CID, Uint8Array]} */ (decodeFirst(bytes)) } /** - * Inspect the initial bytes of a CID to determine its properties. - * - * Involves decoding up to 4 varints. Typically this will require only 4 to 6 - * bytes but for larger multicodec code values and larger multihash digest - * lengths these varints can be quite large. It is recommended that at least - * 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 }} */ static inspectBytes (initialBytes) { - let offset = 0 - const next = () => { - const [i, length] = varint.decode(initialBytes.subarray(offset)) - offset += length - return i - } - - let version = next() - let codec = DAG_PB_CODE - if (version === 18) { // CIDv0 - version = 0 - offset = 0 - } else if (version === 1) { - codec = next() - } - - if (version !== 0 && version !== 1) { - throw new RangeError(`Invalid CID version ${version}`) - } - - const prefixSize = offset - const multihashCode = next() // multihash code - const digestSize = next() // multihash length - const size = offset + digestSize - const multihashSize = size - prefixSize - - return { version, codec, multihashCode, digestSize, multihashSize, size } + return inspectBytes(initialBytes) } /** - * 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 * @param {string} source - * @param {MultibaseDecoder} [base] + * @param {API.MultibaseDecoder} [base] */ 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) - - return cid + return /** @type {CID} */ (parse(source, base)) } } /** * @template {string} Prefix * @param {string} source - * @param {MultibaseDecoder} [base] + * @param {API.MultibaseDecoder} [base] * @returns {[string, Uint8Array]} */ const parseCIDtoBytes = (source, base) => { @@ -407,7 +557,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 +579,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 +597,7 @@ const DAG_PB_CODE = 0x70 const SHA_256_CODE = 0x12 /** - * @param {CIDVersion} version + * @param {API.CIDVersion} version * @param {number} code * @param {Uint8Array} multihash * @returns {Uint8Array} diff --git a/src/cid/interface.js b/src/cid/interface.js new file mode 100644 index 00000000..d9b3f4f7 --- /dev/null +++ b/src/cid/interface.js @@ -0,0 +1 @@ +// this is dummy module overlayed by interface.ts diff --git a/src/cid/interface.ts b/src/cid/interface.ts new file mode 100644 index 00000000..da71864e --- /dev/null +++ b/src/cid/interface.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable no-use-before-define */ +import type { MultihashDigest } from '../hashes/interface' +import type { MultibaseEncoder, MultibaseDecoder } from '../bases/interface' +import type { Phantom } from '../block/interface' +export type { CID as CIDView } from '../cid' + +export type { MultihashDigest, MultibaseEncoder, MultibaseDecoder } +export type CIDVersion = 0 | 1 + +export type DAG_PB = 0x70 +export type SHA_256 = 0x12 + +export interface CID< + Format extends number = number, + Alg extends number = number, + Version extends CIDVersion = CIDVersion +> { + readonly version: Version + readonly code: Format + readonly multihash: MultihashDigest + + readonly byteOffset: number + readonly byteLength: number + readonly bytes: Uint8Array + + readonly asCID: this + + equals(other: unknown): other is CID + + toString(base?: MultibaseEncoder): string + toJSON(): { version: Version, code:Format, hash:Uint8Array } + + toV0(): CIDv0 + toV1(): CIDv1 +} + +/** + * 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< + T extends unknown = unknown, + C extends number = number, + A extends number = number, + V extends CIDVersion = 1 +> extends CID, Phantom { +} + +export interface CIDv0 extends CID { + readonly version: 0 +} + +export interface CIDv1< + Format extends number = number, + Alg extends number = number +> extends CID { + readonly version: 1 +} + +// Export interface with different name because +// cid.js will shadow `CID` interface with a class +export type { CID as CIDType } 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/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..ed29ee8e --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,5 @@ +export * from './bases/interface' +export * from './hashes/interface' +export * from './codecs/interface' +export * from './cid/interface' +export * from './block/interface' diff --git a/src/traversal.js b/src/traversal.js index a13d1d5c..52f4eb52 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/interface').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..b21cf181 100644 --- a/test/test-cid.js +++ b/test/test-cid.js @@ -1,8 +1,9 @@ /* 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 * as CIDLib from 'multiformats/cid' import { base58btc } from 'multiformats/bases/base58' import { base32 } from 'multiformats/bases/base32' import { base64 } from 'multiformats/bases/base64' @@ -10,6 +11,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 +43,26 @@ describe('CID', () => { assert.deepStrictEqual(cid.toString(), base58btc.baseEncode(hash.bytes)) }) + it('createV0', async () => { + const hash = await sha256.digest(textEncoder.encode('abc')) + const cid = CIDLib.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('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')) @@ -91,8 +115,8 @@ describe('CID', () => { 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', () => { @@ -152,6 +176,24 @@ describe('CID', () => { equalDigest(cid.multihash, hash) }) + it('createV1', async () => { + const hash = await sha256.digest(textEncoder.encode('abc')) + const cid = CIDLib.createV1(0x71, hash) + + assert.deepStrictEqual(cid.code, 0x71) + assert.deepStrictEqual(cid.version, 1) + 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) @@ -195,8 +237,8 @@ describe('CID', () => { it('should construct from an old CID without a multibaseName', () => { 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) }) }) @@ -247,8 +289,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)) }) @@ -344,6 +384,20 @@ 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 +412,52 @@ 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') + const b32 = { + ...base32, + callCount: 0, + /** + * @param {Uint8Array} bytes + */ + encode (bytes) { + this.callCount += 1 + return base32.encode(bytes) + '!' + } + } - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.has(base32.prefix), false) + const b64 = { + ...base64, + callCount: 0, + /** + * @param {Uint8Array} bytes + */ + encode (bytes) { + this.callCount += 1 + return base64.encode(bytes) + } + } const base32String = 'bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu' - assert.deepStrictEqual(cid.toString(), base32String) + assert.deepEqual(cid.toString(b32), `${base32String}!`) + assert.deepEqual(b32.callCount, 1) + assert.deepEqual(cid.toString(), `${base32String}!`) + assert.deepEqual(b32.callCount, 1) - // @ts-expect-error - _baseCache is private - assert.deepStrictEqual(cid._baseCache.get(base32.prefix), base32String) - assert.deepStrictEqual(cid.toString(base64), 'mAXASILp4Fr+PAc/qQUFA3l2uIiOwA2Gjlhd6nLQQ/2HyABWt') + 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 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) }) }) @@ -458,8 +534,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 +572,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) }) @@ -576,6 +652,7 @@ describe('CID', () => { 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') diff --git a/test/test-multibase-spec.js b/test/test-multibase-spec.js index 8d42e45c..3a4f69c5 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 f8a42c72..d55a4bb2 100644 --- a/test/test-traversal.js +++ b/test/test-traversal.js @@ -4,8 +4,10 @@ import * as dagPB from '@ipld/dag-pb' import { sha256 as hasher } from 'multiformats/hashes/sha2' import * as main from 'multiformats/block' import { walk } from 'multiformats/traversal' +// eslint-disable-next-line +import * as API from 'multiformats/interface' import { assert } from 'chai' -import { fromString } from '../src/bytes.js' +import { fromString } from 'multiformats/bytes' const { createLink, createNode } = dagPB @@ -43,7 +45,7 @@ describe('traversal', () => { const cidA = blockA.cid /** - * @param {import('multiformats').CID} cid + * @param {API.CID} cid */ const load = async (cid) => { if (cid.equals(cidE)) { @@ -70,7 +72,7 @@ describe('traversal', () => { */ const loadWrapper = (load, arr = []) => /** - * @param {import('multiformats').CID} cid + * @param {API.CID} cid */ (cid) => { arr.push(cid.toString()) @@ -163,7 +165,7 @@ describe('traversal', () => { /** @type {[]} */ const links = [] const value = createNode(fromString('test'), links) - const block = await main.encode({ value: value, codec, hasher }) + const block = await main.encode({ value, codec, hasher }) const cid = block.cid const expectedCallArray = [cid.toString()] /** @type {string[]} */ 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