diff --git a/benchmarks/operations/README.md b/benchmarks/operations/README.md new file mode 100644 index 00000000..bec526fe --- /dev/null +++ b/benchmarks/operations/README.md @@ -0,0 +1,26 @@ +# multiaddr Benchmark + +Benchmarks multiaddr performance during common operations - parsing strings, +encapsulating/decapsulating addresses, turning to bytes, decoding bytes, etc. + +## Running the benchmarks + +```console +% npm start + +> benchmarks-add-dir@1.0.0 start +> npm run build && node dist/src/index.js + + +> benchmarks-add-dir@1.0.0 build +> aegir build --bundle false + +[06:10:56] tsc [started] +[06:10:56] tsc [completed] +┌─────────┬──────────────────────────────────┬─────────────┬────────┬───────┬────────┐ +│ (index) │ Implementation │ ops/s │ ms/op │ runs │ p99 │ +├─────────┼──────────────────────────────────┼─────────────┼────────┼───────┼────────┤ +│ 0 │ 'head' │ '105679.89' │ '0.01' │ 50000 │ '0.01' │ +│ 1 │ '@multiformats/multiaddr@12.4.0' │ '18244.31' │ '0.06' │ 50000 │ '0.13' │ +└─────────┴──────────────────────────────────┴─────────────┴────────┴───────┴────────┘ +``` diff --git a/benchmarks/operations/package.json b/benchmarks/operations/package.json new file mode 100644 index 00000000..b97da0ba --- /dev/null +++ b/benchmarks/operations/package.json @@ -0,0 +1,21 @@ +{ + "name": "benchmarks-operations", + "version": "1.0.0", + "main": "index.js", + "private": true, + "type": "module", + "scripts": { + "clean": "aegir clean", + "build": "aegir build --bundle false", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "doc-check": "aegir doc-check", + "start": "npm run build && node dist/test/index.js" + }, + "devDependencies": { + "@multiformats/multiaddr-12.4.0": "npm:@multiformats/multiaddr@12.4.0", + "@multiformats/multiaddr": "../../", + "aegir": "^47.0.7", + "tinybench": "^4.0.1" + } +} diff --git a/benchmarks/operations/src/index.ts b/benchmarks/operations/src/index.ts new file mode 100644 index 00000000..336ce12b --- /dev/null +++ b/benchmarks/operations/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/benchmarks/operations/test/index.ts b/benchmarks/operations/test/index.ts new file mode 100644 index 00000000..1623c7cd --- /dev/null +++ b/benchmarks/operations/test/index.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-console */ + +import { multiaddr } from '@multiformats/multiaddr' +import { multiaddr as multiaddr12 } from '@multiformats/multiaddr-12.4.0' +import { Bench } from 'tinybench' + +const ITERATIONS = parseInt(process.env.ITERATIONS ?? '50000') +const MIN_TIME = parseInt(process.env.MIN_TIME ?? '1') +const RESULT_PRECISION = 2 + +function bench (m: typeof multiaddr | typeof multiaddr12): void { + const ma = m('/ip4/127.0.0.1/udp/1234/quic-v1/webtransport/certhash/uEiAkH5a4DPGKUuOBjYw0CgwjvcJCJMD2K_1aluKR_tpevQ/certhash/uEiAfbgiymPP2_nX7Dgir8B4QkksjHp2lVuJZz0F79Be9JA') + ma.encapsulate('/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') + ma.decapsulate('/quic-v1') + + const ma2 = m(ma.bytes) + ma2.encapsulate('/tls/sni/example.com/http/http-path/path%2Findex.html') + ma2.equals(ma) + + ma2.getPath() + ma2.getPeerId() + + const ma3 = m('/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/1234/quic-v1/webtransport/certhash/uEiAkH5a4DPGKUuOBjYw0CgwjvcJCJMD2K_1aluKR_tpevQ/certhash/uEiAfbgiymPP2_nX7Dgir8B4QkksjHp2lVuJZz0F79Be9JA') + ma3.encapsulate('/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') + ma3.decapsulate('/quic-v1') +} + +async function main (): Promise { + const suite = new Bench({ + iterations: ITERATIONS, + time: MIN_TIME + }) + suite.add('head', () => { + bench(multiaddr) + }) + suite.add('@multiformats/multiaddr@12.4.0', () => { + bench(multiaddr12) + }) + + await suite.run() + + console.table(suite.tasks.map(({ name, result }) => { + if (result?.error != null) { + console.error(result.error) + + return { + Implementation: name, + 'ops/s': 'error', + 'ms/op': 'error', + runs: 'error', + p99: 'error' + } + } + + return { + Implementation: name, + 'ops/s': result?.hz.toFixed(RESULT_PRECISION), + 'ms/op': result?.period.toFixed(RESULT_PRECISION), + runs: result?.samples.length, + p99: result?.p99.toFixed(RESULT_PRECISION) + } + })) +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/benchmarks/operations/tsconfig.json b/benchmarks/operations/tsconfig.json new file mode 100644 index 00000000..4a695eb5 --- /dev/null +++ b/benchmarks/operations/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../../" + } + ] +} diff --git a/package.json b/package.json index 2972907b..f1365976 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "@chainsafe/is-ip": "^2.0.1", "@chainsafe/netmask": "^2.0.0", "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", "multiformats": "^13.0.0", "uint8-varint": "^2.0.1", "uint8arrays": "^5.0.0" diff --git a/src/codec.ts b/src/codec.ts deleted file mode 100644 index e17237f5..00000000 --- a/src/codec.ts +++ /dev/null @@ -1,236 +0,0 @@ -import * as varint from 'uint8-varint' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { convertToBytes, convertToString } from './convert.js' -import { getProtocol } from './protocols-table.js' -import type { StringTuple, Tuple, Protocol } from './index.js' - -export interface MultiaddrParts { - bytes: Uint8Array - string: string - tuples: Tuple[] - stringTuples: StringTuple[] - path: string | null -} - -export function stringToMultiaddrParts (str: string): MultiaddrParts { - str = cleanPath(str) - const tuples: Tuple[] = [] - const stringTuples: StringTuple[] = [] - let path: string | null = null - - const parts = str.split('/').slice(1) - if (parts.length === 1 && parts[0] === '') { - return { - bytes: new Uint8Array(), - string: '/', - tuples: [], - stringTuples: [], - path: null - } - } - - for (let p = 0; p < parts.length; p++) { - const part = parts[p] - const proto = getProtocol(part) - - if (proto.size === 0) { - tuples.push([proto.code]) - stringTuples.push([proto.code]) - - continue - } - - p++ // advance addr part - if (p >= parts.length) { - throw new ParseError('invalid address: ' + str) - } - - // if it's a path proto, take the rest - if (proto.path === true) { - // should we need to check each path part to see if it's a proto? - // This would allow for other protocols to be added after a unix path, - // however it would have issues if the path had a protocol name in the path - path = cleanPath(parts.slice(p).join('/')) - tuples.push([proto.code, convertToBytes(proto.code, path)]) - stringTuples.push([proto.code, path]) - break - } - - const bytes = convertToBytes(proto.code, parts[p]) - tuples.push([proto.code, bytes]) - stringTuples.push([proto.code, convertToString(proto.code, bytes)]) - } - - return { - string: stringTuplesToString(stringTuples), - bytes: tuplesToBytes(tuples), - tuples, - stringTuples, - path - } -} - -export function bytesToMultiaddrParts (bytes: Uint8Array): MultiaddrParts { - const tuples: Tuple[] = [] - const stringTuples: StringTuple[] = [] - let path: string | null = null - - let i = 0 - while (i < bytes.length) { - const code = varint.decode(bytes, i) - const n = varint.encodingLength(code) - - const p = getProtocol(code) - - const size = sizeForAddr(p, bytes.slice(i + n)) - - if (size === 0) { - tuples.push([code]) - stringTuples.push([code]) - i += n - - continue - } - - const addr = bytes.slice(i + n, i + n + size) - - i += (size + n) - - if (i > bytes.length) { // did not end _exactly_ at buffer.length - throw new ParseError('Invalid address Uint8Array: ' + uint8ArrayToString(bytes, 'base16')) - } - - // ok, tuple seems good. - tuples.push([code, addr]) - const stringAddr = convertToString(code, addr) - stringTuples.push([code, stringAddr]) - if (p.path === true) { - // should we need to check each path part to see if it's a proto? - // This would allow for other protocols to be added after a unix path, - // however it would have issues if the path had a protocol name in the path - path = stringAddr - break - } - } - - return { - bytes: Uint8Array.from(bytes), - string: stringTuplesToString(stringTuples), - tuples, - stringTuples, - path - } -} - -/** - * [[num code, str value?]... ] -> Tuple[] - */ -export function stringTuplesToTuples (stringTuples: StringTuple[]): Tuple[] { - const tuples: Tuple[] = [] - - stringTuples.forEach(([code, value]) => { - const tuple: Tuple = [code] - - if (value != null) { - tuple[1] = convertToBytes(code, value) - } - - tuples.push(tuple) - }) - - return tuples -} - -/** - * [[num code, str value?]... ] -> string - */ -function stringTuplesToString (tuples: StringTuple[]): string { - const parts: string[] = [] - tuples.map((tup) => { - const proto = getProtocol(tup[0]) - parts.push(proto.name) - if (tup.length > 1 && tup[1] != null) { - parts.push(tup[1]) - } - return null - }) - - return cleanPath(parts.join('/')) -} - -/** - * [[int code, Uint8Array ]... ] -> Uint8Array - */ -export function tuplesToBytes (tuples: Tuple[]): Uint8Array { - return uint8ArrayConcat(tuples.map((tup) => { - const proto = getProtocol(tup[0]) - let buf: Uint8Array = Uint8Array.from(varint.encode(proto.code)) - - if (tup.length > 1 && tup[1] != null) { - buf = uint8ArrayConcat([buf, tup[1]]) // add address buffer - } - - return buf - })) -} - -/** - * For the passed address, return the serialized size - */ -function sizeForAddr (p: Protocol, addr: Uint8Array | number[]): number { - if (p.size > 0) { - return p.size / 8 - } else if (p.size === 0) { - return 0 - } else { - const size = varint.decode(addr instanceof Uint8Array ? addr : Uint8Array.from(addr)) - return size + varint.encodingLength(size) - } -} - -export function bytesToTuples (buf: Uint8Array): Tuple[] { - const tuples: Array<[number, Uint8Array?]> = [] - let i = 0 - while (i < buf.length) { - const code = varint.decode(buf, i) - const n = varint.encodingLength(code) - - const p = getProtocol(code) - - const size = sizeForAddr(p, buf.slice(i + n)) - - if (size === 0) { - tuples.push([code]) - i += n - - continue - } - - const addr = buf.slice(i + n, i + n + size) - - i += (size + n) - - if (i > buf.length) { // did not end _exactly_ at buffer.length - throw new ParseError('Invalid address Uint8Array: ' + uint8ArrayToString(buf, 'base16')) - } - - // ok, tuple seems good. - tuples.push([code, addr]) - } - - return tuples -} - -export function cleanPath (str: string): string { - return '/' + str.trim().split('/').filter((a) => a).join('/') -} - -export class ParseError extends Error { - static name = 'ParseError' - name = 'ParseError' - - constructor (str: string) { - super(`Error parsing address: ${str}`) - } -} diff --git a/src/components.ts b/src/components.ts new file mode 100644 index 00000000..67cd025b --- /dev/null +++ b/src/components.ts @@ -0,0 +1,205 @@ +import * as varint from 'uint8-varint' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { InvalidMultiaddrError } from './errors.ts' +import { registry, V } from './registry.ts' +import type { Component } from './index.js' +import type { ProtocolCodec } from './registry.ts' + +export function bytesToComponents (bytes: Uint8Array): Component[] { + const components: Component[] = [] + + let i = 0 + while (i < bytes.length) { + const code = varint.decode(bytes, i) + const codec = registry.getCodec(code) + const codeLength = varint.encodingLength(code) + const size = sizeForAddr(codec, bytes, i + codeLength) + let sizeLength = 0 + + if (size > 0 && codec.size === V) { + sizeLength = varint.encodingLength(size) + } + + const componentLength = codeLength + sizeLength + size + + const component: Component = { + code, + name: codec.name, + bytes: bytes.subarray(i, i + componentLength) + } + + if (size > 0) { + const valueOffset = i + codeLength + sizeLength + const valueBytes = bytes.subarray(valueOffset, valueOffset + size) + + component.value = codec.bytesToValue?.(valueBytes) ?? uint8ArrayToString(valueBytes) + } + + components.push(component) + + i += componentLength + } + + return components +} + +export function componentsToBytes (components: Component[]): Uint8Array { + let length = 0 + const bytes: Uint8Array[] = [] + + for (const component of components) { + if (component.bytes == null) { + const codec = registry.getCodec(component.code) + const codecLength = varint.encodingLength(component.code) + let valueBytes: Uint8Array | undefined + let valueLength = 0 + let valueLengthLength = 0 + + if (component.value != null) { + valueBytes = codec.valueToBytes?.(component.value) ?? uint8ArrayFromString(component.value) + valueLength = valueBytes.byteLength + + if (codec.size === V) { + valueLengthLength = varint.encodingLength(valueLength) + } + } + + const bytes = new Uint8Array(codecLength + valueLengthLength + valueLength) + + // encode the protocol code + let offset = 0 + varint.encodeUint8Array(component.code, bytes, offset) + offset += codecLength + + // if there is a value + if (valueBytes != null) { + // if the value has variable length, encode the length + if (codec.size === V) { + varint.encodeUint8Array(valueLength, bytes, offset) + offset += valueLengthLength + } + + // finally encode the value + bytes.set(valueBytes, offset) + } + + component.bytes = bytes + } + + bytes.push(component.bytes) + length += component.bytes.byteLength + } + + return uint8ArrayConcat(bytes, length) +} + +export function stringToComponents (string: string): Component[] { + if (string.charAt(0) !== '/') { + throw new InvalidMultiaddrError('String multiaddr must start with "/"') + } + + const components: Component[] = [] + let collecting: 'protocol' | 'value' = 'protocol' + let value = '' + let protocol = '' + + for (let i = 1; i < string.length; i++) { + const char = string.charAt(i) + + if (char !== '/') { + if (collecting === 'protocol') { + protocol += string.charAt(i) + } else { + value += string.charAt(i) + } + } + + const ended = i === string.length - 1 + + if (char === '/' || ended) { + const codec = registry.getCodec(protocol) + + if (collecting === 'protocol') { + if (codec.size == null || codec.size === 0) { + // a protocol without an address, eg. `/tls` + components.push({ + code: codec.code, + name: codec.name + }) + + value = '' + protocol = '' + collecting = 'protocol' + + continue + } else if (ended) { + throw new InvalidMultiaddrError(`Component ${protocol} was missing value`) + } + + // continue collecting value + collecting = 'value' + } else if (collecting === 'value') { + const component: Component = { + code: codec.code, + name: codec.name + } + + if (codec.size != null && codec.size !== 0) { + if (value === '') { + throw new InvalidMultiaddrError(`Component ${protocol} was missing value`) + } + + component.value = codec.stringToValue?.(value) ?? value + } + + components.push(component) + + value = '' + protocol = '' + collecting = 'protocol' + } + } + } + + if (protocol !== '' && value !== '') { + throw new InvalidMultiaddrError('Incomplete multiaddr') + } + + return components +} + +export function componentsToString (components: Component[]): string { + return `/${components.flatMap(component => { + if (component.value == null) { + return component.name + } + + const codec = registry.getCodec(component.code) + + if (codec == null) { + throw new InvalidMultiaddrError(`Unknown protocol code ${component.code}`) + } + + return [ + component.name, + codec.valueToString?.(component.value) ?? component.value + ] + }).join('/')}` +} + +/** + * For the passed address, return the serialized size + */ +function sizeForAddr (codec: ProtocolCodec, bytes: Uint8Array, offset: number): number { + if (codec.size == null || codec.size === 0) { + return 0 + } + + if (codec.size > 0) { + return codec.size / 8 + } + + return varint.decode(bytes, offset) +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..956093ed --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,40 @@ +export const CODE_IP4 = 4 +export const CODE_TCP = 6 +export const CODE_UDP = 273 +export const CODE_DCCP = 33 +export const CODE_IP6 = 41 +export const CODE_IP6ZONE = 42 +export const CODE_IPCIDR = 43 +export const CODE_DNS = 53 +export const CODE_DNS4 = 54 +export const CODE_DNS6 = 55 +export const CODE_DNSADDR = 56 +export const CODE_SCTP = 132 +export const CODE_UDT = 301 +export const CODE_UTP = 302 +export const CODE_UNIX = 400 +export const CODE_P2P = 421 // also IPFS +export const CODE_ONION = 444 +export const CODE_ONION3 = 445 +export const CODE_GARLIC64 = 446 +export const CODE_GARLIC32 = 447 +export const CODE_TLS = 448 +export const CODE_SNI = 449 +export const CODE_NOISE = 454 +export const CODE_QUIC = 460 +export const CODE_QUIC_V1 = 461 +export const CODE_WEBTRANSPORT = 465 +export const CODE_CERTHASH = 466 +export const CODE_HTTP = 480 +export const CODE_HTTP_PATH = 481 +export const CODE_HTTPS = 443 +export const CODE_WS = 477 +export const CODE_WSS = 478 +export const CODE_P2P_WEBSOCKET_STAR = 479 +export const CODE_P2P_STARDUST = 277 +export const CODE_P2P_WEBRTC_STAR = 275 +export const CODE_P2P_WEBRTC_DIRECT = 276 +export const CODE_WEBRTC_DIRECT = 280 +export const CODE_WEBRTC = 281 +export const CODE_P2P_CIRCUIT = 290 +export const CODE_MEMORY = 777 diff --git a/src/convert.ts b/src/convert.ts index cbc5b078..7f91bded 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -1,283 +1,274 @@ +import { isIPv4 } from '@chainsafe/is-ip' import { IpNet } from '@chainsafe/netmask' import { base32 } from 'multiformats/bases/base32' -import { base58btc } from 'multiformats/bases/base58' import { bases } from 'multiformats/basics' -import { CID } from 'multiformats/cid' -import * as Digest from 'multiformats/hashes/digest' -import * as varint from 'uint8-varint' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import * as ip from './ip.js' -import { getProtocol } from './protocols-table.js' -import type { Multiaddr } from './index.js' - -const ip4Protocol = getProtocol('ip4') -const ip6Protocol = getProtocol('ip6') -const ipcidrProtocol = getProtocol('ipcidr') - -/** - * converts (serializes) addresses - */ -export function convert (proto: string, a: string): Uint8Array -export function convert (proto: string, a: Uint8Array): string -export function convert (proto: string, a: string | Uint8Array): Uint8Array | string { - if (a instanceof Uint8Array) { - return convertToString(proto, a) - } else { - return convertToBytes(proto, a) +import { InvalidMultiaddrError } from './errors.ts' +import type { Multiaddr } from './index.ts' +import type { MultibaseCodec } from 'multiformats' +import type { SupportedEncodings } from 'uint8arrays/to-string' + +export function bytesToString (base: SupportedEncodings): (buf: Uint8Array) => string { + return (buf) => { + return uint8ArrayToString(buf, base) } } -/** - * Convert [code,Uint8Array] to string - */ -// eslint-disable-next-line complexity -export function convertToString (proto: number | string, buf: Uint8Array): string { - const protocol = getProtocol(proto) - switch (protocol.code) { - case 4: // ipv4 - case 41: // ipv6 - return bytes2ip(buf) - case 42: // ipv6zone - return bytes2str(buf) - case 43: // ipcidr - return uint8ArrayToString(buf, 'base10') - - case 6: // tcp - case 273: // udp - case 33: // dccp - case 132: // sctp - return bytes2port(buf).toString() - - case 53: // dns - case 54: // dns4 - case 55: // dns6 - case 56: // dnsaddr - case 400: // unix - case 449: // sni - case 777: // memory - return bytes2str(buf) - - case 421: // ipfs - return bytes2mh(buf) - case 444: // onion - return bytes2onion(buf) - case 445: // onion3 - return bytes2onion(buf) - case 466: // certhash - return bytes2mb(buf) - case 481: // http-path - return globalThis.encodeURIComponent(bytes2str(buf)) - default: - return uint8ArrayToString(buf, 'base16') // no clue. convert to hex +export function stringToBytes (base: SupportedEncodings): (value: string) => Uint8Array { + return (buf) => { + return uint8ArrayFromString(buf, base) } } -// eslint-disable-next-line complexity -export function convertToBytes (proto: string | number, str: string): Uint8Array { - const protocol = getProtocol(proto) - switch (protocol.code) { - case 4: // ipv4 - return ip2bytes(str) - case 41: // ipv6 - return ip2bytes(str) - case 42: // ipv6zone - return str2bytes(str) - case 43: // ipcidr - return uint8ArrayFromString(str, 'base10') - - case 6: // tcp - case 273: // udp - case 33: // dccp - case 132: // sctp - return port2bytes(parseInt(str, 10)) - - case 53: // dns - case 54: // dns4 - case 55: // dns6 - case 56: // dnsaddr - case 400: // unix - case 449: // sni - case 777: // memory - return str2bytes(str) - - case 421: // ipfs - return mh2bytes(str) - case 444: // onion - return onion2bytes(str) - case 445: // onion3 - return onion32bytes(str) - case 466: // certhash - return mb2bytes(str) - case 481: // http-path - return str2bytes(globalThis.decodeURIComponent(str)) - default: - return uint8ArrayFromString(str, 'base16') // no clue. convert from hex - } -} - -export function convertToIpNet (multiaddr: Multiaddr): IpNet { - let mask: string | undefined - let addr: string | undefined - multiaddr.stringTuples().forEach(([code, value]) => { - if (code === ip4Protocol.code || code === ip6Protocol.code) { - addr = value - } - if (code === ipcidrProtocol.code) { - mask = value - } - }) - if (mask == null || addr == null) { - throw new Error('Invalid multiaddr') - } - return new IpNet(addr, mask) -} - -const decoders = Object.values(bases).map((c) => c.decoder) -const anybaseDecoder = (function () { - let acc = decoders[0].or(decoders[1]) - decoders.slice(2).forEach((d) => (acc = acc.or(d))) - return acc -})() - -function ip2bytes (ipString: string): Uint8Array { - if (!ip.isIP(ipString)) { - throw new Error('invalid ip address') - } - return ip.toBytes(ipString) -} - -function bytes2ip (ipBuff: Uint8Array): string { - const ipString = ip.toString(ipBuff, 0, ipBuff.length) - if (ipString == null) { - throw new Error('ipBuff is required') - } - if (!ip.isIP(ipString)) { - throw new Error('invalid ip address') - } - return ipString +export function bytes2port (buf: Uint8Array): string { + const view = new DataView(buf.buffer) + return view.getUint16(buf.byteOffset).toString() } -function port2bytes (port: number): Uint8Array { +export function port2bytes (port: string | number): Uint8Array { const buf = new ArrayBuffer(2) const view = new DataView(buf) - view.setUint16(0, port) + view.setUint16(0, typeof port === 'string' ? parseInt(port) : port) return new Uint8Array(buf) } -function bytes2port (buf: Uint8Array): number { - const view = new DataView(buf.buffer) - return view.getUint16(buf.byteOffset) -} - -function str2bytes (str: string): Uint8Array { - const buf = uint8ArrayFromString(str) - const size = Uint8Array.from(varint.encode(buf.length)) - return uint8ArrayConcat([size, buf], size.length + buf.length) -} - -function bytes2str (buf: Uint8Array): string { - const size = varint.decode(buf) - buf = buf.slice(varint.encodingLength(size)) - - if (buf.length !== size) { - throw new Error('inconsistent lengths') - } - - return uint8ArrayToString(buf) -} - -function mh2bytes (hash: string): Uint8Array { - let mh - - if (hash[0] === 'Q' || hash[0] === '1') { - mh = Digest.decode(base58btc.decode(`z${hash}`)).bytes - } else { - mh = CID.parse(hash).multihash.bytes - } - - // the address is a varint prefixed multihash string representation - const size = Uint8Array.from(varint.encode(mh.length)) - return uint8ArrayConcat([size, mh], size.length + mh.length) -} - -function mb2bytes (mbstr: string): Uint8Array { - const mb = anybaseDecoder.decode(mbstr) - const size = Uint8Array.from(varint.encode(mb.length)) - return uint8ArrayConcat([size, mb], size.length + mb.length) -} -function bytes2mb (buf: Uint8Array): string { - const size = varint.decode(buf) - const hash = buf.slice(varint.encodingLength(size)) - - if (hash.length !== size) { - throw new Error('inconsistent lengths') - } - - return 'u' + uint8ArrayToString(hash, 'base64url') -} - -/** - * Converts bytes to bas58btc string - */ -function bytes2mh (buf: Uint8Array): string { - const size = varint.decode(buf) - const address = buf.slice(varint.encodingLength(size)) - - if (address.length !== size) { - throw new Error('inconsistent lengths') - } - - return uint8ArrayToString(address, 'base58btc') -} - -function onion2bytes (str: string): Uint8Array { +export function onion2bytes (str: string): Uint8Array { const addr = str.split(':') + if (addr.length !== 2) { throw new Error(`failed to parse onion addr: ["'${addr.join('", "')}'"]' does not contain a port number`) } + if (addr[0].length !== 16) { throw new Error(`failed to parse onion addr: ${addr[0]} not a Tor onion address.`) } // onion addresses do not include the multibase prefix, add it before decoding - const buf = base32.decode('b' + addr[0]) + const buf = uint8ArrayFromString(addr[0], 'base32') // onion port number const port = parseInt(addr[1], 10) + if (port < 1 || port > 65536) { throw new Error('Port number is not in range(1, 65536)') } + const portBuf = port2bytes(port) + return uint8ArrayConcat([buf, portBuf], buf.length + portBuf.length) } -function onion32bytes (str: string): Uint8Array { +export function onion32bytes (str: string): Uint8Array { const addr = str.split(':') + if (addr.length !== 2) { throw new Error(`failed to parse onion addr: ["'${addr.join('", "')}'"]' does not contain a port number`) } + if (addr[0].length !== 56) { throw new Error(`failed to parse onion addr: ${addr[0]} not a Tor onion3 address.`) } + // onion addresses do not include the multibase prefix, add it before decoding const buf = base32.decode(`b${addr[0]}`) // onion port number const port = parseInt(addr[1], 10) + if (port < 1 || port > 65536) { throw new Error('Port number is not in range(1, 65536)') } + const portBuf = port2bytes(port) + return uint8ArrayConcat([buf, portBuf], buf.length + portBuf.length) } -function bytes2onion (buf: Uint8Array): string { - const addrBytes = buf.slice(0, buf.length - 2) - const portBytes = buf.slice(buf.length - 2) +export function bytes2onion (buf: Uint8Array): string { + const addrBytes = buf.subarray(0, buf.length - 2) + const portBytes = buf.subarray(buf.length - 2) const addr = uint8ArrayToString(addrBytes, 'base32') const port = bytes2port(portBytes) return `${addr}:${port}` } + +// Copied from https://github.com/indutny/node-ip/blob/master/lib/ip.js#L7 +// but with buf/offset args removed because we don't use them +export const ip4ToBytes = function (ip: string): Uint8Array { + ip = ip.toString().trim() + + const bytes = new Uint8Array(4) + + ip.split(/\./g).forEach((byte, index) => { + const value = parseInt(byte, 10) + + if (isNaN(value) || value < 0 || value > 0xff) { + throw new InvalidMultiaddrError('Invalid byte value in IP address') + } + + bytes[index] = value + }) + + return bytes +} + +// Copied from https://github.com/indutny/node-ip/blob/master/lib/ip.js#L7 +// but with buf/offset args removed because we don't use them +export const ip6ToBytes = function (ip: string): Uint8Array { + let offset = 0 + ip = ip.toString().trim() + + const sections = ip.split(':', 8) + + let i + for (i = 0; i < sections.length; i++) { + const isv4 = isIPv4(sections[i]) + let v4Buffer: Uint8Array | undefined + + if (isv4) { + v4Buffer = ip4ToBytes(sections[i]) + sections[i] = uint8ArrayToString(v4Buffer.subarray(0, 2), 'base16') + } + + if (v4Buffer != null && ++i < 8) { + sections.splice(i, 0, uint8ArrayToString(v4Buffer.subarray(2, 4), 'base16')) + } + } + + if (sections[0] === '') { + while (sections.length < 8) { sections.unshift('0') } + } else if (sections[sections.length - 1] === '') { + while (sections.length < 8) { sections.push('0') } + } else if (sections.length < 8) { + for (i = 0; i < sections.length && sections[i] !== ''; i++) { } + const argv: [number, number, ...string[]] = [i, 1] + for (i = 9 - sections.length; i > 0; i--) { + argv.push('0') + } + sections.splice.apply(sections, argv) + } + + const bytes = new Uint8Array(offset + 16) + + for (i = 0; i < sections.length; i++) { + if (sections[i] === '') { + sections[i] = '0' + } + + const word = parseInt(sections[i], 16) + + if (isNaN(word) || word < 0 || word > 0xffff) { + throw new InvalidMultiaddrError('Invalid byte value in IP address') + } + + bytes[offset++] = (word >> 8) & 0xff + bytes[offset++] = word & 0xff + } + + return bytes +} + +// Copied from https://github.com/indutny/node-ip/blob/master/lib/ip.js#L63 +export const ip4ToString = function (buf: Uint8Array): string { + if (buf.byteLength !== 4) { + throw new InvalidMultiaddrError('IPv4 address was incorrect length') + } + + const result = [] + + for (let i = 0; i < buf.byteLength; i++) { + result.push(buf[i]) + } + + return result.join('.') +} + +export const ip6ToString = function (buf: Uint8Array): string { + if (buf.byteLength !== 16) { + throw new InvalidMultiaddrError('IPv6 address was incorrect length') + } + + const result: string[] = [] + + for (let i = 0; i < buf.byteLength; i += 2) { + const byte1 = buf[i] + const byte2 = buf[i + 1] + + let tuple = '' + + if (byte1 > 0) { + tuple = `${byte1.toString(16)}${byte2.toString(16).padStart(2, '0')}` + } else if (byte2 > 0) { + tuple = byte2.toString(16) + } + + result.push(tuple) + } + + return result.join(':') + .replace(/:(:)+/, '::') +} + +export function ip6StringToValue (str: string): string { + let parts = str.split(':') + + parts = parts.map((str, index) => { + if (index === parts.length - 1 && isIPv4(str)) { + const bytes = str.split('.') + return `${ + parseInt(bytes[0], 10).toString(16) + }${ + parseInt(bytes[1], 10).toString(16).padStart(2, '0') + }:${ + parseInt(bytes[2], 10).toString(16) + }${ + parseInt(bytes[3], 10).toString(16).padStart(2, '0') + }` + } + + return str.replace(/^(0+)/, '') + }) + + return `${parts.join(':')}` + .replace(/:(:)+/, '::') +} + +const decoders = Object.values(bases).map((c) => c.decoder) +const anybaseDecoder = (function () { + let acc = decoders[0].or(decoders[1]) + decoders.slice(2).forEach((d) => (acc = acc.or(d))) + return acc +})() + +export function mb2bytes (mbstr: string): Uint8Array { + return anybaseDecoder.decode(mbstr) +} + +export function bytes2mb (base: MultibaseCodec): (buf: Uint8Array) => string { + return (buf) => { + return base.encoder.encode(buf) + } +} + +export function convertToIpNet (multiaddr: Multiaddr): IpNet { + let mask: string | undefined + let addr: string | undefined + + multiaddr.getComponents().forEach(component => { + if (component.name === 'ip4' || component.name === 'ip6') { + addr = component.value + } + if (component.name === 'ipcidr') { + mask = component.value + } + }) + + if (mask == null || addr == null) { + throw new Error('Invalid multiaddr') + } + + return new IpNet(addr, mask) +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 00000000..be66a95f --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,22 @@ +/** + * Thrown when an invalid multiaddr is encountered + */ +export class InvalidMultiaddrError extends Error { + static name = 'InvalidMultiaddrError' + name = 'InvalidMultiaddrError' +} + +export class ValidationError extends Error { + static name = 'ValidationError' + name = 'ValidationError' +} + +export class InvalidParametersError extends Error { + static name = 'InvalidParametersError' + name = 'InvalidParametersError' +} + +export class InvalidProtocolError extends Error { + static name = 'InvalidProtocolError' + name = 'InvalidProtocolError' +} diff --git a/src/index.ts b/src/index.ts index f4e5e607..495f941f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,14 +93,18 @@ * ``` */ -import { stringTuplesToTuples, tuplesToBytes } from './codec.js' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { InvalidParametersError } from './errors.ts' import { Multiaddr as MultiaddrClass, symbol } from './multiaddr.js' -import { getProtocol } from './protocols-table.js' +import { registry } from './registry.ts' import type { Resolver } from './resolvers/index.js' import type { DNS } from '@multiformats/dns' +import type { AbortOptions } from 'abort-error' /** * Protocols are present in the protocol table + * + * @deprecated */ export interface Protocol { code: number @@ -132,27 +136,33 @@ export interface NodeAddress { /** * These types can be parsed into a {@link Multiaddr} object */ -export type MultiaddrInput = string | Multiaddr | Uint8Array | null +export type MultiaddrInput = string | Multiaddr | Uint8Array | null | Component[] /** * A code/value pair + * + * @deprecated Use Component instead */ export type Tuple = [number, Uint8Array?] /** * A code/value pair with the value as a string + * + * @deprecated Use Component instead */ export type StringTuple = [number, string?] /** * Allows aborting long-lived operations + * + * @deprecated Import from `abort-error` instead */ -export interface AbortOptions { - signal?: AbortSignal -} +export type { AbortOptions } /** * All configured {@link Resolver}s + * + * @deprecated DNS resolving will be removed in a future release */ export const resolvers = new Map() @@ -160,6 +170,9 @@ export type { Resolver } export { MultiaddrFilter } from './filter/multiaddr-filter.js' +/** + * @deprecated DNS resolving will be removed in a future release + */ export interface ResolveOptions extends AbortOptions { /** * An optional DNS resolver @@ -175,6 +188,37 @@ export interface ResolveOptions extends AbortOptions { maxRecursiveDepth?: number } +/** + * A Component is a section of a multiaddr with a name/code, possibly with a + * value. + * + * Component names/codes are defined in the protocol table. + * + * @see https://github.com/multiformats/multiaddr/blob/master/protocols.csv + */ +export interface Component { + /** + * The code of the component as defined in the protocol table + */ + code: number + + /** + * The name of the component as defined in the protocol table + */ + name: string + + /** + * The component value, if one is present + */ + value?: string + + /** + * The bytes that make up the component. This will be set if the multiaddr + * was parsed from a `Uint8Array`, or if `.bytes` has been accessed on it. + */ + bytes?: Uint8Array +} + export interface Multiaddr { bytes: Uint8Array @@ -205,7 +249,21 @@ export interface Multiaddr { toJSON(): string /** - * Returns Multiaddr as a convinient options object to be used with net.createConnection + * Returns the components that make up this Multiaddr + * + * @example + * ```ts + * import { multiaddr } from '@multiformats/multiaddr' + * + * multiaddr('/ip4/127.0.0.1/tcp/4001').getComponents() + * // [{ name: 'ip4', code: 4, value: '127.0.0.1' }, { name: 'tcp', code: 6, value: '4001' }] + * ``` + */ + getComponents(): Component[] + + /** + * Returns Multiaddr as a convenient options object to be used with + * `createConnection` from `node:net` * * @example * ```js @@ -218,9 +276,9 @@ export interface Multiaddr { toOptions(): MultiaddrObject /** - * Returns the protocols the Multiaddr is defined with, as an array of objects, in - * left-to-right order. Each object contains the protocol code, protocol name, - * and the size of its address space in bits. + * Returns the protocols the Multiaddr is defined with, as an array of + * objects, in left-to-right order. Each object contains the protocol code, + * protocol name, and the size of its address space in bits. * [See list of protocols](https://github.com/multiformats/multiaddr/blob/master/protocols.csv) * * @example @@ -231,6 +289,8 @@ export interface Multiaddr { * // [ { code: 4, size: 32, name: 'ip4' }, * // { code: 6, size: 16, name: 'tcp' } ] * ``` + * + * @deprecated Use `getComponents()` instead */ protos(): Protocol[] @@ -245,6 +305,8 @@ export interface Multiaddr { * multiaddr('/ip4/127.0.0.1/tcp/4001').protoCodes() * // [ 4, 6 ] * ``` + * + * @deprecated Use `getComponents()` instead */ protoCodes(): number[] @@ -259,6 +321,8 @@ export interface Multiaddr { * multiaddr('/ip4/127.0.0.1/tcp/4001').protoNames() * // [ 'ip4', 'tcp' ] * ``` + * + * @deprecated Use `getComponents()` instead */ protoNames(): string[] @@ -272,6 +336,8 @@ export interface Multiaddr { * multiaddr('/ip4/127.0.0.1/tcp/4001').tuples() * // [ [ 4, ], [ 6, ] ] * ``` + * + * @deprecated Use `getComponents()` instead */ tuples(): Tuple[] @@ -287,6 +353,8 @@ export interface Multiaddr { * multiaddr('/ip4/127.0.0.1/tcp/4001').stringTuples() * // [ [ 4, '127.0.0.1' ], [ 6, '4001' ] ] * ``` + * + * @deprecated Use `getComponents()` instead */ stringTuples(): StringTuple[] @@ -339,8 +407,8 @@ export interface Multiaddr { decapsulate(addr: Multiaddr | string): Multiaddr /** - * A more reliable version of `decapsulate` if you are targeting a - * specific code, such as 421 (the `p2p` protocol code). The last index of the code + * A more reliable version of `decapsulate` if you are targeting a specific + * code, such as 421 (the `p2p` protocol code). The last index of the code * will be removed from the `Multiaddr`, and a new instance will be returned. * If the code is not present, the original `Multiaddr` is returned. * @@ -373,6 +441,8 @@ export interface Multiaddr { * // should return QmValidBase58string or null if the id is missing or invalid * const peerId = mh1.getPeerId() * ``` + * + * @deprecated A multiaddr can contain multiple PeerIds, use stringTuples() to get a specific one */ getPeerId(): string | null @@ -389,6 +459,8 @@ export interface Multiaddr { * // should return utf8 string or null if the id is missing or invalid * const path = mh1.getPath() * ``` + * + * @deprecated A multiaddr can contain multiple tuples that could be interpreted as paths, use stringTuples() to get a specific one */ getPath(): string | null @@ -430,13 +502,15 @@ export interface Multiaddr { * // Multiaddr(/ip4/147.75.83.83/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb) * // ] * ``` + * + * @deprecated If you need to resolve `dnsaddr` addresses, use `getComponents()` to extract them and perform the resolution yourself */ resolve(options?: ResolveOptions): Promise /** - * Gets a Multiaddrs node-friendly address object. Note that protocol information - * is left out: in Node (and most network systems) the protocol is unknowable - * given only the address. + * Gets a Multiaddrs node-friendly address object. Note that protocol + * information is left out: in Node (and most network systems) the protocol is + * unknowable given only the address. * * Has to be a ThinWaist Address, otherwise throws error * @@ -495,10 +569,10 @@ export interface Multiaddr { */ export function fromNodeAddress (addr: NodeAddress, transport: string): Multiaddr { if (addr == null) { - throw new Error('requires node address object') + throw new InvalidParametersError('requires node address object') } if (transport == null) { - throw new Error('requires transport protocol') + throw new InvalidParametersError('requires transport protocol') } let ip: string | undefined let host = addr.address @@ -518,12 +592,13 @@ export function fromNodeAddress (addr: NodeAddress, transport: string): Multiadd host = parts[0] const zone = parts[1] - ip = `/ip6zone/${zone}/ip6` + ip = `ip6zone/${zone}/ip6` } break default: throw Error('Invalid addr family, should be 4 or 6.') } + return new MultiaddrClass('/' + [ip, host, transport, addr.port].join('/')) } @@ -545,7 +620,20 @@ export function fromNodeAddress (addr: NodeAddress, transport: string): Multiadd * ``` */ export function fromTuples (tuples: Tuple[]): Multiaddr { - return multiaddr(tuplesToBytes(tuples)) + return multiaddr(tuples.map(([code, value]) => { + const codec = registry.getCodec(code) + + const component: Component = { + code, + name: codec.name + } + + if (value != null) { + component.value = codec.bytesToValue?.(value) ?? uint8ArrayToString(value) + } + + return component + })) } /** @@ -566,7 +654,20 @@ export function fromTuples (tuples: Tuple[]): Multiaddr { * ``` */ export function fromStringTuples (tuples: StringTuple[]): Multiaddr { - return fromTuples(stringTuplesToTuples(tuples)) + return multiaddr(tuples.map(([code, value]) => { + const codec = registry.getCodec(code) + + const component: Component = { + code, + name: codec.name + } + + if (value != null) { + component.value = value + } + + return component + })) } /** @@ -627,4 +728,50 @@ export function multiaddr (addr?: MultiaddrInput): Multiaddr { return new MultiaddrClass(addr) } -export { getProtocol as protocols } +/** + * For the passed proto string or number, return a {@link Protocol} + * + * @example + * + * ```js + * import { protocol } from '@multiformats/multiaddr' + * + * console.info(protocol(4)) + * // { code: 4, size: 32, name: 'ip4', resolvable: false, path: false } + * ``` + * + * @deprecated This will be removed in a future version + */ +export function protocols (proto: number | string): Protocol { + const codec = registry.getCodec(proto) + + return { + code: codec.code, + size: codec.size ?? 0, + name: codec.name, + resolvable: Boolean(codec.resolvable), + path: Boolean(codec.path) + } +} + +/** + * Ensures all multiaddr tuples are correct. Throws if any contain invalid + * values. + * + * @example + * + * ```ts + * import { multiaddr, validate } from '@multiformats/multiaddr' + * + * const ma = multiaddr('/ip4/123.123.123.123/tcp/1234') + * + * validate(ma) // ok! + * ``` + */ +export { validate } from './multiaddr.ts' + +/** + * Export all table.csv codes. These are all named exports so can be tree-shaken + * out by bundlers. + */ +export * from './constants.ts' diff --git a/src/ip.ts b/src/ip.ts deleted file mode 100644 index 91bb1f16..00000000 --- a/src/ip.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { isIPv4, isIPv6 } from '@chainsafe/is-ip' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' - -export { isIP } from '@chainsafe/is-ip' -export const isV4 = isIPv4 -export const isV6 = isIPv6 - -// Copied from https://github.com/indutny/node-ip/blob/master/lib/ip.js#L7 -// but with buf/offset args removed because we don't use them -export const toBytes = function (ip: string): Uint8Array { - let offset = 0 - ip = ip.toString().trim() - - if (isV4(ip)) { - const bytes = new Uint8Array(offset + 4) - - ip.split(/\./g).forEach((byte) => { - bytes[offset++] = parseInt(byte, 10) & 0xff - }) - - return bytes - } - - if (isV6(ip)) { - const sections = ip.split(':', 8) - - let i - for (i = 0; i < sections.length; i++) { - const isv4 = isV4(sections[i]) - let v4Buffer: Uint8Array | undefined - - if (isv4) { - v4Buffer = toBytes(sections[i]) - sections[i] = uint8ArrayToString(v4Buffer.slice(0, 2), 'base16') - } - - if (v4Buffer != null && ++i < 8) { - sections.splice(i, 0, uint8ArrayToString(v4Buffer.slice(2, 4), 'base16')) - } - } - - if (sections[0] === '') { - while (sections.length < 8) { sections.unshift('0') } - } else if (sections[sections.length - 1] === '') { - while (sections.length < 8) { sections.push('0') } - } else if (sections.length < 8) { - for (i = 0; i < sections.length && sections[i] !== ''; i++) { } - const argv: [number, number, ...string[]] = [i, 1] - for (i = 9 - sections.length; i > 0; i--) { - argv.push('0') - } - sections.splice.apply(sections, argv) - } - - const bytes = new Uint8Array(offset + 16) - - for (i = 0; i < sections.length; i++) { - const word = parseInt(sections[i], 16) - bytes[offset++] = (word >> 8) & 0xff - bytes[offset++] = word & 0xff - } - - return bytes - } - - throw new Error('invalid ip address') -} - -// Copied from https://github.com/indutny/node-ip/blob/master/lib/ip.js#L63 -export const toString = function (buf: Uint8Array, offset: number = 0, length?: number): string { - offset = ~~offset - length = length ?? (buf.length - offset) - - const view = new DataView(buf.buffer) - - if (length === 4) { - const result = [] - - // IPv4 - for (let i = 0; i < length; i++) { - result.push(buf[offset + i]) - } - - return result.join('.') - } - - if (length === 16) { - const result = [] - - // IPv6 - for (let i = 0; i < length; i += 2) { - result.push(view.getUint16(offset + i).toString(16)) - } - - return result.join(':') - .replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3') - .replace(/:{3,4}/, '::') - } - - return '' -} diff --git a/src/multiaddr.ts b/src/multiaddr.ts index e99df803..09a445f7 100644 --- a/src/multiaddr.ts +++ b/src/multiaddr.ts @@ -1,22 +1,23 @@ -/* eslint-disable complexity */ import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { bytesToMultiaddrParts, stringToMultiaddrParts, tuplesToBytes } from './codec.js' -import { getProtocol, names } from './protocols-table.js' +import { bytesToComponents, componentsToBytes, componentsToString, stringToComponents } from './components.js' +import { CODE_DNS, CODE_DNS4, CODE_DNS6, CODE_DNSADDR, CODE_IP4, CODE_IP6, CODE_IP6ZONE, CODE_P2P, CODE_P2P_CIRCUIT, CODE_TCP, CODE_UDP } from './constants.ts' +import { InvalidMultiaddrError, InvalidParametersError } from './errors.ts' +import { registry } from './registry.ts' import { isMultiaddr, multiaddr, resolvers } from './index.js' -import type { MultiaddrParts } from './codec.js' -import type { MultiaddrInput, Multiaddr as MultiaddrInterface, MultiaddrObject, Protocol, StringTuple, Tuple, NodeAddress, ResolveOptions } from './index.js' +import type { MultiaddrInput, Multiaddr as MultiaddrInterface, MultiaddrObject, Protocol, Tuple, NodeAddress, ResolveOptions, Component } from './index.js' const inspect = Symbol.for('nodejs.util.inspect.custom') -export const symbol = Symbol.for('@multiformats/js-multiaddr/multiaddr') +export const symbol = Symbol.for('@multiformats/multiaddr') const DNS_CODES = [ - getProtocol('dns').code, - getProtocol('dns4').code, - getProtocol('dns6').code, - getProtocol('dnsaddr').code + CODE_DNS, + CODE_DNS4, + CODE_DNS6, + CODE_DNSADDR ] class NoAvailableResolverError extends Error { @@ -26,46 +27,65 @@ class NoAvailableResolverError extends Error { } } +function toComponents (addr: MultiaddrInput): Component[] { + if (addr == null) { + addr = '/' + } + + if (isMultiaddr(addr)) { + return addr.getComponents() + } + + if (addr instanceof Uint8Array) { + return bytesToComponents(addr) + } + + if (typeof addr === 'string') { + addr = addr + .replace(/\/(\/)+/, '/') + .replace(/(\/)+$/, '') + + if (addr === '') { + addr = '/' + } + + return stringToComponents(addr) + } + + if (Array.isArray(addr)) { + return addr + } + + throw new InvalidMultiaddrError('Must be a string, Uint8Array, Component[], or another Multiaddr') +} + /** * Creates a {@link Multiaddr} from a {@link MultiaddrInput} */ export class Multiaddr implements MultiaddrInterface { - public bytes: Uint8Array - readonly #string: string - readonly #tuples: Tuple[] - readonly #stringTuples: StringTuple[] - readonly #path: string | null - [symbol]: boolean = true + readonly #components: Component[] - constructor (addr?: MultiaddrInput) { - // default - if (addr == null) { - addr = '' - } + #string: string | undefined + #bytes: Uint8Array | undefined - let parts: MultiaddrParts - if (addr instanceof Uint8Array) { - parts = bytesToMultiaddrParts(addr) - } else if (typeof addr === 'string') { - if (addr.length > 0 && addr.charAt(0) !== '/') { - throw new Error(`multiaddr "${addr}" must start with a "/"`) - } - parts = stringToMultiaddrParts(addr) - } else if (isMultiaddr(addr)) { // Multiaddr - parts = bytesToMultiaddrParts(addr.bytes) - } else { - throw new Error('addr must be a string, Buffer, or another Multiaddr') + constructor (addr: MultiaddrInput | Component[] = '/') { + this.#components = toComponents(addr) + } + + get bytes (): Uint8Array { + if (this.#bytes == null) { + this.#bytes = componentsToBytes(this.#components) } - this.bytes = parts.bytes - this.#string = parts.string - this.#tuples = parts.tuples - this.#stringTuples = parts.stringTuples - this.#path = parts.path + return this.#bytes } toString (): string { + if (this.#string == null) { + this.#string = componentsToString(this.#components) + } + return this.#string } @@ -80,35 +100,28 @@ export class Multiaddr implements MultiaddrInterface { let port: number | undefined let zone = '' - const tcp = getProtocol('tcp') - const udp = getProtocol('udp') - const ip4 = getProtocol('ip4') - const ip6 = getProtocol('ip6') - const dns6 = getProtocol('dns6') - const ip6zone = getProtocol('ip6zone') - - for (const [code, value] of this.stringTuples()) { - if (code === ip6zone.code) { + for (const { code, name, value } of this.#components) { + if (code === CODE_IP6ZONE) { zone = `%${value ?? ''}` } // default to https when protocol & port are omitted from DNS addrs if (DNS_CODES.includes(code)) { - transport = tcp.name === 'tcp' ? 'tcp' : 'udp' + transport = 'tcp' port = 443 host = `${value ?? ''}${zone}` - family = code === dns6.code ? 6 : 4 + family = code === CODE_DNS6 ? 6 : 4 } - if (code === tcp.code || code === udp.code) { - transport = getProtocol(code).name === 'tcp' ? 'tcp' : 'udp' + if (code === CODE_TCP || code === CODE_UDP) { + transport = name === 'tcp' ? 'tcp' : 'udp' port = parseInt(value ?? '') } - if (code === ip4.code || code === ip6.code) { - transport = getProtocol(code).name === 'tcp' ? 'tcp' : 'udp' + if (code === CODE_IP4 || code === CODE_IP6) { + transport = 'tcp' host = `${value ?? ''}${zone}` - family = code === ip6.code ? 6 : 4 + family = code === CODE_IP6 ? 6 : 4 } } @@ -126,30 +139,53 @@ export class Multiaddr implements MultiaddrInterface { return opts } + getComponents (): Component[] { + return [ + ...this.#components + ] + } + protos (): Protocol[] { - return this.#tuples.map(([code]) => Object.assign({}, getProtocol(code))) + return this.#components.map(({ code, value }) => { + const codec = registry.getCodec(code) + + return { + code, + size: codec.size ?? 0, + name: codec.name, + resolvable: Boolean(codec.resolvable), + path: Boolean(codec.path) + } + }) } protoCodes (): number[] { - return this.#tuples.map(([code]) => code) + return this.#components.map(({ code }) => code) } protoNames (): string[] { - return this.#tuples.map(([code]) => getProtocol(code).name) + return this.#components.map(({ name }) => name) } - tuples (): Array<[number, Uint8Array?]> { - return this.#tuples.map(([code, value]) => { + tuples (): Tuple[] { + return this.#components.map(({ code, value }) => { if (value == null) { return [code] } - return [code, value] + const codec = registry.getCodec(code) + const output: Tuple = [code] + + if (value != null) { + output.push(codec.valueToBytes?.(value) ?? uint8ArrayFromString(value)) + } + + return output }) } stringTuples (): Array<[number, string?]> { - return this.#stringTuples.map(([code, value]) => { + return this.#components.map(({ code, value }) => { if (value == null) { return [code] } @@ -158,43 +194,52 @@ export class Multiaddr implements MultiaddrInterface { }) } - encapsulate (addr: MultiaddrInput): Multiaddr { - addr = new Multiaddr(addr) - return new Multiaddr(this.toString() + addr.toString()) + encapsulate (addr: MultiaddrInput): MultiaddrInterface { + const ma = new Multiaddr(addr) + + return new Multiaddr([ + ...this.#components, + ...ma.getComponents() + ]) } - decapsulate (addr: Multiaddr | string): Multiaddr { + decapsulate (addr: Multiaddr | string): MultiaddrInterface { const addrString = addr.toString() const s = this.toString() const i = s.lastIndexOf(addrString) + if (i < 0) { - throw new Error(`Address ${this.toString()} does not contain subaddress: ${addr.toString()}`) + throw new InvalidParametersError(`Address ${this.toString()} does not contain subaddress: ${addr.toString()}`) } + return new Multiaddr(s.slice(0, i)) } decapsulateCode (code: number): Multiaddr { - const tuples = this.tuples() - for (let i = tuples.length - 1; i >= 0; i--) { - if (tuples[i][0] === code) { - return new Multiaddr(tuplesToBytes(tuples.slice(0, i))) + let index + + for (let i = this.#components.length - 1; i > -1; i--) { + if (this.#components[i].code === code) { + index = i + break } } - return this + + return new Multiaddr(this.#components.slice(0, index)) } getPeerId (): string | null { try { let tuples: Array<[number, string | undefined]> = [] - this.stringTuples().forEach(([code, name]) => { - if (code === names.p2p.code) { - tuples.push([code, name]) + this.#components.forEach(({ code, value }) => { + if (code === CODE_P2P) { + tuples.push([code, value]) } // if this is a p2p-circuit address, return the target peer id if present // not the peer id of the relay - if (code === names['p2p-circuit'].code) { + if (code === CODE_P2P_CIRCUIT) { tuples = [] } }) @@ -221,7 +266,17 @@ export class Multiaddr implements MultiaddrInterface { } getPath (): string | null { - return this.#path + for (const component of this.#components) { + const codec = registry.getCodec(component.code) + + if (!codec.path) { + continue + } + + return component.value ?? null + } + + return null } equals (addr: { bytes: Uint8Array }): boolean { @@ -260,19 +315,19 @@ export class Multiaddr implements MultiaddrInterface { } } - isThinWaistAddress (addr?: Multiaddr): boolean { - const protos = (addr ?? this).protos() - - if (protos.length !== 2) { + isThinWaistAddress (): boolean { + if (this.#components.length !== 2) { return false } - if (protos[0].code !== 4 && protos[0].code !== 41) { + if (this.#components[0].code !== CODE_IP4 && this.#components[0].code !== CODE_IP6) { return false } - if (protos[1].code !== 6 && protos[1].code !== 273) { + + if (this.#components[1].code !== CODE_TCP && this.#components[1].code !== CODE_UDP) { return false } + return true } @@ -289,6 +344,23 @@ export class Multiaddr implements MultiaddrInterface { * ``` */ [inspect] (): string { - return `Multiaddr(${this.#string})` + return `Multiaddr(${this.toString()})` } } + +/** + * Ensures all multiaddr tuples are correct. Throws if any invalid protocols or + * values are encountered. + */ +export function validate (addr: Multiaddr): void { + addr.getComponents() + .forEach(component => { + const codec = registry.getCodec(component.code) + + if (component.value == null) { + return + } + + codec.validate?.(component.value) + }) +} diff --git a/src/protocols-table.ts b/src/protocols-table.ts index 0d85beb5..82105da5 100644 --- a/src/protocols-table.ts +++ b/src/protocols-table.ts @@ -78,6 +78,8 @@ export function createProtocol (code: number, size: number, name: string, resolv * console.info(protocol(4)) * // { code: 4, size: 32, name: 'ip4', resolvable: false, path: false } * ``` + * + * @deprecated This will be removed in a future version */ export function getProtocol (proto: number | string): Protocol { if (typeof proto === 'number') { diff --git a/src/registry.ts b/src/registry.ts new file mode 100644 index 00000000..74f3b9d4 --- /dev/null +++ b/src/registry.ts @@ -0,0 +1,292 @@ +import { isIPv4, isIPv6 } from '@chainsafe/is-ip' +import { CID } from 'multiformats' +import { base64url } from 'multiformats/bases/base64' +import { CODE_CERTHASH, CODE_DCCP, CODE_DNS, CODE_DNS4, CODE_DNS6, CODE_DNSADDR, CODE_GARLIC32, CODE_GARLIC64, CODE_HTTP, CODE_HTTP_PATH, CODE_HTTPS, CODE_IP4, CODE_IP6, CODE_IP6ZONE, CODE_IPCIDR, CODE_MEMORY, CODE_NOISE, CODE_ONION, CODE_ONION3, CODE_P2P, CODE_P2P_CIRCUIT, CODE_P2P_STARDUST, CODE_P2P_WEBRTC_DIRECT, CODE_P2P_WEBRTC_STAR, CODE_P2P_WEBSOCKET_STAR, CODE_QUIC, CODE_QUIC_V1, CODE_SCTP, CODE_SNI, CODE_TCP, CODE_TLS, CODE_UDP, CODE_UDT, CODE_UNIX, CODE_UTP, CODE_WEBRTC, CODE_WEBRTC_DIRECT, CODE_WEBTRANSPORT, CODE_WS, CODE_WSS } from './constants.ts' +import { bytes2mb, bytes2onion, bytes2port, bytesToString, ip4ToBytes, ip4ToString, ip6StringToValue, ip6ToBytes, ip6ToString, mb2bytes, onion2bytes, onion32bytes, port2bytes, stringToBytes } from './convert.ts' +import { InvalidProtocolError, ValidationError } from './errors.ts' +import { validatePort } from './validation.ts' + +export const V = -1 + +export interface ProtocolCodec { + code: number + name: string + size?: number + path?: boolean + resolvable?: boolean + aliases?: string[] + + /** + * Where the multiaddr has been encoded as a string, decode the value if + * necessary, unescaping any escaped values + */ + stringToValue?(value: string): string + + /** + * To encode the multiaddr as a string, escape any necessary values + */ + valueToString?(value: string): string + + /** + * To encode the multiaddr as bytes, convert the value to bytes + */ + valueToBytes?(value: string): Uint8Array + + /** + * To decode bytes to a multiaddr, convert the value bytes to a string + */ + bytesToValue?(bytes: Uint8Array): string + + /** + * Perform any necessary validation on the string value + */ + validate?(value: string): void +} + +class Registry { + private protocolsByCode = new Map() + private protocolsByName = new Map() + + getCodec (key: string | number): ProtocolCodec { + let codec: ProtocolCodec | undefined + + if (typeof key === 'string') { + codec = this.protocolsByName.get(key) + } else { + codec = this.protocolsByCode.get(key) + } + + if (codec == null) { + throw new InvalidProtocolError(`Protocol ${key} was unknown`) + } + + return codec + } + + addCodec (key: number, codec: ProtocolCodec, aliases?: string[]): void { + this.protocolsByCode.set(key, codec) + this.protocolsByName.set(codec.name, codec) + + aliases?.forEach(alias => { + this.protocolsByName.set(alias, codec) + }) + } + + deleteCodec (key: number): void { + const codec = this.getCodec(key) + + if (codec == null) { + return + } + + this.protocolsByCode.delete(codec.code) + this.protocolsByName.delete(codec.name) + + codec.aliases?.forEach(alias => { + this.protocolsByName.delete(alias) + }) + } +} + +export const registry = new Registry() + +const codecs: ProtocolCodec[] = [{ + code: CODE_IP4, + name: 'ip4', + size: 32, + valueToBytes: ip4ToBytes, + bytesToValue: ip4ToString, + validate: (value) => { + if (!isIPv4(value)) { + throw new ValidationError('Invalid ip address') + } + } +}, { + code: CODE_TCP, + name: 'tcp', + size: 16, + valueToBytes: port2bytes, + bytesToValue: bytes2port, + validate: validatePort +}, { + code: CODE_UDP, + name: 'udp', + size: 16, + valueToBytes: port2bytes, + bytesToValue: bytes2port, + validate: validatePort +}, { + code: CODE_DCCP, + name: 'dccp', + size: 16, + valueToBytes: port2bytes, + bytesToValue: bytes2port, + validate: validatePort +}, { + code: CODE_IP6, + name: 'ip6', + size: 128, + valueToBytes: ip6ToBytes, + bytesToValue: ip6ToString, + stringToValue: ip6StringToValue, + validate: (value) => { + if (!isIPv6(value)) { + throw new ValidationError('Invalid ip address') + } + } +}, { + code: CODE_IP6ZONE, + name: 'ip6zone', + size: V +}, { + code: CODE_IPCIDR, + name: 'ipcidr', + size: 8, + bytesToValue: bytesToString('base10'), + valueToBytes: stringToBytes('base10') +}, { + code: CODE_DNS, + name: 'dns', + size: V, + resolvable: true +}, { + code: CODE_DNS4, + name: 'dns4', + size: V, + resolvable: true +}, { + code: CODE_DNS6, + name: 'dns6', + size: V, + resolvable: true +}, { + code: CODE_DNSADDR, + name: 'dnsaddr', + size: V, + resolvable: true +}, { + code: CODE_SCTP, + name: 'sctp', + size: 16, + valueToBytes: port2bytes, + bytesToValue: bytes2port, + validate: validatePort +}, { + code: CODE_UDT, + name: 'udt' +}, { + code: CODE_UTP, + name: 'utp' +}, { + code: CODE_UNIX, + name: 'unix', + size: V, + path: true, + stringToValue: (str) => `/${decodeURIComponent(str)}`, + valueToString: (val) => encodeURIComponent(val.substring(1)) +}, { + code: CODE_P2P, + name: 'p2p', + aliases: ['ipfs'], + size: V, + bytesToValue: bytesToString('base58btc'), + valueToBytes: (val) => { + if (val.startsWith('Q') || val.startsWith('1')) { + return stringToBytes('base58btc')(val) + } + + return CID.parse(val).multihash.bytes + } +}, { + code: CODE_ONION, + name: 'onion', + size: 96, + bytesToValue: bytes2onion, + valueToBytes: onion2bytes +}, { + code: CODE_ONION3, + name: 'onion3', + size: 296, + bytesToValue: bytes2onion, + valueToBytes: onion32bytes +}, { + code: CODE_GARLIC64, + name: 'garlic64', + size: V +}, { + code: CODE_GARLIC32, + name: 'garlic32', + size: V +}, { + code: CODE_TLS, + name: 'tls' +}, { + code: CODE_SNI, + name: 'sni', + size: V +}, { + code: CODE_NOISE, + name: 'noise' +}, { + code: CODE_QUIC, + name: 'quic' +}, { + code: CODE_QUIC_V1, + name: 'quic-v1' +}, { + code: CODE_WEBTRANSPORT, + name: 'webtransport' +}, { + code: CODE_CERTHASH, + name: 'certhash', + size: V, + bytesToValue: bytes2mb(base64url), + valueToBytes: mb2bytes +}, { + code: CODE_HTTP, + name: 'http' +}, { + code: CODE_HTTP_PATH, + name: 'http-path', + size: V, + stringToValue: (str) => `/${decodeURIComponent(str)}`, + valueToString: (val) => encodeURIComponent(val.substring(1)) +}, { + code: CODE_HTTPS, + name: 'https' +}, { + code: CODE_WS, + name: 'ws' +}, { + code: CODE_WSS, + name: 'wss' +}, { + code: CODE_P2P_WEBSOCKET_STAR, + name: 'p2p-websocket-star' +}, { + code: CODE_P2P_STARDUST, + name: 'p2p-stardust' +}, { + code: CODE_P2P_WEBRTC_STAR, + name: 'p2p-webrtc-star' +}, { + code: CODE_P2P_WEBRTC_DIRECT, + name: 'p2p-webrtc-direct' +}, { + code: CODE_WEBRTC_DIRECT, + name: 'webrtc-direct' +}, { + code: CODE_WEBRTC, + name: 'webrtc' +}, { + code: CODE_P2P_CIRCUIT, + name: 'p2p-circuit' +}, { + code: CODE_MEMORY, + name: 'memory', + size: V +}] + +codecs.forEach(codec => { + registry.addCodec(codec.code, codec, codec.aliases) +}) diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 00000000..74f6589a --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,37 @@ +import { ValidationError } from './errors.ts' + +export function integer (value: string): void { + const int = parseInt(value) + + if (int.toString() !== value) { + throw new ValidationError('Value must be an integer') + } +} + +export function positive (value: any): void { + if (value < 0) { + throw new ValidationError('Value must be a positive integer') + } +} + +export function maxValue (max: number): (value: any) => void { + return (value) => { + if (value > max) { + throw new ValidationError(`Value must be smaller than ${max}`) + } + } +} + +export function validate (...funcs: Array<(value: string) => void>): (value: string) => void { + return (value) => { + for (const fn of funcs) { + fn(value) + } + } +} + +export const validatePort = validate( + integer, + positive, + maxValue(65_535) +) diff --git a/test/codec.spec.ts b/test/codec.spec.ts index 3cf73357..fb0908b9 100644 --- a/test/codec.spec.ts +++ b/test/codec.spec.ts @@ -1,44 +1,84 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import * as codec from '../src/codec.js' -import { convertToBytes } from '../src/convert.js' -import type { StringTuple, Tuple } from '../src/index.js' +import { componentsToString, stringToComponents } from '../src/components.ts' +import { CODE_HTTP, CODE_HTTP_PATH, CODE_IP4, CODE_UNIX, CODE_UTP } from '../src/constants.ts' +import type { Component } from '../src/index.js' + +interface TestCase { + name: string + input: string + components: Component[] +} describe('codec', () => { - describe('.stringToMultiaddrParts', () => { + describe('.stringToComponents', () => { it('throws on invalid addresses', () => { expect( - () => codec.stringToMultiaddrParts('/ip4/0.0.0.0/ip4') - ).to.throw( - /invalid address/ - ) + () => stringToComponents('/ip4/0.0.0.0/ip4') + ).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) - }) - describe('.stringToMultiaddrParts', () => { - const testCases: Array<{ name: string, string: string, stringTuples: StringTuple[], tuples: Tuple[], path: string | null }> = [ - { name: 'handles non array tuples', string: '/ip4/0.0.0.0/utp', stringTuples: [[4, '0.0.0.0'], [302]], tuples: [[4, Uint8Array.from([0, 0, 0, 0])], [302]], path: null }, - { name: 'handle not null path', string: '/unix/tmp/p2p.sock', stringTuples: [[400, '/tmp/p2p.sock']], tuples: [[400, convertToBytes(400, '/tmp/p2p.sock')]], path: '/tmp/p2p.sock' } - ] + const testCases: TestCase[] = [{ + name: 'handles non array tuples', + input: '/ip4/0.0.0.0/utp', + components: [{ + code: CODE_IP4, + name: 'ip4', + value: '0.0.0.0' + }, { + code: CODE_UTP, + name: 'utp' + }] + }, { + name: 'handle not null path', + input: '/unix/tmp%2Fp2p.sock', + components: [{ + code: CODE_UNIX, + name: 'unix', + value: '/tmp/p2p.sock' + }] + }, { + name: 'handle http path', + input: '/ip4/123.123.123.123/http/http-path/foo%2Findex.html', + components: [{ + code: CODE_IP4, + name: 'ip4', + value: '123.123.123.123' + }, { + code: CODE_HTTP, + name: 'http' + }, { + code: CODE_HTTP_PATH, + name: 'http-path', + value: '/foo/index.html' + }] + }] - for (const { name, string, stringTuples, tuples, path } of testCases) { + for (const { name, input, components } of testCases) { it(name, () => { - const parts = codec.stringToMultiaddrParts(string) - expect(parts.stringTuples).to.eql(stringTuples) - expect(parts.tuples).to.eql(tuples) - expect(parts.path).to.eql(path) + expect(stringToComponents(input)).to.deep.equal(components) }) } + + it('throws on invalid addresses', () => { + expect( + () => stringToComponents('/ip4/0.0.0.0/ip4') + ).to.throw() + .with.property('name', 'InvalidMultiaddrError') + }) }) - describe('.bytesToTuples', () => { + describe('.componentsToString', () => { it('throws on invalid address', () => { expect( - () => codec.bytesToTuples(codec.tuplesToBytes([[4, uint8ArrayFromString('192')]])) - ).to.throw( - /Invalid address/ - ) + () => componentsToString([{ + code: 123, + name: 'non-existant', + value: 'non-existant' + }]) + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) }) }) diff --git a/test/convert.spec.ts b/test/convert.spec.ts index 4914b107..5240f852 100644 --- a/test/convert.spec.ts +++ b/test/convert.spec.ts @@ -1,73 +1,68 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' +import { base64url } from 'multiformats/bases/base64' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import * as convert from '../src/convert.js' +import { bytes2mb, bytes2port, convertToIpNet, ip4ToBytes, ip4ToString, ip6ToBytes, ip6ToString, mb2bytes, port2bytes } from '../src/convert.ts' import { multiaddr } from '../src/index.js' describe('convert', () => { it('handles ip4 buffers', () => { - expect( - convert.convertToString('ip4', uint8ArrayFromString('c0a80001', 'base16')) - ).to.eql( - '192.168.0.1' - ) + expect(ip4ToString(uint8ArrayFromString('c0a80001', 'base16'))).to.equal('192.168.0.1') }) it('handles ip6 buffers', () => { - expect( - convert.convertToString('ip6', uint8ArrayFromString('abcd0000000100020003000400050006', 'base16')) - ).to.eql( - 'abcd:0:1:2:3:4:5:6' - ) + expect(ip6ToString(uint8ArrayFromString('abcd0000000100020003000400050006', 'base16'))).to.equal('abcd::1:2:3:4:5:6') }) it('handles ipv6 strings', () => { - expect( - convert.convertToBytes('ip6', 'ABCD::1:2:3:4:5:6') - ).to.eql( - uint8ArrayFromString('ABCD0000000100020003000400050006', 'base16upper') - ) + expect(ip6ToBytes('ABCD::1:2:3:4:5:6')).to.equalBytes(uint8ArrayFromString('ABCD0000000100020003000400050006', 'base16upper')) }) it('handles ip4 strings', () => { - expect( - convert.convertToBytes('ip4', '192.168.0.1') - ).to.eql( - uint8ArrayFromString('c0a80001', 'base16') - ) + expect(ip4ToBytes('192.168.0.1')).to.equalBytes(uint8ArrayFromString('c0a80001', 'base16')) }) it('throws on invalid ip4 conversion', () => { - expect( - () => convert.convertToBytes('ip4', '555.168.0.1') - ).to.throw( - /invalid ip address/ - ) + expect(() => ip4ToBytes('555.168.0.1')).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) it('throws on invalid ip6 conversion', () => { - expect( - () => convert.convertToBytes('ip6', 'FFFF::GGGG') - ).to.throw( - /invalid ip address/ - ) + expect(() => ip6ToBytes('FFFF::GGGG')).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) + it('round trips loopback addresses', () => { + const address = '127.0.0.1' + const bytes = ip4ToBytes(address) + + expect(ip4ToString(bytes)).to.equal(address) + }) + + it('round trips class C addresses', () => { + const address = '192.168.1.1' + const bytes = ip4ToBytes(address) + + expect(ip4ToString(bytes)).to.equal(address) + }) + + /* describe('.toBytes', () => { it('defaults to hex conversion', () => { expect( - convert.convertToBytes('ws', 'c0a80001') + convertToBytes(CODE_WS, 'c0a80001') ).to.eql( Uint8Array.from([192, 168, 0, 1]) ) }) }) - +*/ describe('.toString', () => { + /* it('throws on inconsistent ipfs links', () => { const valid = uint8ArrayFromString('03221220d52ebb89d85b02a284948203a62ff28389c57c9f42beec4ec20db76a68911c0b', 'base16') expect( - () => convert.convertToString('ipfs', valid.slice(0, valid.length - 8)) + () => bytesToString(valid.slice(0, valid.length - 8)) ).to.throw( /inconsistent length/ ) @@ -75,17 +70,17 @@ describe('convert', () => { it('defaults to hex conversion', () => { expect( - convert.convertToString('ws', Uint8Array.from([192, 168, 0, 1])) + convertToString('ws', Uint8Array.from([192, 168, 0, 1])) ).to.eql( 'c0a80001' ) }) - +*/ it('respects byteoffset during conversion', () => { - const bytes = convert.convertToBytes('sctp', '1234') + const bytes = port2bytes('1234') const buffer = new Uint8Array(bytes.byteLength + 5) buffer.set(bytes, 5) - expect(convert.convertToString('sctp', buffer.subarray(5))).to.equal('1234') + expect(bytes2port(buffer.subarray(5))).to.equal('1234') }) }) @@ -95,28 +90,28 @@ describe('convert', () => { value: 'f4:32:a0:45:34:62:85:e0:d8:d7:75:36:84:72:8e:b2:aa:9e:71:64:e4:eb:fe:06:51:64:42:64:fe:04:a8:d0' } const mb = 'f' + myCertFingerprint.value.replaceAll(':', '') - const bytes = convert.convertToBytes('certhash', mb) - const outcome = convert.convertToString(466, bytes) + const bytes = mb2bytes(mb) + const outcome = bytes2mb(base64url)(bytes) expect(outcome).to.equal('u9DKgRTRiheDY13U2hHKOsqqecWTk6_4GUWRCZP4EqNA') - const bytesOut = convert.convertToBytes(466, outcome) - expect(bytesOut.toString()).to.equal(bytes.toString()) + const bytesOut = mb2bytes(outcome) + expect(bytesOut).to.equalBytes(bytes) }) it('convertToIpNet ip4', function () { - const ipnet = convert.convertToIpNet(multiaddr('/ip4/192.0.2.0/ipcidr/24')) + const ipnet = convertToIpNet(multiaddr('/ip4/192.0.2.0/ipcidr/24')) expect(ipnet.toString()).equal('192.0.2.0/24') }) it('convertToIpNet ip6', function () { - const ipnet = convert.convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/ipcidr/64')) + const ipnet = convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/ipcidr/64')) expect(ipnet.toString()).equal('2001:0db8:85a3:0000:0000:0000:0000:0000/64') }) it('convertToIpNet not ipcidr', function () { - expect(() => convert.convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/tcp/64'))).to.throw() + expect(() => convertToIpNet(multiaddr('/ip6/2001:0db8:85a3:0000:0000:8a2e:0370:7334/tcp/64'))).to.throw() }) it('convertToIpNet not ipv6', function () { - expect(() => convert.convertToIpNet(multiaddr('/dns6/foo.com/ipcidr/64'))).to.throw() + expect(() => convertToIpNet(multiaddr('/dns6/foo.com/ipcidr/64'))).to.throw() }) }) diff --git a/test/index.spec.ts b/test/index.spec.ts index 523fc0d5..2527b16d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -6,6 +6,19 @@ import { multiaddr, isMultiaddr, fromNodeAddress, isName, fromTuples, fromString import { codes } from '../src/protocols-table.js' import type { Multiaddr } from '../src/index.js' +function roundTrip (str: string): void { + const output = str.replace('/ipfs/', '/p2p/') + + const ma = multiaddr(str) + expect(ma.toString()).to.equal(output) + + const bytes = multiaddr(ma.bytes) + expect(bytes.toString()).to.equal(output) + + const components = multiaddr(ma.getComponents()) + expect(components.toString()).to.equal(output) +} + describe('construction', () => { let udpAddr: Multiaddr @@ -43,32 +56,45 @@ describe('construction', () => { expect(multiaddr('').toString()).to.equal('/') }) + it('should handle repeated /', () => { + expect(multiaddr('////////').toString()).to.equal('/') + }) + it('null/undefined construct still works', () => { expect(multiaddr().toString()).to.equal('/') expect(multiaddr(null).toString()).to.equal('/') expect(multiaddr(undefined).toString()).to.equal('/') }) + it('should not mutate multiaddr', () => { + const str = '/ip4/127.0.0.1/udp/1234' + udpAddr = multiaddr(str) + udpAddr.getComponents().pop() + expect(udpAddr.toString()).to.equal(str) + }) + it('throws on truthy non string or buffer', () => { - const errRegex = /addr must be a string/ // @ts-expect-error incorrect parameters - expect(() => multiaddr({})).to.throw(errRegex) + expect(() => multiaddr({})).to.throw() + .with.property('name', 'InvalidMultiaddrError') // @ts-expect-error incorrect parameters - expect(() => multiaddr([])).to.throw(errRegex) + expect(() => multiaddr(138)).to.throw() + .with.property('name', 'InvalidMultiaddrError') // @ts-expect-error incorrect parameters - expect(() => multiaddr(138)).to.throw(errRegex) - // @ts-expect-error incorrect parameters - expect(() => multiaddr(true)).to.throw(errRegex) + expect(() => multiaddr(true)).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) it('throws on falsy non string or buffer', () => { - const errRegex = /addr must be a string/ // @ts-expect-error incorrect parameters - expect(() => multiaddr(NaN)).to.throw(errRegex) + expect(() => multiaddr(NaN)).to.throw() + .with.property('name', 'InvalidMultiaddrError') // @ts-expect-error incorrect parameters - expect(() => multiaddr(false)).to.throw(errRegex) + expect(() => multiaddr(false)).to.throw() + .with.property('name', 'InvalidMultiaddrError') // @ts-expect-error incorrect parameters - expect(() => multiaddr(0)).to.throw(errRegex) + expect(() => multiaddr(0)).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) }) @@ -112,7 +138,7 @@ describe('manipulation', () => { const udpAddr = multiaddr(udpAddrStr) expect(udpAddr.toString()).to.equal(udpAddrStr) - expect(udpAddr.bytes).to.deep.equal(udpAddrBuf) + expect(udpAddr.bytes).to.equalBytes(udpAddrBuf) expect(udpAddr.protoCodes()).to.deep.equal([4, 273]) expect(udpAddr.protoNames()).to.deep.equal(['ip4', 'udp']) @@ -179,375 +205,126 @@ describe('manipulation', () => { }) describe('variants', () => { - it('ip4', () => { - const str = '/ip4/127.0.0.1' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + tcp', () => { - const str = '/ip4/127.0.0.1/tcp/5000' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/5000' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + udp', () => { - const str = '/ip4/127.0.0.1/udp/5000' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + udp', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + p2p', () => { - const str = '/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + ipfs', () => { - const str = '/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('ip6 + p2p', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + ip6zone', () => { - const str = '/ip6zone/x/ip6/fe80::1' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it.skip('ip4 + dccp', () => {}) - it.skip('ip6 + dccp', () => {}) - - it.skip('ip4 + sctp', () => {}) - it.skip('ip6 + sctp', () => {}) - - it('ip4 + udp + utp', () => { - const str = '/ip4/127.0.0.1/udp/5000/utp' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + udp + utp', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/utp' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.protoNames()) - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + tcp + http', () => { - const str = '/ip4/127.0.0.1/tcp/8000/http' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + tcp + unix', () => { - const str = '/ip4/127.0.0.1/tcp/80/unix/a/b/c/d/e/f' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp + http', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/http' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp + unix', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/unix/a/b/c/d/e/f' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + tcp + https', () => { - const str = '/ip4/127.0.0.1/tcp/8000/https' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp + https', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/https' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 + tcp + websockets', () => { - const str = '/ip4/127.0.0.1/tcp/8000/ws' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp + websockets', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + tcp + websockets + ipfs', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('ip6 + tcp + websockets + p2p', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + udp + quic + ipfs', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('ip6 + udp + quic + p2p', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + udp + quic-v1 + ipfs', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('ip6 + udp + quic-v1 + p2p', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 webtransport', () => { - const str = '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/webtransport' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip4 webtransport', () => { - const str = '/ip4/1.2.3.4/udp/4001/quic-v1/webtransport' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('webtransport with certhash', () => { - const str = '/ip4/1.2.3.4/udp/4001/quic-v1/webtransport/certhash/uEiAkH5a4DPGKUuOBjYw0CgwjvcJCJMD2K_1aluKR_tpevQ/certhash/uEiAfbgiymPP2_nX7Dgir8B4QkksjHp2lVuJZz0F79Be9JA/p2p/12D3KooWBdmLJjhpgJ9KZgLM3f894ff9xyBfPvPjFNn7MKJpyrC2' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('ip6 + ip6zone + udp + quic', () => { - const str = '/ip6zone/x/ip6/fe80::1/udp/1234/quic' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('unix', () => { - const str = '/unix/a/b/c/d/e' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p', () => { - const str = '/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p', () => { - const str = '/p2p/bafzbeidt255unskpefjmqb2rc27vjuyxopkxgaylxij6pw35hhys4vnyp4' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal('/p2p/QmW8rAgaaA6sRydK1k6vonShQME47aDxaFidbtMevWs73t') - }) - - it('ipfs', () => { - const str = '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('tls', () => { - const str = '/ip4/127.0.0.1/tcp/9090/tls/ws' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('sni', () => { - const str = '/ip4/127.0.0.1/tcp/9090/tls/sni/example.com/ws' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('http-path', () => { - const str = '/ip4/127.0.0.1/tcp/9090/tls/http-path/tmp%2Ffoo%2F..%2Fbar' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - const parts = addr.tuples() - const lastPart = parts[parts.length - 1] - const httpPath = new TextDecoder().decode(lastPart[1]?.subarray(1)) // skip the first byte since it's the length prefix - expect(httpPath).to.equal('tmp/foo/../bar') - expect(addr.toString()).to.equal(str) - }) - - it('onion', () => { - const str = '/onion/timaq4ygg2iegci7:1234' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('onion bad length', () => { + const variants = { + ip4: '/ip4/127.0.0.1', + ip6: '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095', + 'ip4 + tcp': '/ip4/127.0.0.1/tcp/5000', + 'ip6 + tcp': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/5000', + 'ip4 + udp': '/ip4/127.0.0.1/udp/5000', + 'ip6 + udp': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000', + 'ip4 + p2p': '/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234', + 'ip4 + ipfs': '/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234', + 'ip6 + p2p': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234', + 'ip6zone + ip6': '/ip6zone/x/ip6/fe80::1', + 'ip4 + udp + utp': '/ip4/127.0.0.1/udp/5000/utp', + 'ip6 + udp + utp': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/utp', + 'ip4 + tcp + http': '/ip4/127.0.0.1/tcp/8000/http', + 'ip4 + tcp + unix': '/ip4/127.0.0.1/tcp/80/unix/a%2Fb%2Fc%2Fd%2Fe%2Ff', + 'ip6 + tcp + http': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/http', + 'ip6 + tcp + unix': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/unix/a%2Fb%2Fc%2Fd%2Fe%2Ff', + 'ip4 + tcp + https': '/ip4/127.0.0.1/tcp/8000/https', + 'ip6 + tcp + https': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/https', + 'ip4 + tcp + websockets': '/ip4/127.0.0.1/tcp/8000/ws', + 'ip6 + tcp + websockets': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws', + 'ip6 + tcp + websockets + ipfs': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 + tcp + websockets + p2p': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 + udp + quic + ipfs': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 + udp + quic + p2p': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 + udp + quic-v1 + ipfs': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 + udp + quic-v1 + p2p': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'ip6 webtransport': '/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/4001/quic-v1/webtransport', + 'ip4 webtransport': '/ip4/1.2.3.4/udp/4001/quic-v1/webtransport', + 'webtransport with certhash': '/ip4/1.2.3.4/udp/4001/quic-v1/webtransport/certhash/uEiAkH5a4DPGKUuOBjYw0CgwjvcJCJMD2K_1aluKR_tpevQ/certhash/uEiAfbgiymPP2_nX7Dgir8B4QkksjHp2lVuJZz0F79Be9JA/p2p/12D3KooWBdmLJjhpgJ9KZgLM3f894ff9xyBfPvPjFNn7MKJpyrC2', + 'ip6zone + ip6 + udp + quic': '/ip6zone/x/ip6/fe80::1/udp/1234/quic', + unix: '/unix/a%2Fb%2Fc%2Fd%2Fe', + 'p2p (base58btc)': '/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + ipfs: '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + tls: '/ip4/127.0.0.1/tcp/9090/tls/ws', + sni: '/ip4/127.0.0.1/tcp/9090/tls/sni/example.com/ws', + 'http-path': '/ip4/127.0.0.1/tcp/9090/tls/http-path/tmp%2Ffoo%2F..%2Fbar', + onion: '/onion/timaq4ygg2iegci7:1234', + onion3: '/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234', + 'p2p-circuit': '/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'p2p p2p-circuit': '/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit', + 'ipfs p2p-circuit': '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit', + 'p2p-webrtc-star': '/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'p2p-webrtc-star ipfs': '/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC', + 'p2p-webrtc-direct': '/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct', + 'p2p-websocket-star': '/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star', + 'memory p2p': '/memory/test/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' + } + + for (const [name, value] of Object.entries(variants)) { + it(`should round trip ${name}`, () => { + roundTrip(value) + }) + } + + it.skip('onion bad length', () => { const str = '/onion/timaq4ygg2iegci:80' expect(() => multiaddr(str)).to.throw() }) - it('onion bad port', () => { + it.skip('onion bad port', () => { const str = '/onion/timaq4ygg2iegci7:-1' expect(() => multiaddr(str)).to.throw() }) - it('onion no port', () => { + it.skip('onion no port', () => { const str = '/onion/timaq4ygg2iegci7' expect(() => multiaddr(str)).to.throw() }) - it('onion3', () => { - const str = '/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('onion3 bad length', () => { + it.skip('onion3 bad length', () => { const str = '/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopyyd:1234' expect(() => multiaddr(str)).to.throw() }) - it('onion3 bad port', () => { + it.skip('onion3 bad port', () => { const str = '/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:-1' expect(() => multiaddr(str)).to.throw() }) - it('onion3 no port', () => { + it.skip('onion3 no port', () => { const str = '/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd' expect(() => multiaddr(str)).to.throw() }) +}) - it('p2p-circuit', () => { - const str = '/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p-circuit p2p', () => { - const str = '/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p-circuit ipfs', () => { - const str = '/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('p2p-webrtc-star', () => { - const str = '/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p-webrtc-star ipfs', () => { - const str = '/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str.replace('/ipfs/', '/p2p/')) - }) - - it('p2p-webrtc-direct', () => { - const str = '/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) - - it('p2p-websocket-star', () => { - const str = '/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) +describe('normalize', () => { + const variants = { + 'ip6 with zeroes': { + input: '/ip6/0001:08a0:00c5:4201:3ac9:86ff:fe31:0709', + output: '/ip6/1:8a0:c5:4201:3ac9:86ff:fe31:709' + }, + 'ip6 loopback': { + input: '/ip6/0000:0000:0000:0000:0000:0000:0000:0001', + output: '/ip6/::1' + }, + 'ip6 empty': { + input: '/ip6/:0::00::000::001', + output: '/ip6/::1' + }, + 'ip6 with ip4': { + input: '/ip6/::101.45.75.219/tcp/9090', + output: '/ip6/::652d:4bdb/tcp/9090' + }, + 'ip6 with ip4 without padding': { + input: '/ip6/::1.45.5.219/tcp/9090', + output: '/ip6/::12d:5db/tcp/9090' + }, + 'ipfs to p2p': { + input: '/ipfs/bafzbeidt255unskpefjmqb2rc27vjuyxopkxgaylxij6pw35hhys4vnyp4', + output: '/p2p/bafzbeidt255unskpefjmqb2rc27vjuyxopkxgaylxij6pw35hhys4vnyp4' + } + } - it('memory + p2p', () => { - const str = '/memory/test/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC' - const addr = multiaddr(str) - expect(addr).to.have.property('bytes') - expect(addr.toString()).to.equal(str) - }) + for (const [name, value] of Object.entries(variants)) { + it(`should normalize ${name}`, () => { + const ma = multiaddr(value.input) + expect(ma.toString()).to.equal(value.output) + }) + } }) describe('helpers', () => { @@ -682,7 +459,7 @@ describe('helpers', () => { it('works with unix', () => { expect( - multiaddr('/ip4/0.0.0.0/tcp/8000/unix/tmp/p2p.sock').protos() + multiaddr('/ip4/0.0.0.0/tcp/8000/unix/tmp%2Fp2p.sock').protos() ).to.be.eql([{ code: 4, name: 'ip4', @@ -775,13 +552,13 @@ describe('helpers', () => { const relay = relayTCP.encapsulate('/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit') const target = multiaddr('/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') const original = relay.encapsulate(target) - expect(original.decapsulateCode(421).toJSON()).to.eql(relay.toJSON()) - expect(relay.decapsulateCode(421).toJSON()).to.eql(relayTCP.toJSON()) + expect(original.decapsulateCode(421).toJSON()).to.equal(relay.toJSON()) + expect(relay.decapsulateCode(421).toJSON()).to.equal(relayTCP.toJSON()) }) it('ignores missing codes', () => { const tcp = multiaddr('/ip4/0.0.0.0/tcp/8080') - expect(tcp.decapsulateCode(421).toJSON()).to.eql(tcp.toJSON()) + expect(tcp.decapsulateCode(421).toJSON()).to.equal(tcp.toJSON()) }) }) @@ -883,25 +660,22 @@ describe('helpers', () => { it('throws on an invalid format address when the addr is not prefixed with a /', () => { expect( () => multiaddr('ip4/192.168.0.1/udp').nodeAddress() - ).to.throw( - /must start with a/ - ) + ).to.throw() + .with.property('name', 'InvalidMultiaddrError') }) it('throws on an invalid protocol name when the addr has an invalid one', () => { expect( () => multiaddr('/ip5/127.0.0.1/udp/5000') - ).to.throw( - /no protocol with name/ - ) + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) it('throws on an invalid protocol name when the transport protocol is not valid', () => { expect( () => multiaddr('/ip4/127.0.0.1/utp/5000') - ).to.throw( - /no protocol with name/ - ) + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) }) @@ -910,18 +684,16 @@ describe('helpers', () => { expect( // @ts-expect-error incorrect parameters () => fromNodeAddress() - ).to.throw( - /requires node address/ - ) + ).to.throw() + .with.property('name', 'InvalidParametersError') }) it('throws on missing transport', () => { expect( // @ts-expect-error incorrect parameters () => fromNodeAddress({ address: '0.0.0.0' }) - ).to.throw( - /requires transport protocol/ - ) + ).to.throw() + .with.property('name', 'InvalidParametersError') }) it('parses a node address', () => { @@ -931,9 +703,7 @@ describe('helpers', () => { family: 4, port: 1234 }, 'tcp').toString() - ).to.be.eql( - '/ip4/192.168.0.1/tcp/1234' - ) + ).to.equal('/ip4/192.168.0.1/tcp/1234') }) it('parses a node address with an ip6zone', () => { @@ -943,9 +713,7 @@ describe('helpers', () => { family: 6, port: 1234 }, 'tcp').toString() - ).to.be.eql( - '/ip6zone/x/ip6/fe80::1/tcp/1234' - ) + ).to.equal('/ip6zone/x/ip6/fe80::1/tcp/1234') }) }) @@ -991,27 +759,21 @@ describe('helpers', () => { it('returns false for two protocols not using {IPv4, IPv6}/{TCP, UDP}', () => { expect( multiaddr('/ip4/192.168.0.1/utp').isThinWaistAddress() - ).to.equal(false) + ).to.be.false() expect( - multiaddr('/sctp/192.168.0.1/tcp/1234').isThinWaistAddress() - ).to.eql( - false - ) + multiaddr('/ip4/192.168.0.1/sctp/1234').isThinWaistAddress() + ).to.be.false() expect( multiaddr('/http/utp').isThinWaistAddress() - ).to.eql( - false - ) + ).to.be.false() }) it('returns false for more than two protocols', () => { expect( multiaddr('/ip4/0.0.0.0/tcp/1234/utp').isThinWaistAddress() - ).to.equal( - false - ) + ).to.be.false() }) }) @@ -1026,11 +788,6 @@ describe('helpers', () => { multiaddr('/ip4/0.0.0.0/tcp/8080/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC').getPeerId() ).to.equal('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') }) - it('extracts the peer Id from a multiaddr, ipfs', () => { - expect( - multiaddr('/p2p-circuit/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC').getPeerId() - ).to.equal('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC') - }) it('extracts the peer Id from a multiaddr, p2p and CIDv1 Base32', () => { expect( multiaddr('/p2p-circuit/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4').getPeerId() @@ -1064,13 +821,13 @@ describe('helpers', () => { describe('.getPath', () => { it('should return a path for unix', () => { expect( - multiaddr('/unix/tmp/p2p.sock').getPath() + multiaddr('/unix/tmp%2Fp2p.sock').getPath() ).to.eql('/tmp/p2p.sock') }) it('should return a path for unix when other protos exist', () => { expect( - multiaddr('/ip4/0.0.0.0/tcp/1234/unix/tmp/p2p.sock').getPath() + multiaddr('/ip4/0.0.0.0/tcp/1234/unix/tmp%2Fp2p.sock').getPath() ).to.eql('/tmp/p2p.sock') }) @@ -1130,6 +887,7 @@ describe('helpers', () => { describe('unknown protocols', () => { it('throws an error', () => { const str = '/ip4/127.0.0.1/unknown' - expect(() => multiaddr(str)).to.throw('no protocol with name: unknown') + expect(() => multiaddr(str)).to.throw() + .with.property('name', 'InvalidProtocolError') }) }) diff --git a/test/ip.spec.ts b/test/ip.spec.ts deleted file mode 100644 index d3843eb2..00000000 --- a/test/ip.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint max-nested-callbacks: ["error", 8] */ -/* eslint-env mocha */ -import { expect } from 'aegir/chai' -import { toBytes, toString } from '../src/ip.js' - -describe('ip', () => { - describe('toBytes', () => { - it('should handle extra characters', () => { - const address = '127.0.0.1 ' - const bytes = toBytes(address) - - expect(toString(bytes)).to.equal(address.trim()) - }) - - it('should turn loopback into bytes', () => { - const address = '127.0.0.1' - const bytes = toBytes(address) - - expect(toString(bytes)).to.equal(address) - }) - - it('should turn private address into bytes', () => { - const address = '192.168.1.1' - const bytes = toBytes(address) - - expect(toString(bytes)).to.equal(address) - }) - }) -}) diff --git a/test/protocols.spec.ts b/test/protocols.spec.ts index 3453a980..171adf16 100644 --- a/test/protocols.spec.ts +++ b/test/protocols.spec.ts @@ -1,32 +1,29 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' -import { getProtocol } from '../src/protocols-table.js' +import { protocols } from '../src/index.js' describe('protocols', () => { describe('throws on non existent protocol', () => { it('number', () => { expect( - () => getProtocol(1234) - ).to.throw( - /no protocol with code/ - ) + () => protocols(1234) + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) it('string', () => { expect( - () => getProtocol('hello') - ).to.throw( - /no protocol with name/ - ) + () => protocols('hello') + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) it('else', () => { expect( // @ts-expect-error incorrect parameters - () => getProtocol({ hi: 34 }) - ).to.throw( - /invalid protocol id type/ - ) + () => protocols({ hi: 34 }) + ).to.throw() + .with.property('name', 'InvalidProtocolError') }) }) })