multiformats
Version:
Interface for multihash, multicodec, multibase and CID
240 lines (209 loc) • 8.27 kB
text/typescript
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
})
}