UNPKG

multiformats

Version:

Interface for multihash, multicodec, multibase and CID

599 lines (547 loc) 17.2 kB
import { base32 } from './bases/base32.js' import { base58btc } from './bases/base58.js' import { coerce } from './bytes.js' import * as Digest from './hashes/digest.js' // Linter can see that API is used in types. // eslint-disable-next-line import * as API from "./link/interface.js" import * as varint from './varint.js' // This way TS will also expose all the types from module export * from './link/interface.js' /** * @template {API.Link<unknown, number, number, API.Version>} T * @template {string} Prefix * @param {T} link * @param {API.MultibaseEncoder<Prefix>} [base] * @returns {API.ToString<T, Prefix>} */ 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<Prefix>} */ (base || base32.encoder) ) } } /** * @template {API.UnknownLink} Link * @param {Link} link * @returns {API.LinkJSON<Link>} */ export const toJSON = (link) => ({ '/': format(link) }) /** * @template {API.UnknownLink} Link * @param {API.LinkJSON<Link>} json */ export const fromJSON = (json) => CID.parse(json['/']) /** @type {WeakMap<API.UnknownLink, Map<string, string>>} */ const cache = new WeakMap() /** * @param {API.UnknownLink} cid * @returns {Map<string, string>} */ const baseCache = cid => { const baseCache = cache.get(cid) if (baseCache == null) { const baseCache = new Map() cache.set(cid, baseCache) return baseCache } return baseCache } /** * @template {unknown} [Data=unknown] * @template {number} [Format=number] * @template {number} [Alg=number] * @template {API.Version} [Version=API.Version] * @implements {API.Link<Data, Format, Alg, Version>} */ export class CID { /** * @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<Alg>} 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 // flag to serializers that this is a CID and // should be treated specially /** @readonly */ this['/'] = bytes } /** * Signalling `cid.asCID === cid` has been replaced with `cid['/'] === cid.bytes` * please either use `CID.asCID(cid)` or switch to new signalling mechanism * * @deprecated */ get asCID () { return this } // ArrayBufferView get byteOffset () { return this.bytes.byteOffset } // ArrayBufferView get byteLength () { return this.bytes.byteLength } /** * @returns {CID<Data, API.DAG_PB, API.SHA_256, 0>} */ toV0 () { switch (this.version) { case 0: { return /** @type {CID<Data, API.DAG_PB, API.SHA_256, 0>} */ (this) } case 1: { const { code, multihash } = this if (code !== DAG_PB_CODE) { throw new Error('Cannot convert a non dag-pb CID to CIDv0') } // sha2-256 if (multihash.code !== SHA_256_CODE) { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } return /** @type {CID<Data, API.DAG_PB, API.SHA_256, 0>} */ ( CID.createV0( /** @type {API.MultihashDigest<API.SHA_256>} */ (multihash) ) ) } default: { throw Error( `Can not convert CID version ${this.version} to version 0. This is a bug please report` ) } } } /** * @returns {CID<Data, Format, Alg, 1>} */ toV1 () { switch (this.version) { case 0: { const { code, digest } = this.multihash const multihash = Digest.create(code, digest) return /** @type {CID<Data, Format, Alg, 1>} */ ( CID.createV1(this.code, multihash) ) } case 1: { return /** @type {CID<Data, Format, Alg, 1>} */ (this) } default: { throw Error( `Can not convert CID version ${this.version} to version 1. This is a bug please report` ) } } } /** * @param {unknown} other * @returns {other is CID<Data, Format, Alg, Version>} */ equals (other) { return CID.equals(this, other) } /** * @template {unknown} Data * @template {number} Format * @template {number} Alg * @template {API.Version} Version * @param {API.Link<Data, Format, Alg, Version>} 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 {API.MultibaseEncoder<string>} [base] * @returns {string} */ toString (base) { return format(this, base) } /** * @returns {API.LinkJSON<this>} */ toJSON () { return { '/': format(this) } } link () { return this } get [Symbol.toStringTag] () { return 'CID' } // Legacy [Symbol.for('nodejs.util.inspect.custom')] () { return `CID(${this.toString()})` } /** * 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 returns null. * * This allows two different incompatible versions of CID library to * co-exist and interop as long as binary interface is compatible. * * @template {unknown} Data * @template {number} Format * @template {number} Alg * @template {API.Version} Version * @template {unknown} U * @param {API.Link<Data, Format, Alg, Version>|U} input * @returns {CID<Data, Format, Alg, Version>|null} */ static asCID (input) { if (input == null) { return null } 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['/'] === value.bytes) || value.asCID === value) { // If value isn't instance of this CID class but `this.asCID === this` or // `value['/'] === value.bytes` 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<Alg>} */ (multihash), bytes || encodeCID(version, code, multihash.bytes) ) } else if (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<Alg>} */ (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 } } /** * * @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<Alg>} digest - (Multi)hash of the of the content. * @returns {CID<Data, Format, Alg, Version>} */ static create (version, code, digest) { if (typeof code !== 'number') { throw new Error('String codecs are no longer supported') } if (!(digest.bytes instanceof Uint8Array)) { throw new Error('Invalid digest') } 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') } } } /** * Simplified version of `create` for CIDv0. * * @template {unknown} [T=unknown] * @param {API.MultihashDigest<typeof SHA_256_CODE>} digest - Multihash. * @returns {CID<T, typeof DAG_PB_CODE, typeof SHA_256_CODE, 0>} */ static createV0 (digest) { return 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<Alg>} digest - Miltihash of the content. * @returns {CID<Data, Code, Alg, 1>} */ 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. * * @template {unknown} Data * @template {number} Code * @template {number} Alg * @template {API.Version} Ver * @param {API.ByteView<API.Link<Data, Code, Alg, Ver>>} bytes * @returns {CID<Data, Code, Alg, Ver>} */ static decode (bytes) { const [cid, remainder] = CID.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. * * @template {unknown} T * @template {number} C * @template {number} A * @template {API.Version} V * @param {API.ByteView<API.Link<T, C, A, V>>} bytes * @returns {[CID<T, C, A, V>, 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(/** @type {API.MultihashDigest<API.SHA_256>} */ (digest)) : CID.createV1(specs.codec, digest) return [/** @type {CID<T, C, A, V>} */(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. * * @template {unknown} T * @template {number} C * @template {number} A * @template {API.Version} V * @param {API.ByteView<API.Link<T, C, A, V>>} initialBytes * @returns {{ version:V, codec:C, multihashCode:A, 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 = /** @type {V} */ (next()) let codec = /** @type {C} */ (DAG_PB_CODE) if (/** @type {number} */(version) === 18) { // CIDv0 version = /** @type {V} */ (0) offset = 0 } else { codec = /** @type {C} */ (next()) } if (version !== 0 && version !== 1) { throw new RangeError(`Invalid CID version ${version}`) } const prefixSize = offset const multihashCode = /** @type {A} */ (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 * @template {unknown} Data * @template {number} Code * @template {number} Alg * @template {API.Version} Ver * @param {API.ToString<API.Link<Data, Code, Alg, Ver>, Prefix>} source * @param {API.MultibaseDecoder<Prefix>} [base] * @returns {CID<Data, Code, Alg, Ver>} */ static parse (source, base) { const [prefix, bytes] = parseCIDtoBytes(source, base) const cid = CID.decode(bytes) if (cid.version === 0 && source[0] !== 'Q') { throw Error('Version 0 CID string must not include multibase prefix') } // Cache string representation to avoid computing it on `this.toString()` baseCache(cid).set(prefix, source) return cid } } /** * @template {string} Prefix * @template {unknown} Data * @template {number} Code * @template {number} Alg * @template {API.Version} Ver * @param {API.ToString<API.Link<Data, Code, Alg, Ver>, Prefix>} source * @param {API.MultibaseDecoder<Prefix>} [base] * @returns {[Prefix, API.ByteView<API.Link<Data, Code, Alg, Ver>>]} */ const parseCIDtoBytes = (source, base) => { switch (source[0]) { // CIDv0 is parsed differently case 'Q': { const decoder = base || base58btc return [ /** @type {Prefix} */ (base58btc.prefix), decoder.decode(`${base58btc.prefix}${source}`) ] } case base58btc.prefix: { const decoder = base || base58btc return [/** @type {Prefix} */(base58btc.prefix), decoder.decode(source)] } case base32.prefix: { const decoder = base || base32 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' ) } return [/** @type {Prefix} */(source[0]), base.decode(source)] } } } /** * * @param {Uint8Array} bytes * @param {Map<string, string>} cache * @param {API.MultibaseEncoder<'z'>} base */ const toStringV0 = (bytes, cache, base) => { const { prefix } = base if (prefix !== base58btc.prefix) { throw Error(`Cannot string encode V0 in ${base.name} encoding`) } const cid = cache.get(prefix) if (cid == null) { const cid = base.encode(bytes).slice(1) cache.set(prefix, cid) return cid } else { return cid } } /** * @template {string} Prefix * @param {Uint8Array} bytes * @param {Map<string, string>} cache * @param {API.MultibaseEncoder<Prefix>} base */ const toStringV1 = (bytes, cache, base) => { const { prefix } = base const cid = cache.get(prefix) if (cid == null) { const cid = base.encode(bytes) cache.set(prefix, cid) return cid } else { return cid } } const DAG_PB_CODE = 0x70 const SHA_256_CODE = 0x12 /** * @param {API.Version} version * @param {number} code * @param {Uint8Array} multihash * @returns {Uint8Array} */ const encodeCID = (version, code, multihash) => { const codeOffset = varint.encodingLength(version) const hashOffset = codeOffset + varint.encodingLength(code) const bytes = new Uint8Array(hashOffset + multihash.byteLength) varint.encodeTo(version, bytes, 0) varint.encodeTo(code, bytes, codeOffset) bytes.set(multihash, hashOffset) return bytes } const cidSymbol = Symbol.for('@ipld/js-cid/CID')