diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml index 2c7a14bb..359eb975 100644 --- a/.github/workflows/js-test-and-release.yml +++ b/.github/workflows/js-test-and-release.yml @@ -9,7 +9,9 @@ on: permissions: contents: write + id-token: write packages: write + pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml new file mode 100644 index 00000000..bd00f090 --- /dev/null +++ b/.github/workflows/semantic-pull-request.yml @@ -0,0 +1,12 @@ +name: Semantic PR + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..16d65d72 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,13 @@ +name: Close and mark stale issue + +on: + schedule: + - cron: '0 0 * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml deleted file mode 100644 index ae9947ea..00000000 --- a/.github/workflows/typecheck.yml +++ /dev/null @@ -1,29 +0,0 @@ -on: - push: - branches: - - master - - main - - default - pull_request: - branches: - - "**" - -name: Typecheck -jobs: - check: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [12.x] - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4.0.0 - with: - node-version: ${{ matrix.node-version }} - - name: Install dependencies - run: npm install - - name: Typecheck - uses: gozala/typescript-error-reporter-action@v1.0.9 - with: - project: tsconfig.json diff --git a/.gitignore b/.gitignore index dec11b7f..7ad9e674 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ -.coverage -package-lock.json node_modules -.DS_Store -yarn.lock -types -test/ts-use/tsconfig.tsbuildinfo -types/tsconfig.tsbuildinfo -*.log +build dist .docs +.coverage +node_modules +package-lock.json +yarn.lock +.vscode diff --git a/README.md b/README.md index 1ed02dda..260df917 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,16 @@ -# multiformats - [![multiformats.io](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://multiformats.io) [![codecov](https://img.shields.io/codecov/c/github/multiformats/js-multiformats.svg?style=flat-square)](https://codecov.io/gh/multiformats/js-multiformats) [![CI](https://img.shields.io/github/actions/workflow/status/multiformats/js-multiformats/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/multiformats/js-multiformats/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) > Interface for multihash, multicodec, multibase and CID -## Table of contents - -- [Install](#install) - - [Browser ` -``` - -## Interfaces +# About This library defines common interfaces and low level building blocks for various interrelated multiformat technologies (multicodec, multihash, multibase, and CID). They can be used to implement custom base encoders / decoders / codecs, codec encoders /decoders and multihash hashers that comply to the interface that layers above assume. This library provides implementations for most basics and many others can be found in linked repositories. -```js +```TypeScript import { CID } from 'multiformats/cid' import * as json from 'multiformats/codecs/json' import { sha256 } from 'multiformats/hashes/sha2' @@ -57,9 +22,9 @@ const cid = CID.create(1, json.code, hash) //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) ``` -### Creating Blocks +## Creating Blocks -```js +```TypeScript import * as Block from 'multiformats/block' import * as codec from '@ipld/dag-cbor' import { sha256 as hasher } from 'multiformats/hashes/sha2' @@ -80,11 +45,11 @@ block = await Block.decode({ bytes: block.bytes, codec, hasher }) block = await Block.create({ bytes: block.bytes, cid: block.cid, codec, hasher }) ``` -### Multibase Encoders / Decoders / Codecs +## Multibase Encoders / Decoders / Codecs CIDs can be serialized to string representation using multibase encoders that implement [`MultibaseEncoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. This library provides quite a few implementations that can be imported: -```js +```TypeScript import { base64 } from "multiformats/bases/base64" cid.toString(base64.encoder) //> 'mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA' @@ -92,7 +57,7 @@ cid.toString(base64.encoder) Parsing CID string serialized CIDs requires multibase decoder that implements [`MultibaseDecoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. This library provides a decoder for every encoder it provides: -```js +```TypeScript CID.parse('mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA', base64.decoder) //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) ``` @@ -102,7 +67,7 @@ them as `encoder` and `decoder` properties. For added convenience codecs also implement `MultibaseEncoder` and `MultibaseDecoder` interfaces so they could be used as either or both: -```js +```TypeScript cid.toString(base64) CID.parse(cid.toString(base64), base64) ``` @@ -111,7 +76,7 @@ CID.parse(cid.toString(base64), base64) multibase codecs so that CIDs can be base serialized to (version specific) default base encoding and parsed without having to supply base encoders/decoders: -```js +```TypeScript const v1 = CID.parse('bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea') v1.toString() //> 'bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea' @@ -123,17 +88,13 @@ v0.toV1().toString() //> 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku' ``` -### Multicodec Encoders / Decoders / Codecs +## Multicodec Encoders / Decoders / Codecs This library defines [`BlockEncoder`, `BlockDecoder` and `BlockCodec` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/interface.ts). Codec implementations should conform to the `BlockCodec` interface which implements both `BlockEncoder` and `BlockDecoder`. Here is an example implementation of JSON `BlockCodec`. -```js -/** - * @template T - * @type {BlockCodec<0x0200, T>} - */ +```TypeScript export const { name, code, encode, decode } = { name: 'json', code: 0x0200, @@ -142,11 +103,11 @@ export const { name, code, encode, decode } = { } ``` -### Multihash Hashers +## Multihash Hashers This library defines [`MultihashHasher` and `MultihashDigest` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/hashes/interface.ts) and convinient function for implementing them: -```js +```TypeScript import * as hasher from 'multiformats/hashes/hasher' const sha256 = hasher.from({ @@ -164,13 +125,13 @@ CID.create(1, json.code, hash) //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) ``` -### Traversal +## Traversal This library contains higher-order functions for traversing graphs of data easily. `walk()` walks through the links in each block of a DAG calling a user-supplied loader function for each one, in depth-first order with no duplicate block visits. The loader should return a `Block` object and can be used to inspect and collect block ordering for a full DAG walk. The loader should `throw` on error, and return `null` if a block should be skipped by `walk()`. -```js +```TypeScript import { walk } from 'multiformats/traversal' import * as Block from 'multiformats/block' import * as codec from 'multiformats/codecs/json' @@ -243,17 +204,31 @@ Other (less useful) bases implemented in [multiformats/js-multiformats](https:// | `dag-pb` | `@ipld/dag-pb` | [ipld/js-dag-pb](https://github.com/ipld/js-dag-pb) | | `dag-jose` | `dag-jose` | [ceramicnetwork/js-dag-jose](https://github.com/ceramicnetwork/js-dag-jose) | -## API Docs +# Install + +```console +$ npm i multiformats +``` + +## Browser ` +``` + +# API Docs - -## License +# License Licensed under either of - Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) - MIT ([LICENSE-MIT](LICENSE-MIT) / ) -## Contribution +# Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/package.json b/package.json index 3843b15f..8e9e2df3 100644 --- a/package.json +++ b/package.json @@ -12,178 +12,167 @@ "bugs": { "url": "https://github.com/multiformats/js-multiformats/issues" }, + "publishConfig": { + "access": "public", + "provenance": true + }, "keywords": [ "ipfs", "ipld", "multiformats" ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, "type": "module", - "types": "./dist/types/src/index.d.ts", + "types": "./dist/src/index.d.ts", "typesVersions": { "*": { "*": [ "*", - "dist/types/*", - "dist/types/src/*", - "dist/types/src/*/index" + "dist/*", + "dist/src/*", + "dist/src/*/index" ], "src/*": [ "*", - "dist/types/*", - "dist/types/src/*", - "dist/types/src/*/index" + "dist/*", + "dist/src/*", + "dist/src/*/index" ] } }, "files": [ - "CHANGELOG.md", - "examples", - "LICENSE*", "src", - "test", - "tsconfig.json", "dist", - "vendor", - "!**/*.tsbuildinfo", - "!test/ts-use/node_modules" + "!dist/test", + "!**/*.tsbuildinfo" ], "exports": { ".": { - "types": "./dist/types/src/index.d.ts", - "import": "./src/index.js" + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" }, "./bases/base10": { - "types": "./dist/types/src/bases/base10.d.ts", - "import": "./src/bases/base10.js" + "types": "./dist/src/bases/base10.d.ts", + "import": "./dist/src/bases/base10.js" }, "./bases/base16": { - "types": "./dist/types/src/bases/base16.d.ts", - "import": "./src/bases/base16.js" + "types": "./dist/src/bases/base16.d.ts", + "import": "./dist/src/bases/base16.js" }, "./bases/base2": { - "types": "./dist/types/src/bases/base2.d.ts", - "import": "./src/bases/base2.js" + "types": "./dist/src/bases/base2.d.ts", + "import": "./dist/src/bases/base2.js" }, "./bases/base256emoji": { - "types": "./dist/types/src/bases/base256emoji.d.ts", - "import": "./src/bases/base256emoji.js" + "types": "./dist/src/bases/base256emoji.d.ts", + "import": "./dist/src/bases/base256emoji.js" }, "./bases/base32": { - "types": "./dist/types/src/bases/base32.d.ts", - "import": "./src/bases/base32.js" + "types": "./dist/src/bases/base32.d.ts", + "import": "./dist/src/bases/base32.js" }, "./bases/base36": { - "types": "./dist/types/src/bases/base36.d.ts", - "import": "./src/bases/base36.js" + "types": "./dist/src/bases/base36.d.ts", + "import": "./dist/src/bases/base36.js" }, "./bases/base58": { - "types": "./dist/types/src/bases/base58.d.ts", - "import": "./src/bases/base58.js" + "types": "./dist/src/bases/base58.d.ts", + "import": "./dist/src/bases/base58.js" }, "./bases/base64": { - "types": "./dist/types/src/bases/base64.d.ts", - "import": "./src/bases/base64.js" + "types": "./dist/src/bases/base64.d.ts", + "import": "./dist/src/bases/base64.js" }, "./bases/base8": { - "types": "./dist/types/src/bases/base8.d.ts", - "import": "./src/bases/base8.js" + "types": "./dist/src/bases/base8.d.ts", + "import": "./dist/src/bases/base8.js" }, "./bases/identity": { - "types": "./dist/types/src/bases/identity.d.ts", - "import": "./src/bases/identity.js" + "types": "./dist/src/bases/identity.d.ts", + "import": "./dist/src/bases/identity.js" }, "./bases/interface": { - "types": "./dist/types/src/bases/interface.d.ts", - "import": "./src/bases/interface.js" + "types": "./dist/src/bases/interface.d.ts", + "import": "./dist/src/bases/interface.js" }, "./basics": { - "types": "./dist/types/src/basics.d.ts", - "import": "./src/basics.js" + "types": "./dist/src/basics.d.ts", + "import": "./dist/src/basics.js" }, "./block": { - "types": "./dist/types/src/block.d.ts", - "import": "./src/block.js" + "types": "./dist/src/block.d.ts", + "import": "./dist/src/block.js" }, "./block/interface": { - "types": "./dist/types/src/block/interface.d.ts", - "import": "./src/block/interface.js" + "types": "./dist/src/block/interface.d.ts", + "import": "./dist/src/block/interface.js" }, "./bytes": { - "types": "./dist/types/src/bytes.d.ts", - "import": "./src/bytes.js" + "types": "./dist/src/bytes.d.ts", + "import": "./dist/src/bytes.js" }, "./cid": { - "types": "./dist/types/src/cid.d.ts", - "import": "./src/cid.js" + "types": "./dist/src/cid.d.ts", + "import": "./dist/src/cid.js" }, "./codecs/interface": { - "types": "./dist/types/src/codecs/interface.d.ts", - "import": "./src/codecs/interface.js" + "types": "./dist/src/codecs/interface.d.ts", + "import": "./dist/src/codecs/interface.js" }, "./codecs/json": { - "types": "./dist/types/src/codecs/json.d.ts", - "import": "./src/codecs/json.js" + "types": "./dist/src/codecs/json.d.ts", + "import": "./dist/src/codecs/json.js" }, "./codecs/raw": { - "types": "./dist/types/src/codecs/raw.d.ts", - "import": "./src/codecs/raw.js" + "types": "./dist/src/codecs/raw.d.ts", + "import": "./dist/src/codecs/raw.js" }, "./hashes/digest": { - "types": "./dist/types/src/hashes/digest.d.ts", - "import": "./src/hashes/digest.js" + "types": "./dist/src/hashes/digest.d.ts", + "import": "./dist/src/hashes/digest.js" }, "./hashes/hasher": { - "types": "./dist/types/src/hashes/hasher.d.ts", - "import": "./src/hashes/hasher.js" + "types": "./dist/src/hashes/hasher.d.ts", + "import": "./dist/src/hashes/hasher.js" }, "./hashes/identity": { - "types": "./dist/types/src/hashes/identity.d.ts", - "import": "./src/hashes/identity.js" + "types": "./dist/src/hashes/identity.d.ts", + "import": "./dist/src/hashes/identity.js" }, "./hashes/interface": { - "types": "./dist/types/src/hashes/interface.d.ts", - "import": "./src/hashes/interface.js" + "types": "./dist/src/hashes/interface.d.ts", + "import": "./dist/src/hashes/interface.js" }, "./hashes/sha1": { "types": "./dist/types/src/hashes/sha1.d.ts", - "browser": "./src/hashes/sha1-browser.js", - "import": "./src/hashes/sha1.js" + "browser": "./dist/src/hashes/sha1-browser.js", + "import": "./dist/src/hashes/sha1.js" }, "./hashes/sha2": { - "types": "./dist/types/src/hashes/sha2.d.ts", - "browser": "./src/hashes/sha2-browser.js", - "import": "./src/hashes/sha2.js" + "types": "./dist/src/hashes/sha2.d.ts", + "browser": "./dist/src/hashes/sha2-browser.js", + "import": "./dist/src/hashes/sha2.js" }, "./interface": { - "types": "./dist/types/src/interface.d.ts", - "import": "./src/interface.js" + "types": "./dist/src/interface.d.ts", + "import": "./dist/src/interface.js" }, "./link": { - "types": "./dist/types/src/link.d.ts", - "import": "./src/link.js" + "types": "./dist/src/link.d.ts", + "import": "./dist/src/link.js" }, "./link/interface": { - "types": "./dist/types/src/link/interface.d.ts", - "import": "./src/link/interface.js" + "types": "./dist/src/link/interface.d.ts", + "import": "./dist/src/link/interface.js" }, "./traversal": { - "types": "./dist/types/src/traversal.d.ts", - "import": "./src/traversal.js" + "types": "./dist/src/traversal.d.ts", + "import": "./dist/src/traversal.js" } }, - "browser": { - "./hashes/sha1": "./src/hashes/sha1-browser.js", - "./src/hashes/sha1.js": "./src/hashes/sha1-browser.js", - "./hashes/sha2": "./src/hashes/sha2-browser.js", - "./src/hashes/sha2.js": "./src/hashes/sha2-browser.js" - }, "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -278,8 +267,7 @@ "build": "aegir build", "release": "aegir release", "docs": "aegir docs", - "test": "npm run lint && npm run test:node && npm run test:chrome && npm run test:ts", - "test:ts": "npm run test --prefix test/ts-use", + "test": "npm run lint && npm run test:node && npm run test:chrome", "test:node": "aegir test -t node --cov", "test:chrome": "aegir test -t browser --cov", "test:chrome-webworker": "aegir test -t webworker", @@ -303,5 +291,11 @@ "browser" ] } + }, + "browser": { + "./hashes/sha1": "./dist/src/hashes/sha1-browser.js", + "./dist/src/hashes/sha1.js": "./dist/src/hashes/sha1-browser.js", + "./hashes/sha2": "./dist/src/hashes/sha2-browser.js", + "./dist/src/hashes/sha2.js": "./dist/src/hashes/sha2-browser.js" } } diff --git a/src/bases/base.js b/src/bases/base.js deleted file mode 100644 index 4ace5ff4..00000000 --- a/src/bases/base.js +++ /dev/null @@ -1,347 +0,0 @@ -import basex from '../../vendor/base-x.js' -import { coerce } from '../bytes.js' -// Linter can't see that API is used in types. -// eslint-disable-next-line -import * as API from './interface.js' - -/** - * Class represents both BaseEncoder and MultibaseEncoder meaning it - * can be used to encode to multibase or base encode without multibase - * prefix. - * - * @class - * @template {string} Base - * @template {string} Prefix - * @implements {API.MultibaseEncoder} - * @implements {API.BaseEncoder} - */ -class Encoder { - /** - * @param {Base} name - * @param {Prefix} prefix - * @param {(bytes:Uint8Array) => string} baseEncode - */ - constructor (name, prefix, baseEncode) { - this.name = name - this.prefix = prefix - this.baseEncode = baseEncode - } - - /** - * @param {Uint8Array} bytes - * @returns {API.Multibase} - */ - encode (bytes) { - if (bytes instanceof Uint8Array) { - return `${this.prefix}${this.baseEncode(bytes)}` - } else { - throw Error('Unknown type, must be binary type') - } - } -} - -/** - * @template {string} Prefix - */ -/** - * Class represents both BaseDecoder and MultibaseDecoder so it could be used - * to decode multibases (with matching prefix) or just base decode strings - * with corresponding base encoding. - * - * @class - * @template {string} Base - * @template {string} Prefix - * @implements {API.MultibaseDecoder} - * @implements {API.UnibaseDecoder} - * @implements {API.BaseDecoder} - */ -class Decoder { - /** - * @param {Base} name - * @param {Prefix} prefix - * @param {(text:string) => Uint8Array} baseDecode - */ - constructor (name, prefix, baseDecode) { - this.name = name - this.prefix = prefix - /* c8 ignore next 3 */ - if (prefix.codePointAt(0) === undefined) { - throw new Error('Invalid prefix character') - } - /** @private */ - this.prefixCodePoint = /** @type {number} */ (prefix.codePointAt(0)) - this.baseDecode = baseDecode - } - - /** - * @param {string} text - */ - decode (text) { - if (typeof text === 'string') { - if (text.codePointAt(0) !== this.prefixCodePoint) { - throw Error(`Unable to decode multibase string ${JSON.stringify(text)}, ${this.name} decoder only supports inputs prefixed with ${this.prefix}`) - } - return this.baseDecode(text.slice(this.prefix.length)) - } else { - throw Error('Can only multibase decode strings') - } - } - - /** - * @template {string} OtherPrefix - * @param {API.UnibaseDecoder|ComposedDecoder} decoder - * @returns {ComposedDecoder} - */ - or (decoder) { - return or(this, decoder) - } -} - -/** - * @template {string} Prefix - * @typedef {Record>} Decoders - */ - -/** - * @template {string} Prefix - * @implements {API.MultibaseDecoder} - * @implements {API.CombobaseDecoder} - */ -class ComposedDecoder { - /** - * @param {Decoders} decoders - */ - constructor (decoders) { - this.decoders = decoders - } - - /** - * @template {string} OtherPrefix - * @param {API.UnibaseDecoder|ComposedDecoder} decoder - * @returns {ComposedDecoder} - */ - or (decoder) { - return or(this, decoder) - } - - /** - * @param {string} input - * @returns {Uint8Array} - */ - decode (input) { - const prefix = /** @type {Prefix} */ (input[0]) - const decoder = this.decoders[prefix] - if (decoder) { - return decoder.decode(input) - } else { - throw RangeError(`Unable to decode multibase string ${JSON.stringify(input)}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) - } - } -} - -/** - * @template {string} L - * @template {string} R - * @param {API.UnibaseDecoder|API.CombobaseDecoder} left - * @param {API.UnibaseDecoder|API.CombobaseDecoder} right - * @returns {ComposedDecoder} - */ -export const or = (left, right) => new ComposedDecoder(/** @type {Decoders} */({ - ...(left.decoders || { [/** @type API.UnibaseDecoder */(left).prefix]: left }), - ...(right.decoders || { [/** @type API.UnibaseDecoder */(right).prefix]: right }) -})) - -/** - * @class - * @template {string} Base - * @template {string} Prefix - * @implements {API.MultibaseCodec} - * @implements {API.MultibaseEncoder} - * @implements {API.MultibaseDecoder} - * @implements {API.BaseCodec} - * @implements {API.BaseEncoder} - * @implements {API.BaseDecoder} - */ -export class Codec { - /** - * @param {Base} name - * @param {Prefix} prefix - * @param {(bytes:Uint8Array) => string} baseEncode - * @param {(text:string) => Uint8Array} baseDecode - */ - constructor (name, prefix, baseEncode, baseDecode) { - this.name = name - this.prefix = prefix - this.baseEncode = baseEncode - this.baseDecode = baseDecode - this.encoder = new Encoder(name, prefix, baseEncode) - this.decoder = new Decoder(name, prefix, baseDecode) - } - - /** - * @param {Uint8Array} input - */ - encode (input) { - return this.encoder.encode(input) - } - - /** - * @param {string} input - */ - decode (input) { - return this.decoder.decode(input) - } -} - -/** - * @template {string} Base - * @template {string} Prefix - * @param {object} options - * @param {Base} options.name - * @param {Prefix} options.prefix - * @param {(bytes:Uint8Array) => string} options.encode - * @param {(input:string) => Uint8Array} options.decode - * @returns {Codec} - */ -export const from = ({ name, prefix, encode, decode }) => - new Codec(name, prefix, encode, decode) - -/** - * @template {string} Base - * @template {string} Prefix - * @param {object} options - * @param {Base} options.name - * @param {Prefix} options.prefix - * @param {string} options.alphabet - * @returns {Codec} - */ -export const baseX = ({ prefix, name, alphabet }) => { - const { encode, decode } = basex(alphabet, name) - return from({ - prefix, - name, - encode, - /** - * @param {string} text - */ - decode: text => coerce(decode(text)) - }) -} - -/** - * @param {string} string - * @param {string} alphabet - * @param {number} bitsPerChar - * @param {string} name - * @returns {Uint8Array} - */ -const decode = (string, alphabet, bitsPerChar, name) => { - // Build the character lookup table: - /** @type {Record} */ - const codes = {} - for (let i = 0; i < alphabet.length; ++i) { - codes[alphabet[i]] = i - } - - // Count the padding bytes: - let end = string.length - while (string[end - 1] === '=') { - --end - } - - // Allocate the output: - const out = new Uint8Array((end * bitsPerChar / 8) | 0) - - // Parse the data: - let bits = 0 // Number of bits currently in the buffer - let buffer = 0 // Bits waiting to be written out, MSB first - let written = 0 // Next byte to write - for (let i = 0; i < end; ++i) { - // Read one character from the string: - const value = codes[string[i]] - if (value === undefined) { - throw new SyntaxError(`Non-${name} character`) - } - - // Append the bits to the buffer: - buffer = (buffer << bitsPerChar) | value - bits += bitsPerChar - - // Write out some bits if the buffer has a byte's worth: - if (bits >= 8) { - bits -= 8 - out[written++] = 0xff & (buffer >> bits) - } - } - - // Verify that we have received just enough bits: - if (bits >= bitsPerChar || 0xff & (buffer << (8 - bits))) { - throw new SyntaxError('Unexpected end of data') - } - - return out -} - -/** - * @param {Uint8Array} data - * @param {string} alphabet - * @param {number} bitsPerChar - * @returns {string} - */ -const encode = (data, alphabet, bitsPerChar) => { - const pad = alphabet[alphabet.length - 1] === '=' - const mask = (1 << bitsPerChar) - 1 - let out = '' - - let bits = 0 // Number of bits currently in the buffer - let buffer = 0 // Bits waiting to be written out, MSB first - for (let i = 0; i < data.length; ++i) { - // Slurp data into the buffer: - buffer = (buffer << 8) | data[i] - bits += 8 - - // Write out as much as we can: - while (bits > bitsPerChar) { - bits -= bitsPerChar - out += alphabet[mask & (buffer >> bits)] - } - } - - // Partial character: - if (bits) { - out += alphabet[mask & (buffer << (bitsPerChar - bits))] - } - - // Add padding characters until we hit a byte boundary: - if (pad) { - while ((out.length * bitsPerChar) & 7) { - out += '=' - } - } - - return out -} - -/** - * RFC4648 Factory - * - * @template {string} Base - * @template {string} Prefix - * @param {object} options - * @param {Base} options.name - * @param {Prefix} options.prefix - * @param {string} options.alphabet - * @param {number} options.bitsPerChar - */ -export const rfc4648 = ({ name, prefix, bitsPerChar, alphabet }) => { - return from({ - prefix, - name, - encode (input) { - return encode(input, alphabet, bitsPerChar) - }, - decode (input) { - return decode(input, alphabet, bitsPerChar, name) - } - }) -} diff --git a/src/bases/base.ts b/src/bases/base.ts new file mode 100644 index 00000000..03be1c84 --- /dev/null +++ b/src/bases/base.ts @@ -0,0 +1,237 @@ +import { coerce } from '../bytes.js' +import basex from '../vendor/base-x.js' +import type { BaseCodec, BaseDecoder, BaseEncoder, CombobaseDecoder, Multibase, MultibaseCodec, MultibaseDecoder, MultibaseEncoder, UnibaseDecoder } from './interface.js' + +interface EncodeFn { (bytes: Uint8Array): string } +interface DecodeFn { (text: string): Uint8Array } + +/** + * Class represents both BaseEncoder and MultibaseEncoder meaning it + * can be used to encode to multibase or base encode without multibase + * prefix. + */ +class Encoder implements MultibaseEncoder, BaseEncoder { + readonly name: Base + readonly prefix: Prefix + readonly baseEncode: EncodeFn + + constructor (name: Base, prefix: Prefix, baseEncode: EncodeFn) { + this.name = name + this.prefix = prefix + this.baseEncode = baseEncode + } + + encode (bytes: Uint8Array): Multibase { + if (bytes instanceof Uint8Array) { + return `${this.prefix}${this.baseEncode(bytes)}` + } else { + throw Error('Unknown type, must be binary type') + } + } +} + +/** + * Class represents both BaseDecoder and MultibaseDecoder so it could be used + * to decode multibases (with matching prefix) or just base decode strings + * with corresponding base encoding. + */ +class Decoder implements MultibaseDecoder, UnibaseDecoder, BaseDecoder { + readonly name: Base + readonly prefix: Prefix + readonly baseDecode: DecodeFn + private readonly prefixCodePoint: number + + constructor (name: Base, prefix: Prefix, baseDecode: DecodeFn) { + this.name = name + this.prefix = prefix + /* c8 ignore next 3 */ + if (prefix.codePointAt(0) === undefined) { + throw new Error('Invalid prefix character') + } + this.prefixCodePoint = prefix.codePointAt(0) as number + this.baseDecode = baseDecode + } + + decode (text: string): Uint8Array { + if (typeof text === 'string') { + if (text.codePointAt(0) !== this.prefixCodePoint) { + throw Error(`Unable to decode multibase string ${JSON.stringify(text)}, ${this.name} decoder only supports inputs prefixed with ${this.prefix}`) + } + return this.baseDecode(text.slice(this.prefix.length)) + } else { + throw Error('Can only multibase decode strings') + } + } + + or (decoder: UnibaseDecoder | ComposedDecoder): ComposedDecoder { + return or(this, decoder) + } +} + +type Decoders = Record> + +class ComposedDecoder implements MultibaseDecoder, CombobaseDecoder { + readonly decoders: Decoders + + constructor (decoders: Decoders) { + this.decoders = decoders + } + + or (decoder: UnibaseDecoder | ComposedDecoder): ComposedDecoder { + return or(this, decoder) + } + + decode (input: string): Uint8Array { + const prefix = input[0] as Prefix + const decoder = this.decoders[prefix] + if (decoder != null) { + return decoder.decode(input) + } else { + throw RangeError(`Unable to decode multibase string ${JSON.stringify(input)}, only inputs prefixed with ${Object.keys(this.decoders)} are supported`) + } + } +} + +export function or (left: UnibaseDecoder | CombobaseDecoder, right: UnibaseDecoder | CombobaseDecoder): ComposedDecoder { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return new ComposedDecoder({ + ...(left.decoders ?? { [(left as UnibaseDecoder).prefix]: left }), + ...(right.decoders ?? { [(right as UnibaseDecoder).prefix]: right }) + } as Decoders) +} + +export class Codec implements MultibaseCodec, MultibaseEncoder, MultibaseDecoder, BaseCodec, BaseEncoder, BaseDecoder { + readonly name: Base + readonly prefix: Prefix + readonly baseEncode: EncodeFn + readonly baseDecode: DecodeFn + readonly encoder: Encoder + readonly decoder: Decoder + + constructor (name: Base, prefix: Prefix, baseEncode: EncodeFn, baseDecode: DecodeFn) { + this.name = name + this.prefix = prefix + this.baseEncode = baseEncode + this.baseDecode = baseDecode + this.encoder = new Encoder(name, prefix, baseEncode) + this.decoder = new Decoder(name, prefix, baseDecode) + } + + encode (input: Uint8Array): string { + return this.encoder.encode(input) + } + + decode (input: string): Uint8Array { + return this.decoder.decode(input) + } +} + +export function from ({ name, prefix, encode, decode }: { name: Base, prefix: Prefix, encode: EncodeFn, decode: DecodeFn }): Codec { + return new Codec(name, prefix, encode, decode) +} + +export function baseX ({ name, prefix, alphabet }: { name: Base, prefix: Prefix, alphabet: string }): Codec { + const { encode, decode } = basex(alphabet, name) + return from({ + prefix, + name, + encode, + decode: (text: string): Uint8Array => coerce(decode(text)) + }) +} + +function decode (string: string, alphabet: string, bitsPerChar: number, name: string): Uint8Array { + // Build the character lookup table: + const codes: Record = {} + for (let i = 0; i < alphabet.length; ++i) { + codes[alphabet[i]] = i + } + + // Count the padding bytes: + let end = string.length + while (string[end - 1] === '=') { + --end + } + + // Allocate the output: + const out = new Uint8Array((end * bitsPerChar / 8) | 0) + + // Parse the data: + let bits = 0 // Number of bits currently in the buffer + let buffer = 0 // Bits waiting to be written out, MSB first + let written = 0 // Next byte to write + for (let i = 0; i < end; ++i) { + // Read one character from the string: + const value = codes[string[i]] + if (value === undefined) { + throw new SyntaxError(`Non-${name} character`) + } + + // Append the bits to the buffer: + buffer = (buffer << bitsPerChar) | value + bits += bitsPerChar + + // Write out some bits if the buffer has a byte's worth: + if (bits >= 8) { + bits -= 8 + out[written++] = 0xff & (buffer >> bits) + } + } + + // Verify that we have received just enough bits: + if (bits >= bitsPerChar || (0xff & (buffer << (8 - bits))) !== 0) { + throw new SyntaxError('Unexpected end of data') + } + + return out +} + +function encode (data: Uint8Array, alphabet: string, bitsPerChar: number): string { + const pad = alphabet[alphabet.length - 1] === '=' + const mask = (1 << bitsPerChar) - 1 + let out = '' + + let bits = 0 // Number of bits currently in the buffer + let buffer = 0 // Bits waiting to be written out, MSB first + for (let i = 0; i < data.length; ++i) { + // Slurp data into the buffer: + buffer = (buffer << 8) | data[i] + bits += 8 + + // Write out as much as we can: + while (bits > bitsPerChar) { + bits -= bitsPerChar + out += alphabet[mask & (buffer >> bits)] + } + } + + // Partial character: + if (bits !== 0) { + out += alphabet[mask & (buffer << (bitsPerChar - bits))] + } + + // Add padding characters until we hit a byte boundary: + if (pad) { + while (((out.length * bitsPerChar) & 7) !== 0) { + out += '=' + } + } + + return out +} + +/** + * RFC4648 Factory + */ +export function rfc4648 ({ name, prefix, bitsPerChar, alphabet }: { name: Base, prefix: Prefix, bitsPerChar: number, alphabet: string }): Codec { + return from({ + prefix, + name, + encode (input: Uint8Array): string { + return encode(input, alphabet, bitsPerChar) + }, + decode (input: string): Uint8Array { + return decode(input, alphabet, bitsPerChar, name) + } + }) +} diff --git a/src/bases/base10.js b/src/bases/base10.ts similarity index 100% rename from src/bases/base10.js rename to src/bases/base10.ts diff --git a/src/bases/base16.js b/src/bases/base16.ts similarity index 95% rename from src/bases/base16.js rename to src/bases/base16.ts index 394a8ca3..8e7b2380 100644 --- a/src/bases/base16.js +++ b/src/bases/base16.ts @@ -1,5 +1,3 @@ -// @ts-check - import { rfc4648 } from './base.js' export const base16 = rfc4648({ diff --git a/src/bases/base2.js b/src/bases/base2.ts similarity index 90% rename from src/bases/base2.js rename to src/bases/base2.ts index e32baa32..2ff4977d 100644 --- a/src/bases/base2.js +++ b/src/bases/base2.ts @@ -1,5 +1,3 @@ -// @ts-check - import { rfc4648 } from './base.js' export const base2 = rfc4648({ diff --git a/src/bases/base256emoji.js b/src/bases/base256emoji.ts similarity index 72% rename from src/bases/base256emoji.js rename to src/bases/base256emoji.ts index 957c81f8..d17a44c2 100644 --- a/src/bases/base256emoji.js +++ b/src/bases/base256emoji.ts @@ -1,28 +1,20 @@ import { from } from './base.js' const alphabet = Array.from('๐Ÿš€๐Ÿชโ˜„๐Ÿ›ฐ๐ŸŒŒ๐ŸŒ‘๐ŸŒ’๐ŸŒ“๐ŸŒ”๐ŸŒ•๐ŸŒ–๐ŸŒ—๐ŸŒ˜๐ŸŒ๐ŸŒ๐ŸŒŽ๐Ÿ‰โ˜€๐Ÿ’ป๐Ÿ–ฅ๐Ÿ’พ๐Ÿ’ฟ๐Ÿ˜‚โค๐Ÿ˜๐Ÿคฃ๐Ÿ˜Š๐Ÿ™๐Ÿ’•๐Ÿ˜ญ๐Ÿ˜˜๐Ÿ‘๐Ÿ˜…๐Ÿ‘๐Ÿ˜๐Ÿ”ฅ๐Ÿฅฐ๐Ÿ’”๐Ÿ’–๐Ÿ’™๐Ÿ˜ข๐Ÿค”๐Ÿ˜†๐Ÿ™„๐Ÿ’ช๐Ÿ˜‰โ˜บ๐Ÿ‘Œ๐Ÿค—๐Ÿ’œ๐Ÿ˜”๐Ÿ˜Ž๐Ÿ˜‡๐ŸŒน๐Ÿคฆ๐ŸŽ‰๐Ÿ’žโœŒโœจ๐Ÿคท๐Ÿ˜ฑ๐Ÿ˜Œ๐ŸŒธ๐Ÿ™Œ๐Ÿ˜‹๐Ÿ’—๐Ÿ’š๐Ÿ˜๐Ÿ’›๐Ÿ™‚๐Ÿ’“๐Ÿคฉ๐Ÿ˜„๐Ÿ˜€๐Ÿ–ค๐Ÿ˜ƒ๐Ÿ’ฏ๐Ÿ™ˆ๐Ÿ‘‡๐ŸŽถ๐Ÿ˜’๐Ÿคญโฃ๐Ÿ˜œ๐Ÿ’‹๐Ÿ‘€๐Ÿ˜ช๐Ÿ˜‘๐Ÿ’ฅ๐Ÿ™‹๐Ÿ˜ž๐Ÿ˜ฉ๐Ÿ˜ก๐Ÿคช๐Ÿ‘Š๐Ÿฅณ๐Ÿ˜ฅ๐Ÿคค๐Ÿ‘‰๐Ÿ’ƒ๐Ÿ˜ณโœ‹๐Ÿ˜š๐Ÿ˜๐Ÿ˜ด๐ŸŒŸ๐Ÿ˜ฌ๐Ÿ™ƒ๐Ÿ€๐ŸŒท๐Ÿ˜ป๐Ÿ˜“โญโœ…๐Ÿฅบ๐ŸŒˆ๐Ÿ˜ˆ๐Ÿค˜๐Ÿ’ฆโœ”๐Ÿ˜ฃ๐Ÿƒ๐Ÿ’โ˜น๐ŸŽŠ๐Ÿ’˜๐Ÿ˜ โ˜๐Ÿ˜•๐ŸŒบ๐ŸŽ‚๐ŸŒป๐Ÿ˜๐Ÿ–•๐Ÿ’๐Ÿ™Š๐Ÿ˜น๐Ÿ—ฃ๐Ÿ’ซ๐Ÿ’€๐Ÿ‘‘๐ŸŽต๐Ÿคž๐Ÿ˜›๐Ÿ”ด๐Ÿ˜ค๐ŸŒผ๐Ÿ˜ซโšฝ๐Ÿค™โ˜•๐Ÿ†๐Ÿคซ๐Ÿ‘ˆ๐Ÿ˜ฎ๐Ÿ™†๐Ÿป๐Ÿƒ๐Ÿถ๐Ÿ’๐Ÿ˜ฒ๐ŸŒฟ๐Ÿงก๐ŸŽโšก๐ŸŒž๐ŸŽˆโŒโœŠ๐Ÿ‘‹๐Ÿ˜ฐ๐Ÿคจ๐Ÿ˜ถ๐Ÿค๐Ÿšถ๐Ÿ’ฐ๐Ÿ“๐Ÿ’ข๐ŸคŸ๐Ÿ™๐Ÿšจ๐Ÿ’จ๐Ÿคฌโœˆ๐ŸŽ€๐Ÿบ๐Ÿค“๐Ÿ˜™๐Ÿ’Ÿ๐ŸŒฑ๐Ÿ˜–๐Ÿ‘ถ๐Ÿฅดโ–ถโžกโ“๐Ÿ’Ž๐Ÿ’ธโฌ‡๐Ÿ˜จ๐ŸŒš๐Ÿฆ‹๐Ÿ˜ท๐Ÿ•บโš ๐Ÿ™…๐Ÿ˜Ÿ๐Ÿ˜ต๐Ÿ‘Ž๐Ÿคฒ๐Ÿค ๐Ÿคง๐Ÿ“Œ๐Ÿ”ต๐Ÿ’…๐Ÿง๐Ÿพ๐Ÿ’๐Ÿ˜—๐Ÿค‘๐ŸŒŠ๐Ÿคฏ๐Ÿทโ˜Ž๐Ÿ’ง๐Ÿ˜ฏ๐Ÿ’†๐Ÿ‘†๐ŸŽค๐Ÿ™‡๐Ÿ‘โ„๐ŸŒด๐Ÿ’ฃ๐Ÿธ๐Ÿ’Œ๐Ÿ“๐Ÿฅ€๐Ÿคข๐Ÿ‘…๐Ÿ’ก๐Ÿ’ฉ๐Ÿ‘๐Ÿ“ธ๐Ÿ‘ป๐Ÿค๐Ÿคฎ๐ŸŽผ๐Ÿฅต๐Ÿšฉ๐ŸŽ๐ŸŠ๐Ÿ‘ผ๐Ÿ’๐Ÿ“ฃ๐Ÿฅ‚') -const alphabetBytesToChars = /** @type {string[]} */ (alphabet.reduce((p, c, i) => { p[i] = c; return p }, /** @type {string[]} */([]))) -const alphabetCharsToBytes = /** @type {number[]} */ (alphabet.reduce((p, c, i) => { p[/** @type {number} */ (c.codePointAt(0))] = i; return p }, /** @type {number[]} */([]))) +const alphabetBytesToChars: string[] = (alphabet.reduce((p, c, i) => { p[i] = c; return p }, ([]))) +const alphabetCharsToBytes: number[] = (alphabet.reduce((p, c, i) => { p[c.codePointAt(0) as number] = i; return p }, ([]))) -/** - * @param {Uint8Array} data - * @returns {string} - */ -function encode (data) { +function encode (data: Uint8Array): string { return data.reduce((p, c) => { p += alphabetBytesToChars[c] return p }, '') } -/** - * @param {string} str - * @returns {Uint8Array} - */ -function decode (str) { +function decode (str: string): Uint8Array { const byts = [] for (const char of str) { - const byt = alphabetCharsToBytes[/** @type {number} */ (char.codePointAt(0))] + const byt = alphabetCharsToBytes[char.codePointAt(0) as number] if (byt === undefined) { throw new Error(`Non-base256emoji character: ${char}`) } diff --git a/src/bases/base32.js b/src/bases/base32.ts similarity index 100% rename from src/bases/base32.js rename to src/bases/base32.ts diff --git a/src/bases/base36.js b/src/bases/base36.ts similarity index 100% rename from src/bases/base36.js rename to src/bases/base36.ts diff --git a/src/bases/base58.js b/src/bases/base58.ts similarity index 100% rename from src/bases/base58.js rename to src/bases/base58.ts diff --git a/src/bases/base64.js b/src/bases/base64.ts similarity index 98% rename from src/bases/base64.js rename to src/bases/base64.ts index 4fe40f9e..6e66d933 100644 --- a/src/bases/base64.js +++ b/src/bases/base64.ts @@ -1,5 +1,3 @@ -// @ts-check - import { rfc4648 } from './base.js' export const base64 = rfc4648({ diff --git a/src/bases/base8.js b/src/bases/base8.ts similarity index 91% rename from src/bases/base8.js rename to src/bases/base8.ts index caa50f7c..7c584a57 100644 --- a/src/bases/base8.js +++ b/src/bases/base8.ts @@ -1,5 +1,3 @@ -// @ts-check - import { rfc4648 } from './base.js' export const base8 = rfc4648({ diff --git a/src/bases/identity.js b/src/bases/identity.ts similarity index 94% rename from src/bases/identity.js rename to src/bases/identity.ts index 5db2a0ca..301ac7a8 100644 --- a/src/bases/identity.js +++ b/src/bases/identity.ts @@ -1,5 +1,3 @@ -// @ts-check - import { fromString, toString } from '../bytes.js' import { from } from './base.js' diff --git a/src/bases/interface.js b/src/bases/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/bases/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/bases/interface.ts b/src/bases/interface.ts index ba3bddbe..f0b63abe 100644 --- a/src/bases/interface.ts +++ b/src/bases/interface.ts @@ -8,8 +8,6 @@ export interface BaseEncoder { /** * Base encodes to a **plain** (and not a multibase) string. Unlike * `encode` no multibase prefix is added. - * - * @param bytes */ baseEncode(bytes: Uint8Array): string } @@ -21,8 +19,6 @@ export interface BaseDecoder { /** * Decodes **plain** (and not a multibase) string. Unlike * decode - * - * @param text */ baseDecode(text: string): Uint8Array } @@ -73,8 +69,6 @@ export interface MultibaseDecoder { /** * Decodes **multibase** string (which must have a multibase prefix added). * If prefix does not match - * - * @param multibase */ decode(multibase: Multibase): Uint8Array } diff --git a/src/basics.js b/src/basics.ts similarity index 70% rename from src/basics.js rename to src/basics.ts index fde44152..11b45ed1 100644 --- a/src/basics.js +++ b/src/basics.ts @@ -1,5 +1,3 @@ -// @ts-check - import * as base10 from './bases/base10.js' import * as base16 from './bases/base16.js' import * as base2 from './bases/base2.js' @@ -16,8 +14,8 @@ import * as identity from './hashes/identity.js' import * as sha2 from './hashes/sha2.js' import { CID, hasher, digest, varint, bytes } from './index.js' -const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base58, ...base64, ...base256emoji } -const hashes = { ...sha2, ...identity } -const codecs = { raw, json } +export const bases = { ...identityBase, ...base2, ...base8, ...base10, ...base16, ...base32, ...base36, ...base58, ...base64, ...base256emoji } +export const hashes = { ...sha2, ...identity } +export const codecs = { raw, json } -export { CID, hasher, digest, varint, bytes, hashes, bases, codecs } +export { CID, hasher, digest, varint, bytes } diff --git a/src/block.js b/src/block.js deleted file mode 100644 index 16adbec5..00000000 --- a/src/block.js +++ /dev/null @@ -1,272 +0,0 @@ -import { bytes as binary, CID } from './index.js' -// Linter can see that API is used in types. -// eslint-disable-next-line -import * as API from './interface.js' - -function readonly ({ enumerable = true, configurable = false } = {}) { - return { enumerable, configurable, writable: false } -} - -/** - * @param {[string|number, string]} path - * @param {any} value - * @returns {Iterable<[string, CID]>} - */ -function * linksWithin (path, value) { - 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) { - yield [elementPath.join('/'), cid] - } else if (typeof element === 'object') { - yield * links(element, elementPath) - } - } - } else { - const cid = CID.asCID(value) - if (cid) { - yield [path.join('/'), cid] - } else { - yield * links(value, path) - } - } - } -} - -/** - * @template T - * @param {T} source - * @param {Array} base - * @returns {Iterable<[string, CID]>} - */ -function * links (source, base) { - if (source == null || source instanceof Uint8Array) { - return - } - const cid = CID.asCID(source) - if (cid) { - yield [base.join('/'), cid] - } - for (const [key, value] of Object.entries(source)) { - const path = /** @type {[string|number, string]} */ ([...base, key]) - yield * linksWithin(path, value) - } -} - -/** - * @param {[string|number, string]} path - * @param {any} value - * @returns {Iterable} - */ -function * treeWithin (path, value) { - 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)) { - yield * tree(element, elementPath) - } - } - } else { - yield * tree(value, path) - } -} - -/** - * @template T - * @param {T} source - * @param {Array} base - * @returns {Iterable} - */ -function * tree (source, base) { - if (source == null || typeof source !== 'object') { - return - } - for (const [key, value] of Object.entries(source)) { - const path = /** @type {[string|number, string]} */ ([...base, key]) - yield path.join('/') - if (value != null && !(value instanceof Uint8Array) && typeof value === 'object' && !CID.asCID(value)) { - yield * treeWithin(path, value) - } - } -} - -/** - * - * @template T - * @param {T} source - * @param {string[]} path - * @returns {API.BlockCursorView} - */ -function get (source, path) { - let node = /** @type {Record} */(source) - 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) { - return { value: cid, remaining: path.slice(index + 1).join('/') } - } - } - return { value: node } -} - -/** - * @template {unknown} T - Logical type of the data encoded in the block - * @template {number} C - multicodec code corresponding to codec used to encode the block - * @template {number} A - multicodec code corresponding to the hashing algorithm used in CID creation. - * @template {API.Version} V - CID version - * @implements {API.BlockView} - */ -class Block { - /** - * @param {object} options - * @param {CID} options.cid - * @param {API.ByteView} options.bytes - * @param {T} options.value - */ - constructor ({ cid, bytes, value }) { - if (!cid || !bytes || 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 () { - return links(this.value, []) - } - - tree () { - return tree(this.value, []) - } - - /** - * - * @param {string} [path] - * @returns {API.BlockCursorView} - */ - get (path = '/') { - return get(this.value, path.split('/').filter(Boolean)) - } -} - -/** - * @template {unknown} T - Logical type of the data encoded in the block - * @template {number} Code - multicodec code corresponding to codec used to encode the block - * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. - * @param {object} options - * @param {T} options.value - * @param {API.BlockEncoder} options.codec - * @param {API.MultihashHasher} options.hasher - * @returns {Promise>} - */ -async function encode ({ value, codec, hasher }) { - if (typeof value === 'undefined') throw new Error('Missing required argument "value"') - if (!codec || !hasher) throw new Error('Missing required argument: codec or hasher') - - const bytes = codec.encode(value) - const hash = await hasher.digest(bytes) - /** @type {CID} */ - const cid = CID.create( - 1, - codec.code, - hash - ) - - return new Block({ value, bytes, cid }) -} - -/** - * @template {unknown} T - Logical type of the data encoded in the block - * @template {number} Code - multicodec code corresponding to codec used to encode the block - * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. - * @param {object} options - * @param {API.ByteView} options.bytes - * @param {API.BlockDecoder} options.codec - * @param {API.MultihashHasher} options.hasher - * @returns {Promise>} - */ -async function decode ({ bytes, codec, hasher }) { - if (!bytes) throw new Error('Missing required argument "bytes"') - if (!codec || !hasher) throw new Error('Missing required argument: codec or hasher') - - const value = codec.decode(bytes) - const hash = await hasher.digest(bytes) - /** @type {CID} */ - const cid = CID.create(1, codec.code, hash) - - return new Block({ value, bytes, cid }) -} - -/** - * @typedef {object} RequiredCreateOptions - * @property {CID} options.cid - */ - -/** - * @template {unknown} T - Logical type of the data encoded in the block - * @template {number} Code - multicodec code corresponding to codec used to encode the block - * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. - * @template {API.Version} V - CID version - * @param {{ cid: API.Link, value:T, codec?: API.BlockDecoder, bytes: API.ByteView }|{cid:API.Link, bytes:API.ByteView, value?:void, codec:API.BlockDecoder}} options - * @returns {API.BlockView} - */ -function createUnsafe ({ bytes, cid, value: maybeValue, codec }) { - const value = maybeValue !== undefined - ? maybeValue - : (codec && codec.decode(bytes)) - - if (value === undefined) throw new Error('Missing required argument, must either provide "value" or "codec"') - - return new Block({ - // eslint-disable-next-line object-shorthand - cid: /** @type {CID} */ (cid), - bytes, - value - }) -} - -/** - * @template {unknown} T - Logical type of the data encoded in the block - * @template {number} Code - multicodec code corresponding to codec used to encode the block - * @template {number} Alg - multicodec code corresponding to the hashing algorithm used in CID creation. - * @template {API.Version} V - CID version - * @param {object} options - * @param {API.Link} options.cid - * @param {API.ByteView} options.bytes - * @param {API.BlockDecoder} options.codec - * @param {API.MultihashHasher} options.hasher - * @returns {Promise>} - */ -async function create ({ bytes, cid, hasher, codec }) { - if (!bytes) throw new Error('Missing required argument "bytes"') - if (!hasher) 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 - }) -} - -export { encode, decode, create, createUnsafe, Block } diff --git a/src/block.ts b/src/block.ts new file mode 100644 index 00000000..ae59ff13 --- /dev/null +++ b/src/block.ts @@ -0,0 +1,239 @@ +import { bytes as binary, CID } from './index.js' +import type * as API from './interface.js' + +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 (source: T, base: Array): 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 { + 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 (source: T, base: Array): Iterable { + 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 (source: T, path: string[]): API.BlockCursorView { + let node = source as Record + 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 implements API.BlockView { + readonly cid: CID + readonly bytes: API.ByteView + readonly value: T + readonly asBlock: this + + constructor ({ cid, bytes, value }: { cid: CID, bytes: API.ByteView, 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]> { + return links(this.value, []) + } + + tree (): Iterable { + return tree(this.value, []) + } + + get (path = '/'): API.BlockCursorView { + return get(this.value, path.split('/').filter(Boolean)) + } +} + +interface EncodeInput { + value: T + codec: API.BlockEncoder + hasher: API.MultihashHasher +} + +/** + * @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 ({ value, codec, hasher }: EncodeInput): Promise> { + 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) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const cid = CID.create( + 1, + codec.code, + hash + ) as CID + + return new Block({ value, bytes, cid }) +} + +interface DecodeInput { + bytes: API.ByteView + codec: API.BlockDecoder + hasher: API.MultihashHasher +} + +/** + * @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 ({ bytes, codec, hasher }: DecodeInput): Promise> { + 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) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const cid = CID.create(1, codec.code, hash) as CID + + return new Block({ value, bytes, cid }) +} + +type CreateUnsafeInput = { + cid: API.Link + value: T + codec?: API.BlockDecoder + bytes: API.ByteView +} | { + cid: API.Link + value?: undefined + codec: API.BlockDecoder + bytes: API.ByteView +} + +/** + * @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 ({ bytes, cid, value: maybeValue, codec }: CreateUnsafeInput): API.BlockView { + 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, + bytes, + value + }) +} + +interface CreateInput { + bytes: API.ByteView + cid: API.Link + hasher: API.MultihashHasher + codec: API.BlockDecoder +} + +/** + * @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 ({ bytes, cid, hasher, codec }: CreateInput): Promise> { + 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 + }) +} diff --git a/src/block/interface.js b/src/block/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/block/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/block/interface.ts b/src/block/interface.ts index 71c55d05..932327df 100644 --- a/src/block/interface.ts +++ b/src/block/interface.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ -/* eslint-disable no-use-before-define */ - import type { CID } from '../cid.js' import type { Link, Version } from '../link/interface.js' @@ -55,7 +52,7 @@ export interface Block< cid: Link } -export type BlockCursorView = +export type BlockCursorView = | { value: T, remaining?: undefined } | { value: CID, remaining: string } diff --git a/src/bytes.js b/src/bytes.js deleted file mode 100644 index a10ce3ec..00000000 --- a/src/bytes.js +++ /dev/null @@ -1,67 +0,0 @@ -const empty = new Uint8Array(0) - -/** - * @param {Uint8Array} d - */ -const toHex = d => d.reduce((hex, byte) => hex + byte.toString(16).padStart(2, '0'), '') - -/** - * @param {string} hex - */ -const fromHex = hex => { - const hexes = hex.match(/../g) - return hexes ? new Uint8Array(hexes.map(b => parseInt(b, 16))) : empty -} - -/** - * @param {Uint8Array} aa - * @param {Uint8Array} bb - */ -const equals = (aa, bb) => { - if (aa === bb) return true - if (aa.byteLength !== bb.byteLength) { - return false - } - - for (let ii = 0; ii < aa.byteLength; ii++) { - if (aa[ii] !== bb[ii]) { - return false - } - } - - return true -} - -/** - * @param {ArrayBufferView|ArrayBuffer|Uint8Array} o - * @returns {Uint8Array} - */ -const coerce = o => { - if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return o - if (o instanceof ArrayBuffer) return new Uint8Array(o) - if (ArrayBuffer.isView(o)) { - return new Uint8Array(o.buffer, o.byteOffset, o.byteLength) - } - throw new Error('Unknown type, must be binary type') -} - -/** - * @param {any} o - * @returns {o is ArrayBuffer|ArrayBufferView} - */ -const isBinary = o => - o instanceof ArrayBuffer || ArrayBuffer.isView(o) - -/** - * @param {string} str - * @returns {Uint8Array} - */ -const fromString = str => (new TextEncoder()).encode(str) - -/** - * @param {Uint8Array} b - * @returns {string} - */ -const toString = b => (new TextDecoder()).decode(b) - -export { equals, coerce, isBinary, fromHex, toHex, fromString, toString, empty } diff --git a/src/bytes.ts b/src/bytes.ts new file mode 100644 index 00000000..c323993d --- /dev/null +++ b/src/bytes.ts @@ -0,0 +1,46 @@ +export const empty = new Uint8Array(0) + +export function toHex (d: Uint8Array): string { + return d.reduce((hex, byte) => hex + byte.toString(16).padStart(2, '0'), '') +} + +export function fromHex (hex: string): Uint8Array { + const hexes = hex.match(/../g) + return hexes != null ? new Uint8Array(hexes.map(b => parseInt(b, 16))) : empty +} + +export function equals (aa: Uint8Array, bb: Uint8Array): boolean { + if (aa === bb) return true + if (aa.byteLength !== bb.byteLength) { + return false + } + + for (let ii = 0; ii < aa.byteLength; ii++) { + if (aa[ii] !== bb[ii]) { + return false + } + } + + return true +} + +export function coerce (o: ArrayBufferView | ArrayBuffer | Uint8Array): Uint8Array { + if (o instanceof Uint8Array && o.constructor.name === 'Uint8Array') return o + if (o instanceof ArrayBuffer) return new Uint8Array(o) + if (ArrayBuffer.isView(o)) { + return new Uint8Array(o.buffer, o.byteOffset, o.byteLength) + } + throw new Error('Unknown type, must be binary type') +} + +export function isBinary (o: unknown): o is ArrayBuffer | ArrayBufferView { + return o instanceof ArrayBuffer || ArrayBuffer.isView(o) +} + +export function fromString (str: string): Uint8Array { + return new TextEncoder().encode(str) +} + +export function toString (b: Uint8Array): string { + return new TextDecoder().decode(b) +} diff --git a/src/cid.js b/src/cid.ts similarity index 55% rename from src/cid.js rename to src/cid.ts index 40f7c5fc..6baa4a10 100644 --- a/src/cid.js +++ b/src/cid.ts @@ -2,63 +2,43 @@ import { base32 } from './bases/base32.js' import { base58btc } from './bases/base58.js' import { coerce } from './bytes.js' import * as Digest from './hashes/digest.js' -// Linter can see that API is used in types. -// eslint-disable-next-line -import * as API from "./link/interface.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' -/** - * @template {API.Link} T - * @template {string} Prefix - * @param {T} link - * @param {API.MultibaseEncoder} [base] - * @returns {API.ToString} - */ -export const format = (link, base) => { +export function format , Prefix extends string> (link: T, base?: API.MultibaseEncoder): API.ToString { const { bytes, version } = link switch (version) { case 0: return toStringV0( bytes, baseCache(link), - /** @type {API.MultibaseEncoder<"z">} */ (base) || base58btc.encoder + base as API.MultibaseEncoder<'z'> ?? base58btc.encoder ) default: return toStringV1( bytes, baseCache(link), - /** @type {API.MultibaseEncoder} */ (base || base32.encoder) + (base ?? base32.encoder) as API.MultibaseEncoder ) } } -/** - * @template {API.UnknownLink} Link - * @param {Link} link - * @returns {API.LinkJSON} - */ -export const toJSON = (link) => ({ - '/': format(link) -}) - -/** - * @template {API.UnknownLink} Link - * @param {API.LinkJSON} json - */ -export const fromJSON = (json) => - CID.parse(json['/']) - -/** @type {WeakMap>} */ -const cache = new WeakMap() - -/** - * @param {API.UnknownLink} cid - * @returns {Map} - */ -const baseCache = cid => { +export function toJSON (link: Link): API.LinkJSON { + return { + '/': format(link) + } +} + +export function fromJSON (json: API.LinkJSON): CID { + return CID.parse(json['/']) +} + +const cache = new WeakMap>() + +function baseCache (cid: API.UnknownLink): Map { const baseCache = cache.get(cid) if (baseCache == null) { const baseCache = new Map() @@ -68,34 +48,26 @@ const baseCache = cid => { return baseCache } -/** - * @template {unknown} [Data=unknown] - * @template {number} [Format=number] - * @template {number} [Alg=number] - * @template {API.Version} [Version=API.Version] - * @implements {API.Link} - */ +export class CID implements API.Link { + readonly code: Format + readonly version: Version + readonly multihash: API.MultihashDigest + readonly bytes: Uint8Array + readonly '/': Uint8Array -export class CID { /** - * @param {Version} version - Version of the CID - * @param {Format} code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv - * @param {API.MultihashDigest} multihash - (Multi)hash of the of the content. - * @param {Uint8Array} 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) { - /** @readonly */ + constructor (version: Version, code: Format, multihash: API.MultihashDigest, bytes: Uint8Array) { this.code = code - /** @readonly */ this.version = version - /** @readonly */ this.multihash = multihash - /** @readonly */ this.bytes = bytes // flag to serializers that this is a CID and // should be treated specially - /** @readonly */ this['/'] = bytes } @@ -105,27 +77,24 @@ export class CID { * * @deprecated */ - get asCID () { + get asCID (): this { return this } // ArrayBufferView - get byteOffset () { + get byteOffset (): number { return this.bytes.byteOffset } // ArrayBufferView - get byteLength () { + get byteLength (): number { return this.bytes.byteLength } - /** - * @returns {CID} - */ - toV0 () { + toV0 (): CID { switch (this.version) { case 0: { - return /** @type {CID} */ (this) + return this as CID } case 1: { const { code, multihash } = this @@ -139,9 +108,9 @@ export class CID { throw new Error('Cannot convert non sha2-256 multihash CID to CIDv0') } - return /** @type {CID} */ ( + return ( CID.createV0( - /** @type {API.MultihashDigest} */ (multihash) + multihash as API.MultihashDigest ) ) } @@ -153,20 +122,17 @@ export class CID { } } - /** - * @returns {CID} - */ - toV1 () { + toV1 (): CID { switch (this.version) { case 0: { const { code, digest } = this.multihash const multihash = Digest.create(code, digest) - return /** @type {CID} */ ( + return ( CID.createV1(this.code, multihash) ) } case 1: { - return /** @type {CID} */ (this) + return this as CID } default: { throw Error( @@ -176,62 +142,37 @@ export class CID { } } - /** - * @param {unknown} other - * @returns {other is CID} - */ - equals (other) { + equals (other: unknown): other is CID { return CID.equals(this, other) } - /** - * @template {unknown} Data - * @template {number} Format - * @template {number} Alg - * @template {API.Version} Version - * @param {API.Link} self - * @param {unknown} other - * @returns {other is CID} - */ - static equals (self, other) { - const unknown = - /** @type {{code?:unknown, version?:unknown, multihash?:unknown}} */ ( - other - ) + static equals (self: API.Link, other: unknown): other is CID { + const unknown = other as { code?: unknown, version?: unknown, multihash?: unknown } return ( - unknown && + unknown != null && self.code === unknown.code && self.version === unknown.version && Digest.equals(self.multihash, unknown.multihash) ) } - /** - * @param {API.MultibaseEncoder} [base] - * @returns {string} - */ - toString (base) { + toString (base?: API.MultibaseEncoder): string { return format(this, base) } - /** - * @returns {API.LinkJSON} - */ - toJSON () { + toJSON (): API.LinkJSON { return { '/': format(this) } } - link () { + link (): this { return this } - get [Symbol.toStringTag] () { - return 'CID' - } + readonly [Symbol.toStringTag] = 'CID'; // Legacy - [Symbol.for('nodejs.util.inspect.custom')] () { + [Symbol.for('nodejs.util.inspect.custom')] (): string { return `CID(${this.toString()})` } @@ -244,21 +185,13 @@ export class CID { * * This allows two different incompatible versions of CID library to * co-exist and interop as long as binary interface is compatible. - * - * @template {unknown} Data - * @template {number} Format - * @template {number} Alg - * @template {API.Version} Version - * @template {unknown} U - * @param {API.Link|U} input - * @returns {CID|null} */ - static asCID (input) { + static asCID (input: API.Link | U): CID | null { if (input == null) { return null } - const value = /** @type {any} */ (input) + const value = input as any if (value instanceof CID) { // If value is instance of CID then we're all set. return value @@ -272,17 +205,15 @@ export class CID { return new CID( version, code, - /** @type {API.MultihashDigest} */ (multihash), - bytes || encodeCID(version, code, multihash.bytes) + multihash as API.MultihashDigest, + 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 = - /** @type {API.MultihashDigest} */ - (Digest.decode(multihash)) + const digest = Digest.decode(multihash) as API.MultihashDigest return CID.create(version, code, digest) } else { // Otherwise value is not a CID (or an incompatible version of it) in @@ -292,17 +223,11 @@ export class CID { } /** - * - * @template {unknown} Data - * @template {number} Format - * @template {number} Alg - * @template {API.Version} Version - * @param {Version} version - Version of the CID - * @param {Format} code - Code of the codec content is encoded in, see https://github.com/multiformats/multicodec/blob/master/table.csv - * @param {API.MultihashDigest} digest - (Multi)hash of the of the content. - * @returns {CID} + * @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) { + static create (version: Version, code: Format, digest: API.MultihashDigest): CID { if (typeof code !== 'number') { throw new Error('String codecs are no longer supported') } @@ -333,26 +258,18 @@ export class CID { /** * Simplified version of `create` for CIDv0. - * - * @template {unknown} [T=unknown] - * @param {API.MultihashDigest} digest - Multihash. - * @returns {CID} */ - static createV0 (digest) { + static createV0 (digest: API.MultihashDigest): CID { return CID.create(0, DAG_PB_CODE, digest) } /** * Simplified version of `create` for CIDv1. * - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @param {Code} code - Content encoding format code. - * @param {API.MultihashDigest} digest - Miltihash of the content. - * @returns {CID} + * @param code - Content encoding format code. + * @param digest - Multihash of the content. */ - static createV1 (code, digest) { + static createV1 (code: Code, digest: API.MultihashDigest): CID { return CID.create(1, code, digest) } @@ -362,17 +279,10 @@ export class CID { * * An error will be thrown if the bytes provided do not contain a valid * binary representation of a CID. - * - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @template {API.Version} Ver - * @param {API.ByteView>} bytes - * @returns {CID} */ - static decode (bytes) { + static decode (bytes: API.ByteView>): CID { const [cid, remainder] = CID.decodeFirst(bytes) - if (remainder.length) { + if (remainder.length !== 0) { throw new Error('Incorrect length') } return cid @@ -386,15 +296,8 @@ export class CID { * 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. - * - * @template {unknown} T - * @template {number} C - * @template {number} A - * @template {API.Version} V - * @param {API.ByteView>} bytes - * @returns {[CID, Uint8Array]} */ - static decodeFirst (bytes) { + static decodeFirst (bytes: API.ByteView>): [CID, Uint8Array] { const specs = CID.inspectBytes(bytes) const prefixSize = specs.size - specs.multihashSize const multihashBytes = coerce( @@ -414,9 +317,9 @@ export class CID { ) const cid = specs.version === 0 - ? CID.createV0(/** @type {API.MultihashDigest} */ (digest)) + ? CID.createV0(digest as API.MultihashDigest) : CID.createV1(specs.codec, digest) - return [/** @type {CID} */(cid), bytes.subarray(specs.size)] + return [cid as CID, bytes.subarray(specs.size)] } /** @@ -427,30 +330,23 @@ export class CID { * 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. - * - * @template {unknown} T - * @template {number} C - * @template {number} A - * @template {API.Version} V - * @param {API.ByteView>} initialBytes - * @returns {{ version:V, codec:C, multihashCode:A, digestSize:number, multihashSize:number, size:number }} */ - static inspectBytes (initialBytes) { + static inspectBytes (initialBytes: API.ByteView>): { version: V, codec: C, multihashCode: A, digestSize: number, multihashSize: number, size: number } { let offset = 0 - const next = () => { + const next = (): number => { const [i, length] = varint.decode(initialBytes.subarray(offset)) offset += length return i } - let version = /** @type {V} */ (next()) - let codec = /** @type {C} */ (DAG_PB_CODE) - if (/** @type {number} */(version) === 18) { + let version = next() as V + let codec = DAG_PB_CODE as C + if (version as number === 18) { // CIDv0 - version = /** @type {V} */ (0) + version = 0 as V offset = 0 } else { - codec = /** @type {C} */ (next()) + codec = next() as C } if (version !== 0 && version !== 1) { @@ -458,7 +354,7 @@ export class CID { } const prefixSize = offset - const multihashCode = /** @type {A} */ (next()) // multihash code + const multihashCode = next() as A // multihash code const digestSize = next() // multihash length const size = offset + digestSize const multihashSize = size - prefixSize @@ -471,17 +367,8 @@ export class CID { * 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). - * - * @template {string} Prefix - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @template {API.Version} Ver - * @param {API.ToString, Prefix>} source - * @param {API.MultibaseDecoder} [base] - * @returns {CID} */ - static parse (source, base) { + static parse (source: API.ToString, Prefix>, base?: API.MultibaseDecoder): CID { const [prefix, bytes] = parseCIDtoBytes(source, base) const cid = CID.decode(bytes) @@ -497,33 +384,23 @@ export class CID { } } -/** - * @template {string} Prefix - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @template {API.Version} Ver - * @param {API.ToString, Prefix>} source - * @param {API.MultibaseDecoder} [base] - * @returns {[Prefix, API.ByteView>]} - */ -const parseCIDtoBytes = (source, base) => { +function parseCIDtoBytes (source: API.ToString, Prefix>, base?: API.MultibaseDecoder): [Prefix, API.ByteView>] { switch (source[0]) { // CIDv0 is parsed differently case 'Q': { - const decoder = base || base58btc + const decoder = base ?? base58btc return [ - /** @type {Prefix} */ (base58btc.prefix), + base58btc.prefix as Prefix, decoder.decode(`${base58btc.prefix}${source}`) ] } case base58btc.prefix: { - const decoder = base || base58btc - return [/** @type {Prefix} */(base58btc.prefix), decoder.decode(source)] + const decoder = base ?? base58btc + return [base58btc.prefix as Prefix, decoder.decode(source)] } case base32.prefix: { - const decoder = base || base32 - return [/** @type {Prefix} */(base32.prefix), decoder.decode(source)] + const decoder = base ?? base32 + return [base32.prefix as Prefix, decoder.decode(source)] } default: { if (base == null) { @@ -531,18 +408,12 @@ const parseCIDtoBytes = (source, base) => { 'To parse non base32 or base58btc encoded CID multibase decoder must be provided' ) } - return [/** @type {Prefix} */(source[0]), base.decode(source)] + return [source[0] as Prefix, base.decode(source)] } } } -/** - * - * @param {Uint8Array} bytes - * @param {Map} cache - * @param {API.MultibaseEncoder<'z'>} base - */ -const toStringV0 = (bytes, cache, base) => { +function toStringV0 (bytes: Uint8Array, cache: Map, base: API.MultibaseEncoder<'z'>): string { const { prefix } = base if (prefix !== base58btc.prefix) { throw Error(`Cannot string encode V0 in ${base.name} encoding`) @@ -558,13 +429,7 @@ const toStringV0 = (bytes, cache, base) => { } } -/** - * @template {string} Prefix - * @param {Uint8Array} bytes - * @param {Map} cache - * @param {API.MultibaseEncoder} base - */ -const toStringV1 = (bytes, cache, base) => { +function toStringV1 (bytes: Uint8Array, cache: Map, base: API.MultibaseEncoder): string { const { prefix } = base const cid = cache.get(prefix) if (cid == null) { @@ -579,13 +444,7 @@ const toStringV1 = (bytes, cache, base) => { const DAG_PB_CODE = 0x70 const SHA_256_CODE = 0x12 -/** - * @param {API.Version} version - * @param {number} code - * @param {Uint8Array} multihash - * @returns {Uint8Array} - */ -const encodeCID = (version, code, multihash) => { +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) diff --git a/src/codecs/interface.js b/src/codecs/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/codecs/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/codecs/json.js b/src/codecs/json.js deleted file mode 100644 index c3028430..00000000 --- a/src/codecs/json.js +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-check - -/** - * @template T - * @typedef {import('./interface.js').ByteView} ByteView - */ - -const textEncoder = new TextEncoder() -const textDecoder = new TextDecoder() - -export const name = 'json' -export const code = 0x0200 - -/** - * @template T - * @param {T} node - * @returns {ByteView} - */ -export const encode = (node) => textEncoder.encode(JSON.stringify(node)) - -/** - * @template T - * @param {ByteView} data - * @returns {T} - */ -export const decode = (data) => JSON.parse(textDecoder.decode(data)) diff --git a/src/codecs/json.ts b/src/codecs/json.ts new file mode 100644 index 00000000..77151e7b --- /dev/null +++ b/src/codecs/json.ts @@ -0,0 +1,15 @@ +import type { ByteView } from './interface.js' + +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +export const name = 'json' +export const code = 0x0200 + +export function encode (node: T): ByteView { + return textEncoder.encode(JSON.stringify(node)) +} + +export function decode (data: ByteView): T { + return JSON.parse(textDecoder.decode(data)) +} diff --git a/src/codecs/raw.js b/src/codecs/raw.js deleted file mode 100644 index 7a9c7815..00000000 --- a/src/codecs/raw.js +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-check - -import { coerce } from '../bytes.js' - -/** - * @template T - * @typedef {import('./interface.js').ByteView} ByteView - */ - -export const name = 'raw' -export const code = 0x55 - -/** - * @param {Uint8Array} node - * @returns {ByteView} - */ -export const encode = (node) => coerce(node) - -/** - * @param {ByteView} data - * @returns {Uint8Array} - */ -export const decode = (data) => coerce(data) diff --git a/src/codecs/raw.ts b/src/codecs/raw.ts new file mode 100644 index 00000000..2d76a422 --- /dev/null +++ b/src/codecs/raw.ts @@ -0,0 +1,13 @@ +import { coerce } from '../bytes.js' +import type { ByteView } from './interface.js' + +export const name = 'raw' +export const code = 0x55 + +export function encode (node: Uint8Array): ByteView { + return coerce(node) +} + +export function decode (data: ByteView): Uint8Array { + return coerce(data) +} diff --git a/src/hashes/digest.js b/src/hashes/digest.ts similarity index 63% rename from src/hashes/digest.js rename to src/hashes/digest.ts index 0653b685..0badafa7 100644 --- a/src/hashes/digest.js +++ b/src/hashes/digest.ts @@ -1,14 +1,11 @@ import { coerce, equals as equalBytes } from '../bytes.js' import * as varint from '../varint.js' +import type { MultihashDigest } from './interface.js' /** * Creates a multihash digest. - * - * @template {number} Code - * @param {Code} code - * @param {Uint8Array} digest */ -export const create = (code, digest) => { +export function create (code: Code, digest: Uint8Array): Digest { const size = digest.byteLength const sizeOffset = varint.encodingLength(code) const digestOffset = sizeOffset + varint.encodingLength(size) @@ -23,11 +20,8 @@ export const create = (code, digest) => { /** * Turns bytes representation of multihash digest into an instance. - * - * @param {Uint8Array} multihash - * @returns {MultihashDigest} */ -export const decode = (multihash) => { +export function decode (multihash: Uint8Array): MultihashDigest { const bytes = coerce(multihash) const [code, sizeOffset] = varint.decode(bytes) const [size, digestOffset] = varint.decode(bytes.subarray(sizeOffset)) @@ -40,16 +34,11 @@ export const decode = (multihash) => { return new Digest(code, size, digest, bytes) } -/** - * @param {MultihashDigest} a - * @param {unknown} b - * @returns {b is MultihashDigest} - */ -export const equals = (a, b) => { +export function equals (a: MultihashDigest, b: unknown): b is MultihashDigest { if (a === b) { return true } else { - const data = /** @type {{code?:unknown, size?:unknown, bytes?:unknown}} */(b) + const data = b as { code?: unknown, size?: unknown, bytes?: unknown } return ( a.code === data.code && @@ -60,29 +49,20 @@ export const equals = (a, b) => { } } -/** - * @typedef {import('./interface.js').MultihashDigest} MultihashDigest - */ - /** * Represents a multihash digest which carries information about the * hashing algorithm and an actual hash digest. - * - * @template {number} Code - * @template {number} Size - * @class - * @implements {MultihashDigest} */ -export class Digest { +export class Digest implements MultihashDigest { + readonly code: Code + readonly size: Size + readonly digest: Uint8Array + readonly bytes: Uint8Array + /** * Creates a multihash digest. - * - * @param {Code} code - * @param {Size} size - * @param {Uint8Array} digest - * @param {Uint8Array} bytes */ - constructor (code, size, digest, bytes) { + constructor (code: Code, size: Size, digest: Uint8Array, bytes: Uint8Array) { this.code = code this.size = size this.digest = digest diff --git a/src/hashes/hasher.js b/src/hashes/hasher.js deleted file mode 100644 index ac095857..00000000 --- a/src/hashes/hasher.js +++ /dev/null @@ -1,61 +0,0 @@ -import * as Digest from './digest.js' - -/** - * @template {string} Name - * @template {number} Code - * @param {object} options - * @param {Name} options.name - * @param {Code} options.code - * @param {(input: Uint8Array) => Await} options.encode - */ -export const from = ({ name, code, encode }) => new Hasher(name, code, encode) - -/** - * Hasher represents a hashing algorithm implementation that produces as - * `MultihashDigest`. - * - * @template {string} Name - * @template {number} Code - * @class - * @implements {MultihashHasher} - */ -export class Hasher { - /** - * - * @param {Name} name - * @param {Code} code - * @param {(input: Uint8Array) => Await} encode - */ - constructor (name, code, encode) { - this.name = name - this.code = code - this.encode = encode - } - - /** - * @param {Uint8Array} input - * @returns {Await>} - */ - digest (input) { - if (input instanceof Uint8Array) { - const result = this.encode(input) - return result instanceof Uint8Array - ? Digest.create(this.code, result) - /* c8 ignore next 1 */ - : result.then(digest => Digest.create(this.code, digest)) - } else { - throw Error('Unknown type, must be binary type') - /* c8 ignore next 1 */ - } - } -} - -/** - * @template {number} Alg - * @typedef {import('./interface.js').MultihashHasher} MultihashHasher - */ - -/** - * @template T - * @typedef {Promise|T} Await - */ diff --git a/src/hashes/hasher.ts b/src/hashes/hasher.ts new file mode 100644 index 00000000..5202a176 --- /dev/null +++ b/src/hashes/hasher.ts @@ -0,0 +1,37 @@ +import * as Digest from './digest.js' +import type { MultihashHasher } from './interface.js' + +type Await = Promise | T + +export function from ({ name, code, encode }: { name: Name, code: Code, encode(input: Uint8Array): Await }): Hasher { + return new Hasher(name, code, encode) +} + +/** + * Hasher represents a hashing algorithm implementation that produces as + * `MultihashDigest`. + */ +export class Hasher implements MultihashHasher { + readonly name: Name + readonly code: Code + readonly encode: (input: Uint8Array) => Await + + constructor (name: Name, code: Code, encode: (input: Uint8Array) => Await) { + this.name = name + this.code = code + this.encode = encode + } + + digest (input: Uint8Array): Await> { + if (input instanceof Uint8Array) { + const result = this.encode(input) + return result instanceof Uint8Array + ? Digest.create(this.code, result) + /* c8 ignore next 1 */ + : result.then(digest => Digest.create(this.code, digest)) + } else { + throw Error('Unknown type, must be binary type') + /* c8 ignore next 1 */ + } + } +} diff --git a/src/hashes/identity.js b/src/hashes/identity.js deleted file mode 100644 index 45e82458..00000000 --- a/src/hashes/identity.js +++ /dev/null @@ -1,16 +0,0 @@ -import { coerce } from '../bytes.js' -import * as Digest from './digest.js' - -const code = 0x0 -const name = 'identity' - -/** @type {(input:Uint8Array) => Uint8Array} */ -const encode = coerce - -/** - * @param {Uint8Array} input - * @returns {Digest.Digest} - */ -const digest = (input) => Digest.create(code, encode(input)) - -export const identity = { code, name, encode, digest } diff --git a/src/hashes/identity.ts b/src/hashes/identity.ts new file mode 100644 index 00000000..69886c75 --- /dev/null +++ b/src/hashes/identity.ts @@ -0,0 +1,13 @@ +import { coerce } from '../bytes.js' +import * as Digest from './digest.js' + +const code = 0x0 +const name = 'identity' + +const encode: (input: Uint8Array) => Uint8Array = coerce + +function digest (input: Uint8Array): Digest.Digest { + return Digest.create(code, encode(input)) +} + +export const identity = { code, name, encode, digest } diff --git a/src/hashes/interface.js b/src/hashes/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/hashes/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/hashes/interface.ts b/src/hashes/interface.ts index a8003aaa..a5b94536 100644 --- a/src/hashes/interface.ts +++ b/src/hashes/interface.ts @@ -41,8 +41,6 @@ export interface MultihashHasher { * either promise of a digest or a digest. This way general use can `await` * while performance critical code may asses return value to decide whether * await is needed. - * - * @param {Uint8Array} input */ digest(input: Uint8Array): Promise> | MultihashDigest diff --git a/src/hashes/sha1-browser.js b/src/hashes/sha1-browser.js deleted file mode 100644 index 187ba543..00000000 --- a/src/hashes/sha1-browser.js +++ /dev/null @@ -1,18 +0,0 @@ -/* global crypto */ - -import { from } from './hasher.js' - -/** - * @param {AlgorithmIdentifier} name - */ -const sha = name => - /** - * @param {Uint8Array} data - */ - async data => new Uint8Array(await crypto.subtle.digest(name, data)) - -export const sha1 = from({ - name: 'sha-1', - code: 0x11, - encode: sha('SHA-1') -}) diff --git a/src/hashes/sha1-browser.ts b/src/hashes/sha1-browser.ts new file mode 100644 index 00000000..6764ba8e --- /dev/null +++ b/src/hashes/sha1-browser.ts @@ -0,0 +1,12 @@ +/* global crypto */ + +import { from } from './hasher.js' + +const sha = (name: AlgorithmIdentifier) => + async (data: Uint8Array) => new Uint8Array(await crypto.subtle.digest(name, data)) + +export const sha1 = from({ + name: 'sha-1', + code: 0x11, + encode: sha('SHA-1') +}) diff --git a/src/hashes/sha1.js b/src/hashes/sha1.ts similarity index 94% rename from src/hashes/sha1.js rename to src/hashes/sha1.ts index d552fea5..f8480519 100644 --- a/src/hashes/sha1.js +++ b/src/hashes/sha1.ts @@ -1,5 +1,3 @@ -// @ts-check - import crypto from 'crypto' import { coerce } from '../bytes.js' import { from } from './hasher.js' diff --git a/src/hashes/sha2-browser.js b/src/hashes/sha2-browser.ts similarity index 57% rename from src/hashes/sha2-browser.js rename to src/hashes/sha2-browser.ts index 7f7de04a..cbf0b7b8 100644 --- a/src/hashes/sha2-browser.js +++ b/src/hashes/sha2-browser.ts @@ -2,14 +2,9 @@ import { from } from './hasher.js' -/** - * @param {AlgorithmIdentifier} name - */ -const sha = name => - /** - * @param {Uint8Array} data - */ - async data => new Uint8Array(await crypto.subtle.digest(name, data)) +function sha (name: AlgorithmIdentifier): (data: Uint8Array) => Promise { + return async data => new Uint8Array(await crypto.subtle.digest(name, data)) +} export const sha256 = from({ name: 'sha2-256', diff --git a/src/hashes/sha2.js b/src/hashes/sha2.ts similarity index 96% rename from src/hashes/sha2.js rename to src/hashes/sha2.ts index 4193aa66..37d6f2bd 100644 --- a/src/hashes/sha2.js +++ b/src/hashes/sha2.ts @@ -1,5 +1,3 @@ -// @ts-check - import crypto from 'crypto' import { coerce } from '../bytes.js' import { from } from './hasher.js' diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 0f676fcd..00000000 --- a/src/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as bytes from './bytes.js' -import { CID } from './cid.js' -import * as digest from './hashes/digest.js' -import * as hasher from './hashes/hasher.js' -import * as varint from './varint.js' - -// This way TS will also expose all the types from module -export * from './interface.js' - -export { CID, hasher, digest, varint, bytes } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..677ef877 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,212 @@ +/** + * @packageDocumentation + * + * This library defines common interfaces and low level building blocks for various interrelated multiformat technologies (multicodec, multihash, multibase, and CID). They can be used to implement custom base encoders / decoders / codecs, codec encoders /decoders and multihash hashers that comply to the interface that layers above assume. + * + * This library provides implementations for most basics and many others can be found in linked repositories. + * + * ```TypeScript + * import { CID } from 'multiformats/cid' + * import * as json from 'multiformats/codecs/json' + * import { sha256 } from 'multiformats/hashes/sha2' + * + * const bytes = json.encode({ hello: 'world' }) + * + * const hash = await sha256.digest(bytes) + * const cid = CID.create(1, json.code, hash) + * //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) + * ``` + * + * ## Creating Blocks + * + * ```TypeScript + * import * as Block from 'multiformats/block' + * import * as codec from '@ipld/dag-cbor' + * import { sha256 as hasher } from 'multiformats/hashes/sha2' + * + * const value = { hello: 'world' } + * + * // encode a block + * let block = await Block.encode({ value, codec, hasher }) + * + * block.value // { hello: 'world' } + * block.bytes // Uint8Array + * block.cid // CID() w/ sha2-256 hash address and dag-cbor codec + * + * // you can also decode blocks from their binary state + * block = await Block.decode({ bytes: block.bytes, codec, hasher }) + * + * // if you have the cid you can also verify the hash on decode + * block = await Block.create({ bytes: block.bytes, cid: block.cid, codec, hasher }) + * ``` + * + * ## Multibase Encoders / Decoders / Codecs + * + * CIDs can be serialized to string representation using multibase encoders that implement [`MultibaseEncoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. This library provides quite a few implementations that can be imported: + * + * ```TypeScript + * import { base64 } from "multiformats/bases/base64" + * cid.toString(base64.encoder) + * //> 'mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA' + * ``` + * + * Parsing CID string serialized CIDs requires multibase decoder that implements [`MultibaseDecoder`](https://github.com/multiformats/js-multiformats/blob/master/src/bases/interface.ts) interface. This library provides a decoder for every encoder it provides: + * + * ```TypeScript + * CID.parse('mAYAEEiCTojlxqRTl6svwqNJRVM2jCcPBxy+7mRTUfGDzy2gViA', base64.decoder) + * //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) + * ``` + * + * Dual of multibase encoder & decoder is defined as multibase codec and it exposes + * them as `encoder` and `decoder` properties. For added convenience codecs also + * implement `MultibaseEncoder` and `MultibaseDecoder` interfaces so they could be + * used as either or both: + * + * ```TypeScript + * cid.toString(base64) + * CID.parse(cid.toString(base64), base64) + * ``` + * + * **Note:** CID implementation comes bundled with `base32` and `base58btc` + * multibase codecs so that CIDs can be base serialized to (version specific) + * default base encoding and parsed without having to supply base encoders/decoders: + * + * ```TypeScript + * const v1 = CID.parse('bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea') + * v1.toString() + * //> 'bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea' + * + * const v0 = CID.parse('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n') + * v0.toString() + * //> 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' + * v0.toV1().toString() + * //> 'bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku' + * ``` + * + * ## Multicodec Encoders / Decoders / Codecs + * + * This library defines [`BlockEncoder`, `BlockDecoder` and `BlockCodec` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/interface.ts). + * Codec implementations should conform to the `BlockCodec` interface which implements both `BlockEncoder` and `BlockDecoder`. + * Here is an example implementation of JSON `BlockCodec`. + * + * ```TypeScript + * export const { name, code, encode, decode } = { + * name: 'json', + * code: 0x0200, + * encode: json => new TextEncoder().encode(JSON.stringify(json)), + * decode: bytes => JSON.parse(new TextDecoder().decode(bytes)) + * } + * ``` + * + * ## Multihash Hashers + * + * This library defines [`MultihashHasher` and `MultihashDigest` interfaces](https://github.com/multiformats/js-multiformats/blob/master/src/hashes/interface.ts) and convinient function for implementing them: + * + * ```TypeScript + * import * as hasher from 'multiformats/hashes/hasher' + * + * const sha256 = hasher.from({ + * // As per multiformats table + * // https://github.com/multiformats/multicodec/blob/master/table.csv#L9 + * name: 'sha2-256', + * code: 0x12, + * + * encode: (input) => new Uint8Array(crypto.createHash('sha256').update(input).digest()) + * }) + * + * const hash = await sha256.digest(json.encode({ hello: 'world' })) + * CID.create(1, json.code, hash) + * + * //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) + * ``` + * + * ## Traversal + * + * This library contains higher-order functions for traversing graphs of data easily. + * + * `walk()` walks through the links in each block of a DAG calling a user-supplied loader function for each one, in depth-first order with no duplicate block visits. The loader should return a `Block` object and can be used to inspect and collect block ordering for a full DAG walk. The loader should `throw` on error, and return `null` if a block should be skipped by `walk()`. + * + * ```TypeScript + * import { walk } from 'multiformats/traversal' + * import * as Block from 'multiformats/block' + * import * as codec from 'multiformats/codecs/json' + * import { sha256 as hasher } from 'multiformats/hashes/sha2' + * + * // build a DAG (a single block for this simple example) + * const value = { hello: 'world' } + * const block = await Block.encode({ value, codec, hasher }) + * const { cid } = block + * console.log(cid) + * //> CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea) + * + * // create a loader function that also collects CIDs of blocks in + * // their traversal order + * const load = (cid, blocks) => async (cid) => { + * // fetch a block using its cid + * // e.g.: const block = await fetchBlockByCID(cid) + * blocks.push(cid) + * return block + * } + * + * // collect blocks in this DAG starting from the root `cid` + * const blocks = [] + * await walk({ cid, load: load(cid, blocks) }) + * + * console.log(blocks) + * //> [CID(bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea)] + * ``` + * + * ## Legacy interface + * + * [`blockcodec-to-ipld-format`](https://github.com/ipld/js-blockcodec-to-ipld-format) converts a multiformats [`BlockCodec`](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/interface.ts#L21) into an + * [`interface-ipld-format`](https://github.com/ipld/interface-ipld-format) for use with the [`ipld`](https://github.com/ipld/ipld) package. This can help bridge IPLD codecs implemented using the structure and interfaces defined here to existing code that assumes, or requires `interface-ipld-format`. This bridge also includes the relevant TypeScript definitions. + * + * ## Implementations + * + * By default, no base encodings (other than base32 & base58btc), hash functions, + * or codec implementations are exposed by `multiformats`, you need to + * import the ones you need yourself. + * + * ### Multibase codecs + * + * | bases | import | repo | + * | ------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------- | + * | `base16` | `multiformats/bases/base16` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | + * | `base32`, `base32pad`, `base32hex`, `base32hexpad`, `base32z` | `multiformats/bases/base32` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | + * | `base64`, `base64pad`, `base64url`, `base64urlpad` | `multiformats/bases/base64` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | + * | `base58btc`, `base58flick4` | `multiformats/bases/base58` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) | + * + * Other (less useful) bases implemented in [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/bases) include: `base2`, `base8`, `base10`, `base36` and `base256emoji`. + * + * ### Multihash hashers + * + * | hashes | import | repo | + * | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | + * | `sha2-256`, `sha2-512` | `multiformats/hashes/sha2` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/hashes) | + * | `sha3-224`, `sha3-256`, `sha3-384`,`sha3-512`, `shake-128`, `shake-256`, `keccak-224`, `keccak-256`, `keccak-384`, `keccak-512` | `@multiformats/sha3` | [multiformats/js-sha3](https://github.com/multiformats/js-sha3) | + * | `identity` | `multiformats/hashes/identity` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/hashes/identity.js) | + * | `murmur3-128`, `murmur3-32` | `@multiformats/murmur3` | [multiformats/js-murmur3](https://github.com/multiformats/js-murmur3) | + * | `blake2b-*`, `blake2s-*` | `@multiformats/blake2` | [multiformats/js-blake2](https://github.com/multiformats/js-blake2) | + * + * ### IPLD codecs (multicodec) + * + * | codec | import | repo | + * | ---------- | -------------------------- | ------------------------------------------------------------------------------------------------------ | + * | `raw` | `multiformats/codecs/raw` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/codecs) | + * | `json` | `multiformats/codecs/json` | [multiformats/js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/codecs) | + * | `dag-cbor` | `@ipld/dag-cbor` | [ipld/js-dag-cbor](https://github.com/ipld/js-dag-cbor) | + * | `dag-json` | `@ipld/dag-json` | [ipld/js-dag-json](https://github.com/ipld/js-dag-json) | + * | `dag-pb` | `@ipld/dag-pb` | [ipld/js-dag-pb](https://github.com/ipld/js-dag-pb) | + * | `dag-jose` | `dag-jose` | [ceramicnetwork/js-dag-jose](https://github.com/ceramicnetwork/js-dag-jose) | + */ + +import * as bytes from './bytes.js' +import { CID } from './cid.js' +import * as digest from './hashes/digest.js' +import * as hasher from './hashes/hasher.js' +import * as varint from './varint.js' + +// This way TS will also expose all the types from module +export * from './interface.js' + +export { CID, hasher, digest, varint, bytes } diff --git a/src/interface.js b/src/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/link.js b/src/link.js deleted file mode 100644 index 43b361ef..00000000 --- a/src/link.js +++ /dev/null @@ -1,92 +0,0 @@ -import { CID, format, toJSON, fromJSON } from './cid.js' -// Linter can see that API is used in types. -// eslint-disable-next-line -import * as API from "./link/interface.js" -// This way TS will also expose all the types from module -export * from './link/interface.js' - -const DAG_PB_CODE = 0x70 -// eslint-disable-next-line -const SHA_256_CODE = 0x12 - -/** - * Simplified version of `create` for CIDv0. - * - * @param {API.MultihashDigest} digest - Multihash. - * @returns {API.LegacyLink} - */ -export const createLegacy = digest => CID.create(0, DAG_PB_CODE, digest) - -/** - * Simplified version of `create` for CIDv1. - * - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @param {Code} code - Content encoding format code. - * @param {API.MultihashDigest} digest - Miltihash of the content. - * @returns {API.Link} - */ -export const create = (code, digest) => CID.create(1, code, digest) - -/** - * Type predicate returns true if value is the link. - * - * @template {API.Link} L - * @param {unknown|L} value - * @returns {value is L & CID} - */ -export const isLink = value => { - if (value == null) { - return false - } - - const withSlash = /** @type {{'/'?: Uint8Array, bytes: Uint8Array}} */ (value) - - if (withSlash['/'] != null && withSlash['/'] === withSlash.bytes) { - return true - } - - const withAsCID = /** @type {{'asCID'?: unknown}} */ (value) - - if (withAsCID.asCID === value) { - return true - } - - return false -} - -/** - * 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). - * - * @template {string} Prefix - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @template {API.Version} Ver - * @param {API.ToString, Prefix>} source - * @param {API.MultibaseDecoder} [base] - * @returns {API.Link} - */ -export const parse = (source, base) => CID.parse(source, base) - -export { format, toJSON, fromJSON } - -/** - * 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. - * - * @template {unknown} Data - * @template {number} Code - * @template {number} Alg - * @template {API.Version} Ver - * @param {API.ByteView>} bytes - * @returns {API.Link} - */ -export const decode = bytes => CID.decode(bytes) diff --git a/src/link.ts b/src/link.ts new file mode 100644 index 00000000..8d3fb9a6 --- /dev/null +++ b/src/link.ts @@ -0,0 +1,71 @@ +import { CID, format, toJSON, fromJSON } from './cid.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' + +const DAG_PB_CODE = 0x70 +// eslint-disable-next-line +const SHA_256_CODE = 0x12 + +/** + * Simplified version of `create` for CIDv0. + */ +export function createLegacy (digest: API.MultihashDigest): API.LegacyLink { + return CID.create(0, DAG_PB_CODE, digest) +} + +/** + * Simplified version of `create` for CIDv1. + * + * @param code - Content encoding format code. + * @param digest - Miltihash of the content. + */ +export function create (code: Code, digest: API.MultihashDigest): API.Link { + return CID.create(1, code, digest) +} + +/** + * Type predicate returns true if value is the link. + */ +export function isLink > (value: unknown | L): value is L & CID { + if (value == null) { + return false + } + + const withSlash = value as { '/'?: Uint8Array, bytes: Uint8Array } + + if (withSlash['/'] != null && withSlash['/'] === withSlash.bytes) { + return true + } + + const withAsCID = value as { 'asCID'?: unknown } + + if (withAsCID.asCID === value) { + return true + } + + return false +} + +/** + * 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). + */ +export function parse (source: API.ToString, Prefix>, base?: API.MultibaseDecoder): API.Link { + return CID.parse(source, base) +} + +export { format, toJSON, fromJSON } + +/** + * 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. + */ +export function decode (bytes: API.ByteView>): API.Link { + return CID.decode(bytes) +} diff --git a/src/link/interface.js b/src/link/interface.js deleted file mode 100644 index d9b3f4f7..00000000 --- a/src/link/interface.js +++ /dev/null @@ -1 +0,0 @@ -// this is dummy module overlayed by interface.ts diff --git a/src/traversal.js b/src/traversal.js deleted file mode 100644 index 6c61a506..00000000 --- a/src/traversal.js +++ /dev/null @@ -1,36 +0,0 @@ -import { base58btc } from './bases/base58.js' - -/** - * @typedef {import('./cid').CID} CID - */ - -/** - * @typedef {import('./block/interface.js').BlockView} BlockView - */ - -/** - * @param {object} options - * @param {CID} options.cid - * @param {(cid: CID) => Promise} options.load - * @param {Set} [options.seen] - */ -const walk = async ({ cid, load, seen }) => { - seen = seen || new Set() - const b58Cid = cid.toString(base58btc) - if (seen.has(b58Cid)) { - return - } - - const block = await load(cid) - seen.add(b58Cid) - - if (block === null) { // the loader signals with `null` that we should skip this block - return - } - - for (const [, cid] of block.links()) { - await walk({ cid, load, seen }) - } -} - -export { walk } diff --git a/src/traversal.ts b/src/traversal.ts new file mode 100644 index 00000000..abb07663 --- /dev/null +++ b/src/traversal.ts @@ -0,0 +1,24 @@ +import { base58btc } from './bases/base58.js' +import type { BlockView as _BlockView } from './block/interface.js' +import type { CID, Version } from './cid.js' + +type BlockView = _BlockView + +export async function walk ({ cid, load, seen }: { cid: CID, load(cid: CID): Promise, seen?: Set }): Promise { + seen = seen ?? new Set() + const b58Cid = cid.toString(base58btc) + if (seen.has(b58Cid)) { + return + } + + const block = await load(cid) + seen.add(b58Cid) + + if (block === null) { // the loader signals with `null` that we should skip this block + return + } + + for (const [, cid] of block.links()) { + await walk({ cid, load, seen }) + } +} diff --git a/src/varint.js b/src/varint.js deleted file mode 100644 index d0c71eac..00000000 --- a/src/varint.js +++ /dev/null @@ -1,29 +0,0 @@ -import varint from '../vendor/varint.js' - -/** - * @param {Uint8Array} data - * @param {number} [offset=0] - * @returns {[number, number]} - */ -export const decode = (data, offset = 0) => { - const code = varint.decode(data, offset) - return [code, varint.decode.bytes] -} - -/** - * @param {number} int - * @param {Uint8Array} target - * @param {number} [offset=0] - */ -export const encodeTo = (int, target, offset = 0) => { - varint.encode(int, target, offset) - return target -} - -/** - * @param {number} int - * @returns {number} - */ -export const encodingLength = (int) => { - return varint.encodingLength(int) -} diff --git a/src/varint.ts b/src/varint.ts new file mode 100644 index 00000000..49f4e91e --- /dev/null +++ b/src/varint.ts @@ -0,0 +1,15 @@ +import varint from './vendor/varint.js' + +export function decode (data: Uint8Array, offset = 0): [number, number] { + const code = varint.decode(data, offset) + return [code, varint.decode.bytes] +} + +export function encodeTo (int: number, target: Uint8Array, offset = 0): Uint8Array { + varint.encode(int, target, offset) + return target +} + +export function encodingLength (int: number): number { + return varint.encodingLength(int) +} diff --git a/vendor/base-x.d.ts b/src/vendor/base-x.d.ts similarity index 92% rename from vendor/base-x.d.ts rename to src/vendor/base-x.d.ts index a8665dc9..94ada8af 100644 --- a/vendor/base-x.d.ts +++ b/src/vendor/base-x.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ export interface BaseConverter { encode(buffer: Uint8Array | number[]): string; diff --git a/vendor/base-x.js b/src/vendor/base-x.js similarity index 94% rename from vendor/base-x.js rename to src/vendor/base-x.js index d9627414..dd396ab2 100644 --- a/vendor/base-x.js +++ b/src/vendor/base-x.js @@ -1,8 +1,13 @@ +/* eslint-disable */ // base-x encoding / decoding // Copyright (c) 2018 base-x contributors // Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp) // Distributed under the MIT software license, see the accompanying // file LICENSE or http://www.opensource.org/licenses/mit-license.php. +/** + * @param {string} ALPHABET + * @param {any} name + */ function base (ALPHABET, name) { if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') } var BASE_MAP = new Uint8Array(256); @@ -19,7 +24,11 @@ function base (ALPHABET, name) { var LEADER = ALPHABET.charAt(0); var FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up var iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up + /** + * @param {any[] | Iterable} source + */ function encode (source) { + // @ts-ignore if (source instanceof Uint8Array) ; else if (ArrayBuffer.isView(source)) { source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength); } else if (Array.isArray(source)) { @@ -63,6 +72,9 @@ function base (ALPHABET, name) { for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); } return str } + /** + * @param {string | string[]} source + */ function decodeUnsafe (source) { if (typeof source !== 'string') { throw new TypeError('Expected String') } if (source.length === 0) { return new Uint8Array() } @@ -109,6 +121,9 @@ function base (ALPHABET, name) { } return vch } + /** + * @param {string | string[]} string + */ function decode (string) { var buffer = decodeUnsafe(string); if (buffer) { return buffer } diff --git a/vendor/varint.d.ts b/src/vendor/varint.d.ts similarity index 99% rename from vendor/varint.d.ts rename to src/vendor/varint.d.ts index f5a8a021..5d2ce74f 100644 --- a/vendor/varint.d.ts +++ b/src/vendor/varint.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ // Type definitions for varint 5.0 // Project: https://github.com/chrisdickinson/varint#readme // Definitions by: David Brockman Smoliansky diff --git a/vendor/varint.js b/src/vendor/varint.js similarity index 85% rename from vendor/varint.js rename to src/vendor/varint.js index fdc9f1f6..0c5e7b22 100644 --- a/vendor/varint.js +++ b/src/vendor/varint.js @@ -1,3 +1,4 @@ +/* eslint-disable */ var encode_1 = encode; var MSB = 0x80 @@ -5,6 +6,11 @@ var MSB = 0x80 , MSBALL = ~REST , INT = Math.pow(2, 31); +/** + * @param {number} num + * @param {number[]} out + * @param {number} offset + */ function encode(num, out, offset) { out = out || []; offset = offset || 0; @@ -20,6 +26,7 @@ function encode(num, out, offset) { } out[offset] = num | 0; + // @ts-ignore encode.bytes = offset - oldOffset + 1; return out @@ -30,6 +37,10 @@ var decode = read; var MSB$1 = 0x80 , REST$1 = 0x7F; +/** + * @param {string | any[]} buf + * @param {number} offset + */ function read(buf, offset) { var res = 0 , offset = offset || 0 @@ -40,6 +51,7 @@ function read(buf, offset) { do { if (counter >= l) { + // @ts-ignore read.bytes = 0; throw new RangeError('Could not decode varint') } @@ -50,6 +62,7 @@ function read(buf, offset) { shift += 7; } while (b >= MSB$1) + // @ts-ignore read.bytes = counter - offset; return res @@ -65,7 +78,7 @@ var N7 = Math.pow(2, 49); var N8 = Math.pow(2, 56); var N9 = Math.pow(2, 63); -var length = function (value) { +var length = function (/** @type {number} */ value) { return ( value < N1 ? 1 : value < N2 ? 2 diff --git a/test/fixtures/invalid-multihash.js b/test/fixtures/invalid-multihash.ts similarity index 100% rename from test/fixtures/invalid-multihash.js rename to test/fixtures/invalid-multihash.ts diff --git a/test/fixtures/valid-multihash.js b/test/fixtures/valid-multihash.ts similarity index 100% rename from test/fixtures/valid-multihash.js rename to test/fixtures/valid-multihash.ts diff --git a/test/test-block.spec.js b/test/test-block.spec.ts similarity index 94% rename from test/test-block.spec.js rename to test/test-block.spec.ts index 65a79225..120be93c 100644 --- a/test/test-block.spec.js +++ b/test/test-block.spec.ts @@ -118,28 +118,28 @@ describe('block', () => { }) it('encode', async () => { - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.encode({}), 'Missing required argument "value"') - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.encode({ value: true }), 'Missing required argument: codec or hasher') }) it('decode', async () => { - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.decode({}), 'Missing required argument "bytes"') - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.decode({ bytes: true }), 'Missing required argument: codec or hasher') }) it('createUnsafe', async () => { - // @ts-expect-error + // @ts-expect-error testing invalid usage assert.throws(() => main.createUnsafe({}), 'Missing required argument, must either provide "value" or "codec"') }) it('create', async () => { - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.create({}), 'Missing required argument "bytes"') - // @ts-expect-error + // @ts-expect-error testing invalid usage await assert.isRejected(main.create({ bytes: true }), 'Missing required argument "hasher"') const block = await main.encode({ value: fixture, codec, hasher }) const block2 = await main.encode({ value: { ...fixture, test: 'blah' }, codec, hasher }) diff --git a/test/test-bytes.spec.js b/test/test-bytes.spec.ts similarity index 100% rename from test/test-bytes.spec.js rename to test/test-bytes.spec.ts diff --git a/test/test-cid.spec.js b/test/test-cid.spec.ts similarity index 93% rename from test/test-cid.spec.js rename to test/test-cid.spec.ts index e7721a33..5b409a59 100644 --- a/test/test-cid.spec.js +++ b/test/test-cid.spec.ts @@ -2,15 +2,12 @@ import { assert } from 'aegir/chai' import OLDCID from 'cids' -// Linter can see that API is used in types. -// eslint-disable-next-line -import * as API from 'multiformats' import { base32 } from '../src/bases/base32.js' import { base58btc } from '../src/bases/base58.js' import { base64 } from '../src/bases/base64.js' import { fromHex, toHex, equals } from '../src/bytes.js' import { sha256, sha512 } from '../src/hashes/sha2.js' -import { varint, CID } from '../src/index.js' +import { varint, CID, type MultihashDigest } from '../src/index.js' import invalidMultihash from './fixtures/invalid-multihash.js' const textEncoder = new TextEncoder() @@ -107,7 +104,7 @@ describe('CID', () => { it('should construct from an old CID', () => { const cidStr = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' const oldCid = CID.parse(cidStr) - const newCid = /** @type {CID} */ (CID.asCID(oldCid)) + const newCid = (CID.asCID(oldCid) as CID) assert.deepStrictEqual(newCid.toString(), cidStr) }) @@ -228,7 +225,7 @@ describe('CID', () => { const cidStr = 'bafybeidskjjd4zmr7oh6ku6wp72vvbxyibcli2r6if3ocdcy7jjjusvl2u' const oldCid = CID.parse(cidStr) - const newCid = /** @type {CID} */ (CID.asCID(oldCid)) + const newCid = (CID.asCID(oldCid) as CID) assert.deepStrictEqual(newCid.toString(), cidStr) }) @@ -295,7 +292,7 @@ describe('CID', () => { for (const i of parse) { const name = `CID.parse(${JSON.stringify(i)})` - it(name, async () => assert.throws(() => CID.parse(i))) + it(name, async () => { assert.throws(() => CID.parse(i)) }) } const decode = [ @@ -307,7 +304,7 @@ describe('CID', () => { const name = `CID.decode(textEncoder.encode(${JSON.stringify( i.toString() )}))` - it(name, async () => assert.throws(() => CID.decode(i))) + it(name, async () => { assert.throws(() => CID.decode(i)) }) } const create = [ @@ -322,7 +319,7 @@ describe('CID', () => { hash instanceof Uint8Array ? `textEncoder.encode(${form})` : form const name = `CID.create(${version}, ${code}, ${mh})` // @ts-expect-error - version issn't always 0|1 - it(name, async () => assert.throws(() => CID.create(version, code, hash))) + it(name, async () => { assert.throws(() => CID.create(version, code, hash)) }) } it('invalid fixtures', async () => { @@ -428,10 +425,7 @@ describe('CID', () => { const b32 = { ...base32, callCount: 0, - /** - * @param {Uint8Array} bytes - */ - encode (bytes) { + encode (bytes: Uint8Array): string { this.callCount += 1 return base32.encode(bytes) + '!' } @@ -440,10 +434,7 @@ describe('CID', () => { const b64 = { ...base64, callCount: 0, - /** - * @param {Uint8Array} bytes - */ - encode (bytes) { + encode (bytes: Uint8Array): string { this.callCount += 1 return base64.encode(bytes) } @@ -497,19 +488,20 @@ describe('CID', () => { it('asCID', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) class IncompatibleCID { - /** - * @param {number} version - * @param {number} code - * @param {import('multiformats/hashes/interface').MultihashDigest} multihash - */ - constructor (version, code, multihash) { + readonly version: number + readonly code: number + readonly multihash: MultihashDigest + readonly asCID: this + + constructor (version: number, code: number, multihash: MultihashDigest) { this.version = version this.code = code this.multihash = multihash this.asCID = this } - get [Symbol.for('@ipld/js-cid/CID')] () { + // eslint-disable-next-line @typescript-eslint/class-literal-property-style + get [Symbol.for('@ipld/js-cid/CID')] (): boolean { return true } } @@ -518,11 +510,12 @@ describe('CID', () => { const code = 112 const incompatibleCID = new IncompatibleCID(version, code, hash) + // eslint-disable-next-line @typescript-eslint/no-base-to-string assert.strictEqual(incompatibleCID.toString(), '[object Object]') // @ts-expect-error - no such method assert.strictEqual(typeof incompatibleCID.toV0, 'undefined') - const cid1 = /** @type {CID} */ (CID.asCID(incompatibleCID)) + const cid1 = (CID.asCID(incompatibleCID) as CID) assert.ok(cid1 instanceof CID) assert.strictEqual(cid1.code, code) assert.strictEqual(cid1.version, version) @@ -534,7 +527,7 @@ describe('CID', () => { const duckCID = { version, code, multihash: hash } // @ts-expect-error - no such property duckCID.asCID = duckCID - const cid3 = /** @type {CID} */ (CID.asCID(duckCID)) + const cid3 = (CID.asCID(duckCID) as CID) assert.ok(cid3 instanceof CID) assert.strictEqual(cid3.code, code) assert.strictEqual(cid3.version, version) @@ -543,8 +536,8 @@ describe('CID', () => { const cid4 = CID.asCID(cid3) assert.strictEqual(cid3, cid4) - const cid5 = /** @type {CID} */ ( - CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) + const cid5 = ( + CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) as CID ) assert.ok(cid5 instanceof CID) assert.strictEqual(cid5.version, 1) @@ -552,32 +545,24 @@ describe('CID', () => { assert.strictEqual(cid5.code, 85) }) - /** - * @param {API.CID} x - * @param {API.CID} y - */ - const digestsame = (x, y) => { - // @ts-ignore - not sure what this supposed to be + const digestsame = (x: CID, y: CID): void => { + // @ts-expect-error not sure what this supposed to be assert.deepStrictEqual(x.hash, y.hash) assert.deepStrictEqual(x.bytes, y.bytes) - if (x.multihash) { + if (x.multihash != null) { equalDigest(x.multihash, y.multihash) } const empty = { hash: null, bytes: null, digest: null, multihash: null } assert.deepStrictEqual({ ...x, ...empty }, { ...y, ...empty }) } - /** - * @typedef {import('multiformats/hashes/interface').MultihashDigest} MultihashDigest - * @param {MultihashDigest} x - * @param {MultihashDigest} y - */ - const equalDigest = (x, y) => { + const equalDigest = (x: MultihashDigest, y: MultihashDigest): void => { assert.deepStrictEqual(x.digest, y.digest) assert.deepStrictEqual(x.code, y.code) assert.deepStrictEqual(x.digest, y.digest) } + // eslint-disable-next-line @typescript-eslint/no-misused-promises describe('CID.parse', async () => { it('parse 32 encoded CIDv1', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) @@ -667,8 +652,8 @@ describe('CID', () => { it('new CID from old CID', async () => { const hash = await sha256.digest(textEncoder.encode('abc')) - const cid = /** @type {CID} */ ( - CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) + const cid = ( + CID.asCID(new OLDCID(1, 'raw', Uint8Array.from(hash.bytes))) as CID ) assert.deepStrictEqual(cid.version, 1) @@ -700,7 +685,7 @@ describe('CID', () => { const cid = CID.parse('bafybeif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu') const { port1: sender, port2: receiver } = new MessageChannel() sender.postMessage(cid) - const cid2 = await new Promise((resolve) => { + const cid2 = await new Promise((resolve) => { receiver.onmessage = (event) => { resolve(event.data) } }) sender.close() diff --git a/test/test-link.spec.js b/test/test-link.spec.ts similarity index 73% rename from test/test-link.spec.js rename to test/test-link.spec.ts index 980690f1..94fd4d2f 100644 --- a/test/test-link.spec.js +++ b/test/test-link.spec.ts @@ -9,11 +9,10 @@ const utf8 = new TextEncoder() const h1 = 'QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n' const h4 = 'bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae' const CBOR = 0x71 -// eslint-disable-next-line const SHA256 = sha256.code -const sh1 = /** @type {Link.MultihashDigest} */ ( - Link.parse(h4).multihash +const sh1 = ( + Link.parse(h4).multihash as Link.MultihashDigest ) describe('Link', () => { @@ -26,15 +25,12 @@ describe('Link', () => { it('create v1', async () => { const hash = await sha256.digest(utf8.encode('abc')) const link = Link.create(0x71, hash) - /** @type {0x71} */ - const code = link.code + const code = link.code as 0x71 assert.deepStrictEqual(code, 0x71) - /** @type {1} */ - const version = link.version + const version = link.version as 1 assert.deepEqual(version, 1) - /** @type {Link.MultihashDigest}> */ const multihash = link.multihash assert.deepStrictEqual(multihash, hash) }) @@ -43,15 +39,12 @@ describe('Link', () => { const hash = await sha256.digest(utf8.encode('abc')) const link = Link.createLegacy(hash) - /** @type {0x70} */ - const code = link.code + const code = link.code as 0x70 assert.deepStrictEqual(code, 0x70) - /** @type {0} */ - const version = link.version + const version = link.version as 0 assert.deepEqual(version, 0) - /** @type {Link.MultihashDigest}> */ const multihash = link.multihash assert.deepStrictEqual(multihash, hash) }) @@ -61,13 +54,11 @@ describe('Link', () => { it('can parse any string', () => { const link = Link.parse(h1) - /** @type {Link.Link} */ - // @ts-expect-error - types can not be inferred - const t1 = link + const t1 = link as Link.Link assert.ok(t1) // it is possible to manually cast - const t2 = /** @type {Link.LegacyLink} */ (link) + const t2 = (link as Link.LegacyLink) assert.ok(t2) }) @@ -78,14 +69,13 @@ describe('Link', () => { assert.equal(original.equals(link), true, 'format -> parse roundtrips') // ensure that type info is retained - /** @type {Link.Link} */ const t1 = link assert.ok(t1) // ensurate that you can't cast incorrectly const t2 = // @ts-expect-error - version is 1 not 0 - /** @type {Link.Link} */ (link) + (link as Link.Link) assert.ok(t2) }) }) @@ -107,7 +97,7 @@ describe('Link', () => { assert.deepStrictEqual(Link.parse(h1), Link.fromJSON({ '/': h1, - // @ts-expect-error + // @ts-expect-error foo doesn't exist foo: 1 })) @@ -134,15 +124,12 @@ describe('decode', () => { const link = Link.decode(bytes) - /** @type {0x71} */ - const code = link.code + const code = link.code as 0x71 assert.deepStrictEqual(code, 0x71) - /** @type {1} */ - const version = link.version + const version = link.version as 1 assert.deepEqual(version, 1) - /** @type {Link.MultihashDigest}> */ const multihash = link.multihash assert.deepStrictEqual(multihash, hash) }) diff --git a/test/test-multibase-spec.spec.js b/test/test-multibase-spec.spec.ts similarity index 99% rename from test/test-multibase-spec.spec.js rename to test/test-multibase-spec.spec.ts index 4bfc3834..1f7f2523 100644 --- a/test/test-multibase-spec.spec.js +++ b/test/test-multibase-spec.spec.ts @@ -157,7 +157,7 @@ describe('spec test', () => { for (const { input, tests } of encoded) { describe(`multibase spec ${index++}`, () => { for (const [name, output] of tests) { - const base = bases[/** @type {keyof bases} */(name)] + const base = bases[name as keyof typeof bases] describe(name, () => { it('should encode buffer', () => { diff --git a/test/test-multibase.spec.js b/test/test-multibase.spec.ts similarity index 94% rename from test/test-multibase.spec.js rename to test/test-multibase.spec.ts index f3f1881e..3063816f 100644 --- a/test/test-multibase.spec.js +++ b/test/test-multibase.spec.ts @@ -64,12 +64,9 @@ describe('multibase', () => { const buff = bytes.fromString('test') const nonPrintableBuff = Uint8Array.from([239, 250, 254]) - /** - * @param {typeof b2|b8|b10|b16|b32|b36|b58|b64} bases - */ - const baseTest = bases => { + const baseTest = (bases: typeof b2 | typeof b8 | typeof b10 | typeof b16 | typeof b32 | typeof b36 | typeof b58 | typeof b64): void => { for (const base of Object.values(bases)) { - if (base && base.name) { + if (((base as { name: string })?.name) !== '') { it(`encode/decode ${base.name}`, () => { const encoded = base.encode(buff) const decoded = base.decode(encoded) @@ -162,15 +159,12 @@ describe('multibase', () => { }) it('infers prefix and name corretly', () => { - /** @type {'base32'} */ - const name = base32.name + const name = base32.name as 'base32' - /** @type {'base16'} */ // @ts-expect-error - TS catches mismatch - const name2 = base32.name + const name2: 'base16' = base32.name - /** @type {'b'} */ - const prefix = base32.prefix + const prefix = base32.prefix as 'b' assert.equal(prefix, 'b') assert.equal(name, 'base32') assert.equal(name2, name) diff --git a/test/test-multicodec.spec.js b/test/test-multicodec.spec.ts similarity index 100% rename from test/test-multicodec.spec.js rename to test/test-multicodec.spec.ts diff --git a/test/test-multihash.spec.js b/test/test-multihash.spec.ts similarity index 90% rename from test/test-multihash.spec.js rename to test/test-multihash.spec.ts index a99ff5a9..5e884322 100644 --- a/test/test-multihash.spec.js +++ b/test/test-multihash.spec.ts @@ -12,16 +12,8 @@ import { sha256, sha512 } from '../src/hashes/sha2.js' import invalid from './fixtures/invalid-multihash.js' import valid from './fixtures/valid-multihash.js' -/** - * @param {number|string} code - * @param {number} size - * @param {string} hex - */ -const sample = (code, size, hex) => { - /** - * @param {number|string} i - */ - const toHex = (i) => { +const sample = (code: number | string, size: number, hex: string): Uint8Array => { + const toHex = (i: number | string): string => { if (typeof i === 'string') return i const h = i.toString(16) return h.length % 2 === 1 ? `0${h}` : h @@ -37,8 +29,8 @@ describe('multihash', () => { for (const test of valid) { const { encoding, hex, size } = test const { code, varint } = encoding - const buf = sample(varint || code, size, hex) - assert.deepStrictEqual(createDigest(code, hex ? fromHex(hex) : empty).bytes, buf) + const buf = sample(varint ?? code, size, hex) + assert.deepStrictEqual(createDigest(code, (hex !== '') ? fromHex(hex) : empty).bytes, buf) } }) @@ -105,6 +97,7 @@ describe('multihash', () => { }) it('hash identity async', async () => { + // eslint-disable-next-line @typescript-eslint/await-thenable const hash = await identity.digest(fromString('test')) assert.deepStrictEqual(hash.code, identity.code) assert.deepStrictEqual(identity.code, 0) @@ -130,8 +123,8 @@ describe('multihash', () => { for (const { encoding, hex, size } of valid) { it(`valid fixture ${hex}`, () => { const { code, varint } = encoding - const bytes = sample(varint || code, size, hex) - const digest = hex ? fromHex(hex) : empty + const bytes = sample(varint ?? code, size, hex) + const digest = (hex !== '') ? fromHex(hex) : empty const hash = decodeDigest(bytes) assert.deepStrictEqual(hash.bytes, bytes) @@ -147,6 +140,7 @@ describe('multihash', () => { assert.deepStrictEqual(hash.code, 18) }) }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises describe('validate', async () => { it('invalid fixtures', async () => { for (const test of invalid) { diff --git a/test/test-traversal.spec.js b/test/test-traversal.spec.ts similarity index 80% rename from test/test-traversal.spec.js rename to test/test-traversal.spec.ts index 386f5115..e0ae2c59 100644 --- a/test/test-traversal.spec.js +++ b/test/test-traversal.spec.ts @@ -6,30 +6,20 @@ import { fromString } from '../src/bytes.js' import * as codec from '../src/codecs/json.js' import { sha256 as hasher } from '../src/hashes/sha2.js' import { walk } from '../src/traversal.js' - -/** @typedef {import('../src/cid.js').CID} CID */ +import type { BlockView } from '../src/block/interface.js' +import type { CID } from '../src/cid.js' // from dag-pb, simplified -/** - * @param {Uint8Array} data - * @param {{Hash:CID, Name:string, Tsize:number}[]} links - * @returns {{Data:Uint8Array, Links:{Hash:CID, Name:string, Tsize:number}[]}} - */ -function createNode (data, links) { +function createNode (data: Uint8Array, links: Array<{ Hash: CID, Name: string, Tsize: number }>): { Data: Uint8Array, Links: Array<{ Hash: CID, Name: string, Tsize: number }> } { return { Data: data, Links: links } } -/** - * @param {string} name - * @param {number} size - * @param {CID} cid - * @returns {{Hash:CID, Name:string, Tsize:number}} - */ -function createLink (name, size, cid) { +function createLink (name: string, size: number, cid: CID): { Hash: CID, Name: string, Tsize: number } { return { Hash: cid, Name: name, Tsize: size } } describe('traversal', () => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises describe('walk', async () => { // Forming the following DAG for testing // A @@ -37,12 +27,12 @@ describe('traversal', () => { // B C // / \ / \ // D D D E - const linksE = /** @type {[]} */([]) + const linksE = ([] as []) const valueE = createNode(fromString('string E qacdswa'), linksE) const blockE = await main.encode({ value: valueE, codec, hasher }) const cidE = blockE.cid - const linksD = /** @type {[]} */([]) + const linksD = ([] as []) const valueD = createNode(fromString('string D zasa'), linksD) const blockD = await main.encode({ value: valueD, codec, hasher }) const cidD = blockD.cid @@ -62,10 +52,7 @@ describe('traversal', () => { const blockA = await main.encode({ value: valueA, codec, hasher }) const cidA = blockA.cid - /** - * @param {CID} cid - */ - const load = async (cid) => { + const load = async (cid: CID): Promise | null> => { if (cid.equals(cidE)) { return blockE } @@ -84,15 +71,10 @@ describe('traversal', () => { return null } - /** - * @param {typeof load} load - * @param {string[]} arr - */ - const loadWrapper = (load, arr = []) => - /** - * @param {CID} cid - */ - (cid) => { + type loadFn = typeof load + + const loadWrapper = (load: loadFn, arr: string[] = []) => + async (cid: CID): Promise | null> => { arr.push(cid.toString()) return load(cid) } @@ -104,8 +86,7 @@ describe('traversal', () => { // // Expect load to be called with D const expectedCallArray = [cidD.toString()] - /** @type {string[]} */ - const callArray = [] + const callArray: string[] = [] await walk({ cid: cidD, load: loadWrapper(load, callArray) }) @@ -123,8 +104,7 @@ describe('traversal', () => { // // Expect load to be called with C, then D, then E const expectedCallArray = [cidC.toString(), cidD.toString(), cidE.toString()] - /** @type {string[]} */ - const callArray = [] + const callArray: string[] = [] await walk({ cid: cidC, load: loadWrapper(load, callArray) }) @@ -142,8 +122,7 @@ describe('traversal', () => { // // Expect load to be called with B, then D const expectedCallArray = [cidB.toString(), cidD.toString()] - /** @type {string[]} */ - const callArray = [] + const callArray: string[] = [] await walk({ cid: cidB, load: loadWrapper(load, callArray) }) @@ -169,8 +148,7 @@ describe('traversal', () => { cidC.toString(), cidE.toString() ] - /** @type {string[]} */ - const callArray = [] + const callArray: string[] = [] await walk({ cid: cidA, load: loadWrapper(load, callArray) }) @@ -180,14 +158,12 @@ describe('traversal', () => { }) it('null return', async () => { - /** @type {[]} */ - const links = [] + const links = [] as [] const value = createNode(fromString('test'), links) const block = await main.encode({ value, codec, hasher }) const cid = block.cid const expectedCallArray = [cid.toString()] - /** @type {string[]} */ - const callArray = [] + const callArray: string[] = [] await walk({ cid, load: loadWrapper(load, callArray) }) diff --git a/test/test-varint.spec.js b/test/test-varint.spec.ts similarity index 100% rename from test/test-varint.spec.js rename to test/test-varint.spec.ts diff --git a/test/ts-use/package.json b/test/ts-use/package.json deleted file mode 100644 index 64a68bf1..00000000 --- a/test/ts-use/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ts-use", - "private": true, - "type": "module", - "dependencies": { - "multiformats": "file:../../" - }, - "scripts": { - "test": "npm install && ../../node_modules/.bin/tsc --noEmit" - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module", - "project": [ - "./test/ts-use/tsconfig.json" - ] - } - } -} \ No newline at end of file diff --git a/test/ts-use/src/main.ts b/test/ts-use/src/main.ts deleted file mode 100644 index dc678c84..00000000 --- a/test/ts-use/src/main.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Block from 'multiformats/block' -import * as json from 'multiformats/codecs/json' -import { sha256 } from 'multiformats/hashes/sha2' - -const main = async (): Promise => { - const block = await Block.encode({ - value: { hello: 'world' }, - hasher: sha256, - codec: json - }) - - /* eslint-disable no-console */ - console.log(block) -} - -export default main diff --git a/test/ts-use/tsconfig.json b/test/ts-use/tsconfig.json deleted file mode 100644 index 2588ec61..00000000 --- a/test/ts-use/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "module": "NodeNext", - "noImplicitAny": true, - "incremental": true - } -} diff --git a/tsconfig.json b/tsconfig.json index 23bdbd79..13a35996 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,7 @@ { "extends": "aegir/src/config/tsconfig.aegir.json", "compilerOptions": { - "outDir": "dist/types", - "emitDeclarationOnly": true, - "paths": { - "multiformats": [ - "./src/index.js" - ], - "multiformats/interface": [ - "./src/interface" - ], - "multiformats/hashes/interface": [ - "./src/hashes/interface" - ], - "multiformats/*": [ - "./src/*.js" - ] - } + "outDir": "dist" }, "include": [ "src", diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 00000000..87e4675c --- /dev/null +++ b/typedoc.json @@ -0,0 +1,35 @@ +{ + "entryPoints": [ + "./src/index.ts", + "./src/bases/base10.ts", + "./src/bases/base16.ts", + "./src/bases/base2.ts", + "./src/bases/base256emoji.ts", + "./src/bases/base32.ts", + "./src/bases/base36.ts", + "./src/bases/base58.ts", + "./src/bases/base64.ts", + "./src/bases/base8.ts", + "./src/bases/identity.ts", + "./src/bases/interface.ts", + "./src/basics.ts", + "./src/block.ts", + "./src/block/interface.ts", + "./src/bytes.ts", + "./src/cid.ts", + "./src/codecs/interface.ts", + "./src/codecs/json.ts", + "./src/codecs/raw.ts", + "./src/hashes/digest.ts", + "./src/hashes/hasher.ts", + "./src/hashes/identity.ts", + "./src/hashes/interface.ts", + "./src/hashes/sha1.ts", + "./src/hashes/sha2.ts", + "./src/interface.ts", + "./src/link.ts", + "./src/link/interface.ts", + "./src/traversal.ts" + ], + "includeVersion": true +}