merkle-reference
Version:
This is a TS library implementing [merkle reference] specification.
225 lines (192 loc) • 5.17 kB
JavaScript
import { base32 } from 'multiformats/bases/base32'
export { base32 }
/**
* Multiformat code for the CIDv1.
* @see https://github.com/multiformats/multicodec/blob/352d05ad430713088e867216152725f581387bc8/table.csv#L3
*/
export const CID_CODE = /** @type {0x01} */ (0x01)
/**
* Multiformat code for the merkle reference.
* @see https://github.com/multiformats/multicodec/pull/357
*/
export const CODE = /** @type {0x07} */ (0x07)
/**
* @see https://github.com/multiformats/multicodec/blob/master/table.csv#L9
*/
export const SHA256_CODE = /** @type {0x12} */ (0x12)
/**
* Sha256 multihash digest size is 32 bytes.
* @see https://github.com/multiformats/rust-multihash/blob/4c0ef5268355308d7f083482dad1c81318db4f6b/codetable/src/hasher_impl.rs#L207
*/
export const DIGEST_SIZE = 32
const PREFIX = [CID_CODE, CODE, SHA256_CODE, DIGEST_SIZE]
export const PREFIX_SIZE = PREFIX.length
const TEMPLATE = new Uint8Array(PREFIX.length + DIGEST_SIZE)
TEMPLATE.set(PREFIX)
/**
* Binary representation of the merkle reference is as follows
*
* ```ts
* [MerkleReference: 0x07,
* SHA256: 0x12,
* Size: 32,
* ...(Uint8Array & {length: 32})
* ]
* ```
*
* So first 3 bytes represent a header and next 32 bytes
* represent the body.
*/
export const SIZE = 3 + DIGEST_SIZE
/**
* @template T
*/
class Reference {
#multihash
#bytes
/** @type {WeakMap<Uint8Array, Reference<unknown>>} */
static cache = new WeakMap()
/**
* @param {Uint8Array} digest
* @param {T} [of]
*/
constructor(digest, of) {
const bytes = TEMPLATE.slice(0)
bytes.set(digest, PREFIX.length)
this.#bytes = bytes
this.#multihash = {
digest: this.bytes.subarray(PREFIX.length),
code: this.bytes[2],
length: this.bytes[3],
}
}
toString() {
return base32.encode(this.bytes.subarray(1))
}
get multihash() {
return this.#multihash
}
get bytes() {
return this.#bytes
}
get ['/']() {
return this.#bytes
}
get version() {
return 1
}
get code() {
return CODE
}
toJSON() {
return toJSON(this)
}
get [Symbol.toStringTag]() {
return `#${this.toString()}`
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return `#${this.toString()}`
}
}
/**
* @template [T=unknown]
* @typedef {Reference<T>} View
*/
/**
* @template {{} | null} T
* @param {unknown|import('./lib.js').Reference<T>} source
* @returns {source is import('./lib.js').Reference<T>}
*/
export const is = (source) => {
const bytes =
/** @type {undefined|null|{['/']?: {[key:PropertyKey]: unknown}|undefined|null}} */
(source)?.['/']
return (
bytes?.[0] === CID_CODE &&
bytes?.[1] === CODE &&
bytes?.[2] === SHA256_CODE &&
bytes?.[3] === DIGEST_SIZE &&
bytes?.length === TEMPLATE.length
)
}
/**
* @param {Uint8Array} digest
*/
export const fromDigest = (digest) => {
const reference = Reference.cache.get(digest)
if (reference) {
return reference
} else if (digest.length !== DIGEST_SIZE) {
throw new RangeError(`Invalid digest size ${digest.length}`)
} else {
const reference = new Reference(digest)
Reference.cache.set(digest, reference)
return reference
}
}
/**
* @template {{}|null} T
* @param {import('./lib.js').Reference<T>} value
*/
export const toTree = (value) => value
/**
* Takes string produced by `refer({}).toString()` and creates equal reference.
* If string is not a valid reference serialization it will return `implicit`
* that was provided. If implicit is not provided an error will be thrown
* instead.
*
* @template {{}|null} T
* @template {{}|null} [Implicit=never]
* @param {string} source
* @param {Implicit} [implicit]
* @returns {import('./lib.js').Reference<T>|Implicit}
*/
export const fromString = (source, implicit) => {
try {
const bytes = base32.decode(source)
return fromBytes(bytes)
} catch (error) {
if (implicit === undefined) {
throw new ReferenceError(`Invalid reference ${source}`)
} else {
return implicit
}
}
}
/**
* @param {Uint8Array} source
*/
export const fromBytes = (source) => {
if (source[0] !== CODE) {
throw new ReferenceError(`Invalid reference ${source}`)
}
if (source[1] !== SHA256_CODE) {
throw new ReferenceError(`Unsupported hashing algorithm ${source[1]}`)
}
if (source[2] !== DIGEST_SIZE) {
throw new ReferenceError(`Invalid digest size ${source[2]}`)
}
if (source.length < SIZE) {
throw new RangeError(`Incomplete Reference byte sequence`)
}
return fromDigest(source.subarray(3, 3 + DIGEST_SIZE))
}
/**
* @param {import('./lib.js').Reference} reference
*/
export const toDigest = (reference) => reference['/'].subarray(PREFIX.length)
/**}
* @param {import('./lib.js').Reference} reference
* @returns
*/
export const toBytes = (reference) => reference['/'].subarray(1)
/**
* @param {import('./lib.js').Reference} reference
*/
export const toJSON = (reference) => ({ '/': base32.encode(reference['/']) })
/**
* @param {{'/': string}} json
*/
export const fromJSON = (json) =>
fromBytes(base32.decode(json['/']).subarray(1))
const MARKER = Symbol('Marker')