+457
@@ -0,0 +1,457 @@
|
||||
import { base32 } from './bases/base32.js'
|
||||
import { base58btc } from './bases/base58.js'
|
||||
import { coerce } from './bytes.js'
|
||||
import * as Digest from './hashes/digest.js'
|
||||
import * as varint from './varint.js'
|
||||
import type * as API from './link/interface.js'
|
||||
|
||||
// This way TS will also expose all the types from module
|
||||
export * from './link/interface.js'
|
||||
|
||||
export function format <T extends API.Link<unknown, number, number, API.Version>, Prefix extends string> (link: T, base?: API.MultibaseEncoder<Prefix>): API.ToString<T, Prefix> {
|
||||
const { bytes, version } = link
|
||||
switch (version) {
|
||||
case 0:
|
||||
return toStringV0(
|
||||
bytes,
|
||||
baseCache(link),
|
||||
base as API.MultibaseEncoder<'z'> ?? base58btc.encoder
|
||||
)
|
||||
default:
|
||||
return toStringV1(
|
||||
bytes,
|
||||
baseCache(link),
|
||||
(base ?? base32.encoder) as API.MultibaseEncoder<Prefix>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function toJSON <Link extends API.UnknownLink> (link: Link): API.LinkJSON<Link> {
|
||||
return {
|
||||
'/': format(link)
|
||||
}
|
||||
}
|
||||
|
||||
export function fromJSON <Link extends API.UnknownLink> (json: API.LinkJSON<Link>): CID<unknown, number, number, API.Version> {
|
||||
return CID.parse(json['/'])
|
||||
}
|
||||
|
||||
const cache = new WeakMap<API.UnknownLink, Map<string, string>>()
|
||||
|
||||
function baseCache (cid: API.UnknownLink): Map<string, string> {
|
||||
const baseCache = cache.get(cid)
|
||||
if (baseCache == null) {
|
||||
const baseCache = new Map()
|
||||
cache.set(cid, baseCache)
|
||||
return baseCache
|
||||
}
|
||||
return baseCache
|
||||
}
|
||||
|
||||
export class CID<Data = unknown, Format extends number = number, Alg extends number = number, Version extends API.Version = API.Version> implements API.Link<Data, Format, Alg, Version> {
|
||||
readonly code: Format
|
||||
readonly version: Version
|
||||
readonly multihash: API.MultihashDigest<Alg>
|
||||
readonly bytes: Uint8Array
|
||||
readonly '/': Uint8Array
|
||||
|
||||
/**
|
||||
* @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: Version, code: Format, multihash: API.MultihashDigest<Alg>, bytes: Uint8Array) {
|
||||
this.code = code
|
||||
this.version = version
|
||||
this.multihash = multihash
|
||||
this.bytes = bytes
|
||||
|
||||
// flag to serializers that this is a CID and
|
||||
// should be treated specially
|
||||
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 (): this {
|
||||
return this
|
||||
}
|
||||
|
||||
// ArrayBufferView
|
||||
get byteOffset (): number {
|
||||
return this.bytes.byteOffset
|
||||
}
|
||||
|
||||
// ArrayBufferView
|
||||
get byteLength (): number {
|
||||
return this.bytes.byteLength
|
||||
}
|
||||
|
||||
toV0 (): CID<Data, API.DAG_PB, API.SHA_256, 0> {
|
||||
switch (this.version) {
|
||||
case 0: {
|
||||
return this as CID<Data, API.DAG_PB, API.SHA_256, 0>
|
||||
}
|
||||
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 as API.MultihashDigest<API.SHA_256>
|
||||
)
|
||||
)
|
||||
}
|
||||
default: {
|
||||
throw Error(
|
||||
`Can not convert CID version ${this.version} to version 0. This is a bug please report`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toV1 (): CID<Data, Format, Alg, 1> {
|
||||
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 as CID<Data, Format, Alg, 1>
|
||||
}
|
||||
default: {
|
||||
throw Error(
|
||||
`Can not convert CID version ${this.version} to version 1. This is a bug please report`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
equals (other: unknown): other is CID<Data, Format, Alg, Version> {
|
||||
return CID.equals(this, other)
|
||||
}
|
||||
|
||||
static equals <Data, Format extends number, Alg extends number, Version extends API.Version>(self: API.Link<Data, Format, Alg, Version>, other: unknown): other is CID {
|
||||
const unknown = other as { code?: unknown, version?: unknown, multihash?: unknown }
|
||||
return (
|
||||
unknown != null &&
|
||||
self.code === unknown.code &&
|
||||
self.version === unknown.version &&
|
||||
Digest.equals(self.multihash, unknown.multihash)
|
||||
)
|
||||
}
|
||||
|
||||
toString (base?: API.MultibaseEncoder<string>): string {
|
||||
return format(this, base)
|
||||
}
|
||||
|
||||
toJSON (): API.LinkJSON<this> {
|
||||
return { '/': format(this) }
|
||||
}
|
||||
|
||||
link (): this {
|
||||
return this
|
||||
}
|
||||
|
||||
readonly [Symbol.toStringTag] = 'CID';
|
||||
|
||||
// Legacy
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')] (): string {
|
||||
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 <Data, Format extends number, Alg extends number, Version extends API.Version, U>(input: API.Link<Data, Format, Alg, Version> | U): CID<Data, Format, Alg, Version> | null {
|
||||
if (input == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = input as any
|
||||
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 as API.MultihashDigest<Alg>,
|
||||
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) as API.MultihashDigest<Alg>
|
||||
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 <Data, Format extends number, Alg extends number, Version extends API.Version>(version: Version, code: Format, digest: API.MultihashDigest<Alg>): CID<Data, Format, Alg, Version> {
|
||||
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 <T = unknown>(digest: API.MultihashDigest<typeof SHA_256_CODE>): CID<T, typeof DAG_PB_CODE, typeof SHA_256_CODE, 0> {
|
||||
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 <Data, Code extends number, Alg extends number>(code: Code, digest: API.MultihashDigest<Alg>): CID<Data, Code, Alg, 1> {
|
||||
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 <Data, Code extends number, Alg extends number, Version extends API.Version>(bytes: API.ByteView<API.Link<Data, Code, Alg, Version>>): CID<Data, Code, Alg, Version> {
|
||||
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 <T, C extends number, A extends number, V extends API.Version>(bytes: API.ByteView<API.Link<T, C, A, V>>): [CID<T, C, A, V>, Uint8Array] {
|
||||
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 as API.MultihashDigest<API.SHA_256>)
|
||||
: CID.createV1(specs.codec, digest)
|
||||
return [cid as CID<T, C, A, V>, 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 <T, C extends number, A extends number, V extends API.Version>(initialBytes: API.ByteView<API.Link<T, C, A, V>>): { version: V, codec: C, multihashCode: A, digestSize: number, multihashSize: number, size: number } {
|
||||
let offset = 0
|
||||
const next = (): number => {
|
||||
const [i, length] = varint.decode(initialBytes.subarray(offset))
|
||||
offset += length
|
||||
return i
|
||||
}
|
||||
|
||||
let version = next() as V
|
||||
let codec = DAG_PB_CODE as C
|
||||
if (version as number === 18) {
|
||||
// CIDv0
|
||||
version = 0 as V
|
||||
offset = 0
|
||||
} else {
|
||||
codec = next() as C
|
||||
}
|
||||
|
||||
if (version !== 0 && version !== 1) {
|
||||
throw new RangeError(`Invalid CID version ${version}`)
|
||||
}
|
||||
|
||||
const prefixSize = offset
|
||||
const multihashCode = next() as A // 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 <Prefix extends string, Data, Code extends number, Alg extends number, Version extends API.Version>(source: API.ToString<API.Link<Data, Code, Alg, Version>, Prefix>, base?: API.MultibaseDecoder<Prefix>): CID<Data, Code, Alg, Version> {
|
||||
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 <Prefix extends string, Data, Code extends number, Alg extends number, Version extends API.Version> (source: API.ToString<API.Link<Data, Code, Alg, Version>, Prefix>, base?: API.MultibaseDecoder<Prefix>): [Prefix, API.ByteView<API.Link<Data, Code, Alg, Version>>] {
|
||||
switch (source[0]) {
|
||||
// CIDv0 is parsed differently
|
||||
case 'Q': {
|
||||
const decoder = base ?? base58btc
|
||||
return [
|
||||
base58btc.prefix as Prefix,
|
||||
decoder.decode(`${base58btc.prefix}${source}`)
|
||||
]
|
||||
}
|
||||
case base58btc.prefix: {
|
||||
const decoder = base ?? base58btc
|
||||
return [base58btc.prefix as Prefix, decoder.decode(source)]
|
||||
}
|
||||
case base32.prefix: {
|
||||
const decoder = base ?? base32
|
||||
return [base32.prefix as Prefix, decoder.decode(source)]
|
||||
}
|
||||
default: {
|
||||
if (base == null) {
|
||||
throw Error(
|
||||
'To parse non base32 or base58btc encoded CID multibase decoder must be provided'
|
||||
)
|
||||
}
|
||||
return [source[0] as Prefix, base.decode(source)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toStringV0 (bytes: Uint8Array, cache: Map<string, string>, base: API.MultibaseEncoder<'z'>): string {
|
||||
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 <Prefix extends string> (bytes: Uint8Array, cache: Map<string, string>, base: API.MultibaseEncoder<Prefix>): string {
|
||||
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: API.Version, code: number, multihash: Uint8Array): Uint8Array {
|
||||
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')
|
||||
Reference in New Issue
Block a user