Skip to content

Commit caf2bfd

Browse files
authored
feat: allow registering custom protocol codecs (#425)
Exposes the internal registry to allow adding/removing protocol codecs.
1 parent d43de69 commit caf2bfd

File tree

11 files changed

+199
-34
lines changed

11 files changed

+199
-34
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,37 @@ console.info(resolved)
114114
// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
115115
```
116116

117+
## Example - Adding custom protocols
118+
119+
To add application-specific or experimental protocols, add a protocol codec
120+
to the protocol registry:
121+
122+
```ts
123+
import { registry, V, multiaddr } from '@multiformats/multiaddr'
124+
import type { ProtocolCodec } from '@multiformats/multiaddr'
125+
126+
const maWithCustomTuple = '/custom-protocol/hello'
127+
128+
// throws UnknownProtocolError
129+
multiaddr(maWithCustomTuple)
130+
131+
const protocol: ProtocolCodec = {
132+
code: 2059,
133+
name: 'custom-protocol',
134+
size: V
135+
// V means variable length, can also be 0, a positive integer (e.g. a fixed
136+
// length or omitted
137+
}
138+
139+
registry.addProtocol(protocol)
140+
141+
// does not throw UnknownProtocolError
142+
multiaddr(maWithCustomTuple)
143+
144+
// protocols can also be removed
145+
registry.removeProtocol(protocol.code)
146+
```
147+
117148
# Install
118149

119150
```console

src/components.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function bytesToComponents (bytes: Uint8Array): Component[] {
1313
let i = 0
1414
while (i < bytes.length) {
1515
const code = varint.decode(bytes, i)
16-
const codec = registry.getCodec(code)
16+
const codec = registry.getProtocol(code)
1717
const codeLength = varint.encodingLength(code)
1818
const size = sizeForAddr(codec, bytes, i + codeLength)
1919
let sizeLength = 0
@@ -51,7 +51,7 @@ export function componentsToBytes (components: Component[]): Uint8Array {
5151

5252
for (const component of components) {
5353
if (component.bytes == null) {
54-
const codec = registry.getCodec(component.code)
54+
const codec = registry.getProtocol(component.code)
5555
const codecLength = varint.encodingLength(component.code)
5656
let valueBytes: Uint8Array | undefined
5757
let valueLength = 0
@@ -119,7 +119,7 @@ export function stringToComponents (string: string): Component[] {
119119
const ended = i === string.length - 1
120120

121121
if (char === '/' || ended) {
122-
const codec = registry.getCodec(protocol)
122+
const codec = registry.getProtocol(protocol)
123123

124124
if (collecting === 'protocol') {
125125
if (codec.size == null || codec.size === 0) {
@@ -176,7 +176,7 @@ export function componentsToString (components: Component[]): string {
176176
return component.name
177177
}
178178
179-
const codec = registry.getCodec(component.code)
179+
const codec = registry.getProtocol(component.code)
180180
181181
if (codec == null) {
182182
throw new InvalidMultiaddrError(`Unknown protocol code ${component.code}`)

src/convert.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function convert (proto: string, a: string | Uint8Array): Uint8Array | st
4545
* @deprecated Will be removed in a future release
4646
*/
4747
export function convertToString (proto: number | string, buf: Uint8Array): string {
48-
const protocol = registry.getCodec(proto)
48+
const protocol = registry.getProtocol(proto)
4949

5050
return protocol.bytesToValue?.(buf) ?? uint8ArrayToString(buf, 'base16') // no clue. convert to hex
5151
}
@@ -56,7 +56,7 @@ export function convertToString (proto: number | string, buf: Uint8Array): strin
5656
* @deprecated Will be removed in a future release
5757
*/
5858
export function convertToBytes (proto: string | number, str: string): Uint8Array {
59-
const protocol = registry.getCodec(proto)
59+
const protocol = registry.getProtocol(proto)
6060

6161
return protocol.valueToBytes?.(str) ?? uint8ArrayFromString(str, 'base16') // no clue. convert from hex
6262
}

src/errors.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class InvalidParametersError extends Error {
1616
name = 'InvalidParametersError'
1717
}
1818

19-
export class InvalidProtocolError extends Error {
20-
static name = 'InvalidProtocolError'
21-
name = 'InvalidProtocolError'
19+
export class UnknownProtocolError extends Error {
20+
static name = 'UnknownProtocolError'
21+
name = 'UnknownProtocolError'
2222
}

src/index.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,44 @@
9191
* console.info(resolved)
9292
* // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
9393
* ```
94+
*
95+
* @example Adding custom protocols
96+
*
97+
* To add application-specific or experimental protocols, add a protocol codec
98+
* to the protocol registry:
99+
*
100+
* ```ts
101+
* import { registry, V, multiaddr } from '@multiformats/multiaddr'
102+
* import type { ProtocolCodec } from '@multiformats/multiaddr'
103+
*
104+
* const maWithCustomTuple = '/custom-protocol/hello'
105+
*
106+
* // throws UnknownProtocolError
107+
* multiaddr(maWithCustomTuple)
108+
*
109+
* const protocol: ProtocolCodec = {
110+
* code: 2059,
111+
* name: 'custom-protocol',
112+
* size: V
113+
* // V means variable length, can also be 0, a positive integer (e.g. a fixed
114+
* // length or omitted
115+
* }
116+
*
117+
* registry.addProtocol(protocol)
118+
*
119+
* // does not throw UnknownProtocolError
120+
* multiaddr(maWithCustomTuple)
121+
*
122+
* // protocols can also be removed
123+
* registry.removeProtocol(protocol.code)
124+
* ```
94125
*/
95126

96127
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
97128
import { InvalidParametersError } from './errors.ts'
98129
import { Multiaddr as MultiaddrClass, symbol } from './multiaddr.js'
99-
import { registry } from './registry.ts'
130+
import { registry, V } from './registry.ts'
131+
import type { ProtocolCodec } from './registry.ts'
100132
import type { Resolver } from './resolvers/index.js'
101133
import type { DNS } from '@multiformats/dns'
102134
import type { AbortOptions } from 'abort-error'
@@ -124,6 +156,28 @@ export interface MultiaddrObject {
124156
port: number
125157
}
126158

159+
/**
160+
* The protocol registry stores protocol codecs that allow transformation of
161+
* multiaddr tuples from bytes to string and back again, and also validation of
162+
* the address values.
163+
*/
164+
export interface Registry {
165+
/**
166+
* Retrieve a protocol definition by it's code or name
167+
*/
168+
getProtocol (key: string | number): ProtocolCodec
169+
170+
/**
171+
* Add a new protocol definition
172+
*/
173+
addProtocol (codec: ProtocolCodec): void
174+
175+
/**
176+
* Remove a protocol definition by it's code
177+
*/
178+
removeProtocol (code: number): void
179+
}
180+
127181
/**
128182
* A NodeAddress is an IPv4/IPv6 address/TCP port combination
129183
*/
@@ -621,7 +675,7 @@ export function fromNodeAddress (addr: NodeAddress, transport: string): Multiadd
621675
*/
622676
export function fromTuples (tuples: Tuple[]): Multiaddr {
623677
return multiaddr(tuples.map(([code, value]) => {
624-
const codec = registry.getCodec(code)
678+
const codec = registry.getProtocol(code)
625679

626680
const component: Component = {
627681
code,
@@ -655,7 +709,7 @@ export function fromTuples (tuples: Tuple[]): Multiaddr {
655709
*/
656710
export function fromStringTuples (tuples: StringTuple[]): Multiaddr {
657711
return multiaddr(tuples.map(([code, value]) => {
658-
const codec = registry.getCodec(code)
712+
const codec = registry.getProtocol(code)
659713

660714
const component: Component = {
661715
code,
@@ -743,7 +797,7 @@ export function multiaddr (addr?: MultiaddrInput): Multiaddr {
743797
* @deprecated This will be removed in a future version
744798
*/
745799
export function protocols (proto: number | string): Protocol {
746-
const codec = registry.getCodec(proto)
800+
const codec = registry.getProtocol(proto)
747801

748802
return {
749803
code: codec.code,
@@ -759,3 +813,5 @@ export function protocols (proto: number | string): Protocol {
759813
* out by bundlers.
760814
*/
761815
export * from './constants.ts'
816+
export { registry, V }
817+
export type { ProtocolCodec }

src/multiaddr.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class Multiaddr implements MultiaddrInterface {
157157

158158
protos (): Protocol[] {
159159
return this.#components.map(({ code, value }) => {
160-
const codec = registry.getCodec(code)
160+
const codec = registry.getProtocol(code)
161161

162162
return {
163163
code,
@@ -183,7 +183,7 @@ export class Multiaddr implements MultiaddrInterface {
183183
return [code]
184184
}
185185

186-
const codec = registry.getCodec(code)
186+
const codec = registry.getProtocol(code)
187187
const output: Tuple = [code]
188188

189189
if (value != null) {
@@ -283,7 +283,7 @@ export class Multiaddr implements MultiaddrInterface {
283283

284284
getPath (): string | null {
285285
for (const component of this.#components) {
286-
const codec = registry.getCodec(component.code)
286+
const codec = registry.getProtocol(component.code)
287287

288288
if (!codec.path) {
289289
continue
@@ -371,7 +371,7 @@ export class Multiaddr implements MultiaddrInterface {
371371
export function validate (addr: Multiaddr): void {
372372
addr.getComponents()
373373
.forEach(component => {
374-
const codec = registry.getCodec(component.code)
374+
const codec = registry.getProtocol(component.code)
375375

376376
if (component.value == null) {
377377
return

src/registry.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,50 @@ import { isIPv4, isIPv6 } from '@chainsafe/is-ip'
22
import { CID } from 'multiformats'
33
import { base64url } from 'multiformats/bases/base64'
44
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'
5-
import { InvalidProtocolError, ValidationError } from './errors.ts'
5+
import { UnknownProtocolError, ValidationError } from './errors.ts'
66
import { bytes2mb, bytes2onion, bytes2port, bytesToString, ip4ToBytes, ip4ToString, ip6StringToValue, ip6ToBytes, ip6ToString, mb2bytes, onion2bytes, onion32bytes, port2bytes, stringToBytes } from './utils.ts'
77
import { validatePort } from './validation.ts'
8+
import type { Registry as RegistryInterface } from './index.ts'
89

910
export const V = -1
1011

1112
export interface ProtocolCodec {
13+
/**
14+
* A numeric code that will be used in the binary representation of the tuple.
15+
*/
1216
code: number
17+
18+
/**
19+
* A string name that will be used in the string representation of the addr.
20+
*/
1321
name: string
22+
23+
/**
24+
* Size defines the expected length of the address part of the tuple - valid
25+
* values are `-1` (or the `V` constant) for variable length (this will be
26+
* varint encoded in the binary representation), `0` for no address part or a
27+
* number that represents a fixed-length address.
28+
*/
1429
size?: number
30+
31+
/**
32+
* If this protocol is a path protocol.
33+
*
34+
* @deprecated This will be removed in a future release
35+
*/
1536
path?: boolean
37+
38+
/**
39+
* If this protocol can be resolved using configured resolvers.
40+
*
41+
* @deprecated This will be removed in a future release
42+
*/
1643
resolvable?: boolean
44+
45+
/**
46+
* If specified this protocol codec will also be used to decode tuples with
47+
* these names from string multiaddrs.
48+
*/
1749
aliases?: string[]
1850

1951
/**
@@ -43,11 +75,11 @@ export interface ProtocolCodec {
4375
validate?(value: string): void
4476
}
4577

46-
class Registry {
78+
class Registry implements RegistryInterface {
4779
private protocolsByCode = new Map<number, ProtocolCodec>()
4880
private protocolsByName = new Map<string, ProtocolCodec>()
4981

50-
getCodec (key: string | number): ProtocolCodec {
82+
getProtocol (key: string | number): ProtocolCodec {
5183
let codec: ProtocolCodec | undefined
5284

5385
if (typeof key === 'string') {
@@ -57,23 +89,23 @@ class Registry {
5789
}
5890

5991
if (codec == null) {
60-
throw new InvalidProtocolError(`Protocol ${key} was unknown`)
92+
throw new UnknownProtocolError(`Protocol ${key} was unknown`)
6193
}
6294

6395
return codec
6496
}
6597

66-
addCodec (key: number, codec: ProtocolCodec, aliases?: string[]): void {
67-
this.protocolsByCode.set(key, codec)
98+
addProtocol (codec: ProtocolCodec): void {
99+
this.protocolsByCode.set(codec.code, codec)
68100
this.protocolsByName.set(codec.name, codec)
69101

70-
aliases?.forEach(alias => {
102+
codec.aliases?.forEach(alias => {
71103
this.protocolsByName.set(alias, codec)
72104
})
73105
}
74106

75-
deleteCodec (key: number): void {
76-
const codec = this.getCodec(key)
107+
removeProtocol (code: number): void {
108+
const codec = this.protocolsByCode.get(code)
77109

78110
if (codec == null) {
79111
return
@@ -288,5 +320,5 @@ const codecs: ProtocolCodec[] = [{
288320
}]
289321

290322
codecs.forEach(codec => {
291-
registry.addCodec(codec.code, codec, codec.aliases)
323+
registry.addProtocol(codec)
292324
})

test/codec.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('codec', () => {
7878
value: 'non-existant'
7979
}])
8080
).to.throw()
81-
.with.property('name', 'InvalidProtocolError')
81+
.with.property('name', 'UnknownProtocolError')
8282
})
8383
})
8484
})

test/index.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -672,14 +672,14 @@ describe('helpers', () => {
672672
expect(
673673
() => multiaddr('/ip5/127.0.0.1/udp/5000')
674674
).to.throw()
675-
.with.property('name', 'InvalidProtocolError')
675+
.with.property('name', 'UnknownProtocolError')
676676
})
677677

678678
it('throws on an invalid protocol name when the transport protocol is not valid', () => {
679679
expect(
680680
() => multiaddr('/ip4/127.0.0.1/utp/5000')
681681
).to.throw()
682-
.with.property('name', 'InvalidProtocolError')
682+
.with.property('name', 'UnknownProtocolError')
683683
})
684684
})
685685

@@ -892,6 +892,6 @@ describe('unknown protocols', () => {
892892
it('throws an error', () => {
893893
const str = '/ip4/127.0.0.1/unknown'
894894
expect(() => multiaddr(str)).to.throw()
895-
.with.property('name', 'InvalidProtocolError')
895+
.with.property('name', 'UnknownProtocolError')
896896
})
897897
})

0 commit comments

Comments
 (0)