diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0bc3b42..d401a77 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ updates: schedule: interval: daily time: "10:00" - open-pull-requests-limit: 10 + open-pull-requests-limit: 20 commit-message: prefix: "deps" prefix-development: "deps(dev)" diff --git a/README.md b/README.md index 5b5a7b6..335c844 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# @multiformats/multiaddr-matcher + [![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-multiaddr-matcher.svg?style=flat-square)](https://codecov.io/gh/multiformats/js-multiaddr-matcher) [![CI](https://img.shields.io/github/actions/workflow/status/multiformats/js-multiaddr-matcher/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/multiformats/js-multiaddr-matcher/actions/workflows/js-test-and-release.yml?query=branch%3Amain) @@ -6,6 +8,21 @@ # About + + This module exports various matchers that can be used to infer the type of a passed multiaddr. @@ -44,7 +61,7 @@ $ npm i @multiformats/multiaddr-matcher ## Browser ` @@ -58,8 +75,8 @@ Loading this module through a script tag will make it's exports available as `Mu Licensed under either of -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) +- Apache 2.0, ([LICENSE-APACHE](https://github.com/multiformats/js-multiaddr-matcher/LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](https://github.com/multiformats/js-multiaddr-matcher/LICENSE-MIT) / ) # Contribution diff --git a/package.json b/package.json index 5017c63..958f7aa 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,31 @@ "bugs": { "url": "https://github.com/multiformats/js-multiaddr-matcher/issues" }, + "publishConfig": { + "access": "public", + "provenance": true + }, "keywords": [ "multiaddr" ], "type": "module", "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, "files": [ "src", "dist", @@ -26,6 +46,10 @@ ".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" + }, + "./utils": { + "types": "./dist/src/utils.d.ts", + "import": "./dist/src/utils.js" } }, "eslintConfig": { diff --git a/src/index.ts b/src/index.ts index 9c0ee4f..ca47d9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,224 +33,29 @@ */ import { isIPv4, isIPv6 } from '@chainsafe/is-ip' -import { type Multiaddr } from '@multiformats/multiaddr' -import { base58btc } from 'multiformats/bases/base58' -import { base64url } from 'multiformats/bases/base64' - -/** - * Split a multiaddr into path components - */ -const toParts = (ma: Multiaddr): string[] => { - return ma.toString().split('/').slice(1) -} +import { and, or, literal, string, peerId, optional, fmt, func, number, certhash } from './utils.js' +import type { Multiaddr } from '@multiformats/multiaddr' /** * A matcher accepts multiaddr components and either fails to match and returns * false or returns a sublist of unmatched components */ -interface Matcher { +export interface Matcher { match(parts: string[]): string[] | false pattern: string } -const func = (fn: (val: string) => boolean): Matcher => { - return { - match: (vals) => { - if (vals.length < 1) { - return false - } - - if (fn(vals[0])) { - return vals.slice(1) - } - - return false - }, - pattern: 'fn' - } -} - -const literal = (str: string): Matcher => { - return { - match: (vals) => func((val) => val === str).match(vals), - pattern: str - } -} - -const string = (): Matcher => { - return { - match: (vals) => func((val) => typeof val === 'string').match(vals), - pattern: '{string}' - } -} - -const number = (): Matcher => { - return { - match: (vals) => func((val) => !isNaN(parseInt(val))).match(vals), - pattern: '{number}' - } -} - -const peerId = (): Matcher => { - return { - match: (vals) => { - if (vals.length < 2) { - return false - } - - if (vals[0] !== 'p2p' && vals[0] !== 'ipfs') { - return false - } - - // Q is RSA, 1 is Ed25519 or Secp256k1 - if (vals[1].startsWith('Q') || vals[1].startsWith('1')) { - try { - base58btc.decode(`z${vals[1]}`) - } catch (err) { - return false - } - } else { - return false - } - - return vals.slice(2) - }, - pattern: '/p2p/{peerid}' - } -} - -const certhash = (): Matcher => { - return { - match: (vals) => { - if (vals.length < 2) { - return false - } - - if (vals[0] !== 'certhash') { - return false - } - - try { - base64url.decode(vals[1]) - } catch { - return false - } - - return vals.slice(2) - }, - pattern: '/certhash/{certhash}' - } -} - -const optional = (matcher: Matcher): Matcher => { - return { - match: (vals) => { - const result = matcher.match(vals) - - if (result === false) { - return vals - } - - return result - }, - pattern: `optional(${matcher.pattern})` - } -} - -const or = (...matchers: Matcher[]): Matcher => { - return { - match: (vals) => { - let matches: string[] | undefined - - for (const matcher of matchers) { - const result = matcher.match(vals) - - // no match - if (result === false) { - continue - } - - // choose greediest matcher - if (matches == null || result.length < matches.length) { - matches = result - } - } - - if (matches == null) { - return false - } - - return matches - }, - pattern: `or(${matchers.map(m => m.pattern).join(', ')})` - } -} - -const and = (...matchers: Matcher[]): Matcher => { - return { - match: (vals) => { - for (const matcher of matchers) { - // pass what's left of the array - const result = matcher.match(vals) - - // no match - if (result === false) { - return false - } - - vals = result - } - - return vals - }, - pattern: `and(${matchers.map(m => m.pattern).join(', ')})` - } -} - -function fmt (...matchers: Matcher[]): MultiaddrMatcher { - function match (ma: Multiaddr): string[] | false { - let parts = toParts(ma) - - for (const matcher of matchers) { - const result = matcher.match(parts) - - if (result === false) { - return false - } - - parts = result - } - - return parts - } - - function matches (ma: Multiaddr): boolean { - const result = match(ma) - - return result !== false - } - - function exactMatch (ma: Multiaddr): boolean { - const result = match(ma) - - if (result === false) { - return false - } - - return result.length === 0 - } - - return { - matches, - exactMatch - } -} - /** * A MultiaddrMatcher allows interpreting a multiaddr as a certain type of * multiaddr */ export interface MultiaddrMatcher { + /** + * The matchers that make up this MultiaddrMatcher - useful if you want to + * make your own custom matchers + */ + matchers: Matcher[] + /** * Returns true if the passed multiaddr can be treated as this type of * multiaddr diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e1f4ce4 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,205 @@ +import { base58btc } from 'multiformats/bases/base58' +import { base64url } from 'multiformats/bases/base64' +import type { Matcher, MultiaddrMatcher } from './index.js' +import type { Multiaddr } from '@multiformats/multiaddr' + +/** + * Split a multiaddr into path components + */ +const toParts = (ma: Multiaddr): string[] => { + return ma.toString().split('/').slice(1) +} + +export const func = (fn: (val: string) => boolean): Matcher => { + return { + match: (vals) => { + if (vals.length < 1) { + return false + } + + if (fn(vals[0])) { + return vals.slice(1) + } + + return false + }, + pattern: 'fn' + } +} + +export const literal = (str: string): Matcher => { + return { + match: (vals) => func((val) => val === str).match(vals), + pattern: str + } +} + +export const string = (): Matcher => { + return { + match: (vals) => func((val) => typeof val === 'string').match(vals), + pattern: '{string}' + } +} + +export const number = (): Matcher => { + return { + match: (vals) => func((val) => !isNaN(parseInt(val))).match(vals), + pattern: '{number}' + } +} + +export const peerId = (): Matcher => { + return { + match: (vals) => { + if (vals.length < 2) { + return false + } + + if (vals[0] !== 'p2p' && vals[0] !== 'ipfs') { + return false + } + + // Q is RSA, 1 is Ed25519 or Secp256k1 + if (vals[1].startsWith('Q') || vals[1].startsWith('1')) { + try { + base58btc.decode(`z${vals[1]}`) + } catch (err) { + return false + } + } else { + return false + } + + return vals.slice(2) + }, + pattern: '/p2p/{peerid}' + } +} + +export const certhash = (): Matcher => { + return { + match: (vals) => { + if (vals.length < 2) { + return false + } + + if (vals[0] !== 'certhash') { + return false + } + + try { + base64url.decode(vals[1]) + } catch { + return false + } + + return vals.slice(2) + }, + pattern: '/certhash/{certhash}' + } +} + +export const optional = (matcher: Matcher): Matcher => { + return { + match: (vals) => { + const result = matcher.match(vals) + + if (result === false) { + return vals + } + + return result + }, + pattern: `optional(${matcher.pattern})` + } +} + +export const or = (...matchers: Matcher[]): Matcher => { + return { + match: (vals) => { + let matches: string[] | undefined + + for (const matcher of matchers) { + const result = matcher.match(vals) + + // no match + if (result === false) { + continue + } + + // choose greediest matcher + if (matches == null || result.length < matches.length) { + matches = result + } + } + + if (matches == null) { + return false + } + + return matches + }, + pattern: `or(${matchers.map(m => m.pattern).join(', ')})` + } +} + +export const and = (...matchers: Matcher[]): Matcher => { + return { + match: (vals) => { + for (const matcher of matchers) { + // pass what's left of the array + const result = matcher.match(vals) + + // no match + if (result === false) { + return false + } + + vals = result + } + + return vals + }, + pattern: `and(${matchers.map(m => m.pattern).join(', ')})` + } +} + +export function fmt (...matchers: Matcher[]): MultiaddrMatcher { + function match (ma: Multiaddr): string[] | false { + let parts = toParts(ma) + + for (const matcher of matchers) { + const result = matcher.match(parts) + + if (result === false) { + return false + } + + parts = result + } + + return parts + } + + function matches (ma: Multiaddr): boolean { + const result = match(ma) + + return result !== false + } + + function exactMatch (ma: Multiaddr): boolean { + const result = match(ma) + + if (result === false) { + return false + } + + return result.length === 0 + } + + return { + matchers, + matches, + exactMatch + } +} diff --git a/typedoc.json b/typedoc.json index f599dc7..afe3bc6 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,5 +1,6 @@ { "entryPoints": [ - "./src/index.ts" + "./src/index.ts", + "./src/utils.ts" ] }