multiformats
Version:
Interface for multihash, multicodec, multibase and CID
379 lines • 13.5 kB
JavaScript
import { base32 } from "./bases/base32.js";
import { base36 } from "./bases/base36.js";
import { base58btc } from "./bases/base58.js";
import { coerce, toArrayBufferBackedArray } from "./bytes.js";
import * as Digest from "./hashes/digest.js";
import * as varint from "./varint.js";
// This way TS will also expose all the types from module
export * from "./link/interface.js";
export function format(link, base) {
const { bytes, version } = link;
switch (version) {
case 0:
return toStringV0(bytes, baseCache(link), base ?? base58btc.encoder);
default:
return toStringV1(bytes, baseCache(link), (base ?? base32.encoder));
}
}
export function toJSON(link) {
return {
'/': format(link)
};
}
export function fromJSON(json) {
return CID.parse(json['/']);
}
const cache = new WeakMap();
function baseCache(cid) {
const baseCache = cache.get(cid);
if (baseCache == null) {
const baseCache = new Map();
cache.set(cid, baseCache);
return baseCache;
}
return baseCache;
}
export class CID {
code;
version;
multihash;
bytes;
'/';
/**
* @param version - Version of the CID
* @param code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv
* @param multihash - (Multi)hash of the of the content.
*/
constructor(version, code, multihash, bytes) {
this.code = code;
this.version = version;
this.multihash = multihash;
this.bytes = toArrayBufferBackedArray(bytes);
// flag to serializers that this is a CID and
// should be treated specially
this['/'] = 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;
}
toV0() {
switch (this.version) {
case 0: {
return 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 (CID.createV0(multihash));
}
default: {
throw Error(`Can not convert CID version ${this.version} to version 0. This is a bug please report`);
}
}
}
toV1() {
switch (this.version) {
case 0: {
const { code, digest } = this.multihash;
const multihash = Digest.create(code, digest);
return (CID.createV1(this.code, multihash));
}
case 1: {
return this;
}
default: {
throw Error(`Can not convert CID version ${this.version} to version 1. This is a bug please report`);
}
}
}
equals(other) {
return CID.equals(this, other);
}
static equals(self, other) {
const unknown = other;
return (unknown != null &&
self.code === unknown.code &&
self.version === unknown.version &&
Digest.equals(self.multihash, unknown.multihash));
}
toString(base) {
return format(this, base);
}
toJSON() {
return { '/': format(this) };
}
link() {
return this;
}
[Symbol.toStringTag] = '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.
*/
static asCID(input) {
if (input == null) {
return null;
}
const value = 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, 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 = 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;
}
}
/**
* @param version - Version of the CID
* @param code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv
* @param 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');
}
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.
*/
static createV0(digest) {
return CID.create(0, DAG_PB_CODE, digest);
}
/**
* Simplified version of `create` for CIDv1.
*
* @param code - Content encoding format code.
* @param digest - Multihash 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.
*/
static decode(bytes) {
const [cid, remainder] = CID.decodeFirst(bytes);
if (remainder.length !== 0) {
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.
*/
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)];
}
/**
* 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.
*/
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 {
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).
*/
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;
}
}
function parseCIDtoBytes(source, base) {
switch (source[0]) {
// CIDv0 is parsed differently
case 'Q': {
const decoder = base ?? base58btc;
return [
base58btc.prefix,
decoder.decode(`${base58btc.prefix}${source}`)
];
}
case base58btc.prefix: {
const decoder = base ?? base58btc;
return [base58btc.prefix, decoder.decode(source)];
}
case base32.prefix: {
const decoder = base ?? base32;
return [base32.prefix, decoder.decode(source)];
}
case base36.prefix: {
const decoder = base ?? base36;
return [base36.prefix, decoder.decode(source)];
}
default: {
if (base == null) {
throw Error('To parse non base32, base36 or base58btc encoded CID multibase decoder must be provided');
}
return [source[0], base.decode(source)];
}
}
}
function 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;
}
}
function 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;
function 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');
//# sourceMappingURL=cid.js.map