UNPKG

multiformats

Version:

Interface for multihash, multicodec, multibase and CID

240 lines (209 loc) 8.27 kB
import { bytes as binary, CID } from './index.ts' import type * as API from './interface.ts' function readonly ({ enumerable = true, configurable = false } = {}): { enumerable: boolean, configurable: boolean, writable: false } { return { enumerable, configurable, writable: false } } function * linksWithin (path: [string | number, string], value: any): Iterable<[string, CID]> { if (value != null && typeof value === 'object') { if (Array.isArray(value)) { for (const [index, element] of value.entries()) { const elementPath = [...path, index] const cid = CID.asCID(element) if (cid != null) { yield [elementPath.join('/'), cid] } else if (typeof element === 'object') { yield * links(element, elementPath) } } } else { const cid = CID.asCID(value) if (cid != null) { yield [path.join('/'), cid] } else { yield * links(value, path) } } } } function * links <T> (source: T, base: Array<string | number>): Iterable<[string, CID]> { if (source == null || source instanceof Uint8Array) { return } const cid = CID.asCID(source) if (cid != null) { yield [base.join('/'), cid] } for (const [key, value] of Object.entries(source)) { const path = [...base, key] as [string | number, string] yield * linksWithin(path, value) } } function * treeWithin (path: [string | number, string], value: any): Iterable<string> { if (Array.isArray(value)) { for (const [index, element] of value.entries()) { const elementPath = [...path, index] yield elementPath.join('/') if (typeof element === 'object' && (CID.asCID(element) == null)) { yield * tree(element, elementPath) } } } else { yield * tree(value, path) } } function * tree <T> (source: T, base: Array<string | number>): Iterable<string> { if (source == null || typeof source !== 'object') { return } for (const [key, value] of Object.entries(source)) { const path = [...base, key] as [string | number, string] yield path.join('/') if (value != null && !(value instanceof Uint8Array) && typeof value === 'object' && (CID.asCID(value) == null)) { yield * treeWithin(path, value) } } } function get <T> (source: T, path: string[]): API.BlockCursorView<unknown> { let node = source as Record<string, any> for (const [index, key] of path.entries()) { node = node[key] if (node == null) { throw new Error(`Object has no property at ${path.slice(0, index + 1).map(part => `[${JSON.stringify(part)}]`).join('')}`) } const cid = CID.asCID(node) if (cid != null) { return { value: cid, remaining: path.slice(index + 1).join('/') } } } return { value: node } } /** * @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 class Block<T, C extends number, A extends number, V extends API.Version> implements API.BlockView<T, C, A, V> { readonly cid: CID<T, C, A, V> readonly bytes: API.ByteView<T> readonly value: T readonly asBlock: this constructor ({ cid, bytes, value }: { cid: CID<T, C, A, V>, bytes: API.ByteView<T>, value: T }) { if (cid == null || bytes == null || typeof value === 'undefined') { throw new Error('Missing required argument') } this.cid = cid this.bytes = bytes this.value = value this.asBlock = this // Mark all the properties immutable Object.defineProperties(this, { cid: readonly(), bytes: readonly(), value: readonly(), asBlock: readonly() }) } links (): Iterable<[string, CID<unknown, number, number, API.Version>]> { return links(this.value, []) } tree (): Iterable<string> { return tree(this.value, []) } get (path = '/'): API.BlockCursorView<unknown> { return get(this.value, path.split('/').filter(Boolean)) } } interface EncodeInput <T, Code extends number, Alg extends number> { value: T codec: API.BlockEncoder<Code, T> hasher: API.MultihashHasher<Alg> } /** * @template T - Logical type of the data encoded in the block * @template Code - multicodec code corresponding to codec used to encode the block * @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation. */ export async function encode <T, Code extends number, Alg extends number> ({ value, codec, hasher }: EncodeInput<T, Code, Alg>): Promise<API.BlockView<T, Code, Alg>> { if (typeof value === 'undefined') { throw new Error('Missing required argument "value"') } if (codec == null || hasher == null) { throw new Error('Missing required argument: codec or hasher') } const bytes = codec.encode(value) const hash = await hasher.digest(bytes) const cid = CID.create( 1, codec.code, hash ) as CID<T, Code, Alg, 1> return new Block({ value, bytes, cid }) } interface DecodeInput <T, Code extends number, Alg extends number> { bytes: API.ByteView<T> codec: API.BlockDecoder<Code, T> hasher: API.MultihashHasher<Alg> } /** * @template T - Logical type of the data encoded in the block * @template Code - multicodec code corresponding to codec used to encode the block * @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation. */ export async function decode <T, Code extends number, Alg extends number> ({ bytes, codec, hasher }: DecodeInput<T, Code, Alg>): Promise<API.BlockView<T, Code, Alg>> { if (bytes == null) { throw new Error('Missing required argument "bytes"') } if (codec == null || hasher == null) { throw new Error('Missing required argument: codec or hasher') } const value = codec.decode(bytes) const hash = await hasher.digest(bytes) const cid = CID.create(1, codec.code, hash) as CID<T, Code, Alg, 1> return new Block({ value, bytes, cid }) } type CreateUnsafeInput <T, Code extends number, Alg extends number, V extends API.Version> = { cid: API.Link<T, Code, Alg, V> value: T codec?: API.BlockDecoder<Code, T> bytes: API.ByteView<T> } | { cid: API.Link<T, Code, Alg, V> value?: undefined codec: API.BlockDecoder<Code, T> bytes: API.ByteView<T> } /** * @template T - Logical type of the data encoded in the block * @template Code - multicodec code corresponding to codec used to encode the block * @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation. * @template V - CID version */ export function createUnsafe <T, Code extends number, Alg extends number, V extends API.Version> ({ bytes, cid, value: maybeValue, codec }: CreateUnsafeInput<T, Code, Alg, V>): API.BlockView<T, Code, Alg, V> { const value = maybeValue !== undefined ? maybeValue : (codec?.decode(bytes)) if (value === undefined) { throw new Error('Missing required argument, must either provide "value" or "codec"') } return new Block({ cid: cid as CID<T, Code, Alg, V>, bytes, value }) } interface CreateInput <T, Code extends number, Alg extends number, V extends API.Version> { bytes: API.ByteView<T> cid: API.Link<T, Code, Alg, V> hasher: API.MultihashHasher<Alg> codec: API.BlockDecoder<Code, T> } /** * @template T - Logical type of the data encoded in the block * @template Code - multicodec code corresponding to codec used to encode the block * @template Alg - multicodec code corresponding to the hashing algorithm used in CID creation. * @template V - CID version */ export async function create <T, Code extends number, Alg extends number, V extends API.Version> ({ bytes, cid, hasher, codec }: CreateInput<T, Code, Alg, V>): Promise<API.BlockView<T, Code, Alg, V>> { if (bytes == null) { throw new Error('Missing required argument "bytes"') } if (hasher == null) { throw new Error('Missing required argument "hasher"') } const value = codec.decode(bytes) const hash = await hasher.digest(bytes) if (!binary.equals(cid.multihash.bytes, hash.bytes)) { throw new Error('CID hash does not match bytes') } return createUnsafe({ bytes, cid, value, codec }) }