diff --git a/src/codec.ts b/src/codec.ts index 0c870401..fb7ed616 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -5,14 +5,29 @@ import { convertToBytes, convertToString } from './convert.js' import { getProtocol } from './protocols-table.js' import type { StringTuple, Tuple, Protocol } from './index.js' -/** - * string -> [[str name, str addr]... ] - */ -export function stringToStringTuples (str: string): string[][] { - const tuples = [] - const parts = str.split('/').slice(1) // skip first empty elem +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 [] + return { + bytes: new Uint8Array(), + string: '/', + tuples: [], + stringTuples: [], + path: null + } } for (let p = 0; p < parts.length; p++) { @@ -20,7 +35,8 @@ export function stringToStringTuples (str: string): string[][] { const proto = getProtocol(part) if (proto.size === 0) { - tuples.push([part]) + tuples.push([proto.code]) + stringTuples.push([proto.code]) // eslint-disable-next-line no-continue continue } @@ -32,29 +48,88 @@ export function stringToStringTuples (str: string): string[][] { // if it's a path proto, take the rest if (proto.path === true) { - tuples.push([ - part, - // 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 - cleanPath(parts.slice(p).join('/')) - ]) + // 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 } - tuples.push([part, parts[p]]) + const bytes = convertToBytes(proto.code, parts[p]) + tuples.push([proto.code, bytes]) + stringTuples.push([proto.code, convertToString(proto.code, bytes)]) } - return tuples + 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.decode.bytes ?? 0 + + const p = getProtocol(code) + + const size = sizeForAddr(p, bytes.slice(i + n)) + + if (size === 0) { + tuples.push([code]) + stringTuples.push([code]) + i += n + // eslint-disable-next-line no-continue + 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 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 + } } /** * [[str name, str addr]... ] -> string */ -export function stringTuplesToString (tuples: StringTuple[]): string { +function stringTuplesToString (tuples: StringTuple[]): string { const parts: string[] = [] tuples.map((tup) => { - const proto = protoFromTuple(tup) + const proto = getProtocol(tup[0]) parts.push(proto.name) if (tup.length > 1 && tup[1] != null) { parts.push(tup[1]) @@ -65,43 +140,12 @@ export function stringTuplesToString (tuples: StringTuple[]): string { return cleanPath(parts.join('/')) } -/** - * [[str name, str addr]... ] -> [[int code, Uint8Array]... ] - */ -export function stringTuplesToTuples (tuples: Array): Tuple[] { - return tuples.map((tup) => { - if (!Array.isArray(tup)) { - tup = [tup] - } - const proto = protoFromTuple(tup) - if (tup.length > 1) { - return [proto.code, convertToBytes(proto.code, tup[1])] - } - return [proto.code] - }) -} - -/** - * Convert tuples to string tuples - * - * [[int code, Uint8Array]... ] -> [[int code, str addr]... ] - */ -export function tuplesToStringTuples (tuples: Tuple[]): StringTuple[] { - return tuples.map(tup => { - const proto = protoFromTuple(tup) - if (tup[1] != null) { - return [proto.code, convertToString(proto.code, tup[1])] - } - return [proto.code] - }) -} - /** * [[int code, Uint8Array ]... ] -> Uint8Array */ export function tuplesToBytes (tuples: Tuple[]): Uint8Array { - return fromBytes(uint8ArrayConcat(tuples.map((tup) => { - const proto = protoFromTuple(tup) + return uint8ArrayConcat(tuples.map((tup) => { + const proto = getProtocol(tup[0]) let buf = Uint8Array.from(varint.encode(proto.code)) if (tup.length > 1 && tup[1] != null) { @@ -109,13 +153,13 @@ export function tuplesToBytes (tuples: Tuple[]): Uint8Array { } return buf - }))) + })) } /** * For the passed address, return the serialized size */ -export function sizeForAddr (p: Protocol, addr: Uint8Array | number[]): number { +function sizeForAddr (p: Protocol, addr: Uint8Array | number[]): number { if (p.size > 0) { return p.size / 8 } else if (p.size === 0) { @@ -159,56 +203,6 @@ export function bytesToTuples (buf: Uint8Array): Tuple[] { return tuples } -/** - * Uint8Array -> String - */ -export function bytesToString (buf: Uint8Array): string { - const a = bytesToTuples(buf) - const b = tuplesToStringTuples(a) - return stringTuplesToString(b) -} - -/** - * String -> Uint8Array - */ -export function stringToBytes (str: string): Uint8Array { - str = cleanPath(str) - const a = stringToStringTuples(str) - const b = stringTuplesToTuples(a) - - return tuplesToBytes(b) -} - -/** - * String -> Uint8Array - */ -export function fromString (str: string): Uint8Array { - return stringToBytes(str) -} - -/** - * Uint8Array -> Uint8Array - */ -export function fromBytes (buf: Uint8Array): Uint8Array { - const err = validateBytes(buf) - if (err != null) { - throw err - } - return Uint8Array.from(buf) // copy -} - -export function validateBytes (buf: Uint8Array): Error | undefined { - try { - bytesToTuples(buf) // try to parse. will throw if breaks - } catch (err: any) { - return err - } -} - -export function isValidBytes (buf: Uint8Array): boolean { - return validateBytes(buf) === undefined -} - export function cleanPath (str: string): string { return '/' + str.trim().split('/').filter((a) => a).join('/') } @@ -216,8 +210,3 @@ export function cleanPath (str: string): string { export function ParseError (str: string): Error { return new Error('Error parsing address: ' + str) } - -export function protoFromTuple (tup: any[]): Protocol { - const proto = getProtocol(tup[0]) - return proto -} diff --git a/src/index.ts b/src/index.ts index e65323b9..a9fee946 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,8 +17,7 @@ import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import varint from 'varint' -import * as codec from './codec.js' +import { bytesToMultiaddrParts, stringToMultiaddrParts, type MultiaddrParts, tuplesToBytes } from './codec.js' import { getProtocol, names } from './protocols-table.js' const inspect = Symbol.for('nodejs.util.inspect.custom') @@ -494,10 +493,10 @@ export function isMultiaddr (value: any): value is Multiaddr { */ class DefaultMultiaddr implements Multiaddr { public bytes: Uint8Array - #string?: string - #tuples?: Tuple[] - #stringTuples?: StringTuple[] - #path?: string | null + #string: string + #tuples: Tuple[] + #stringTuples: StringTuple[] + #path: string | null [symbol]: boolean = true @@ -507,25 +506,28 @@ class DefaultMultiaddr implements Multiaddr { addr = '' } + let parts: MultiaddrParts if (addr instanceof Uint8Array) { - this.bytes = codec.fromBytes(addr) + parts = bytesToMultiaddrParts(addr) } else if (typeof addr === 'string') { if (addr.length > 0 && addr.charAt(0) !== '/') { throw new Error(`multiaddr "${addr}" must start with a "/"`) } - this.bytes = codec.fromString(addr) + parts = stringToMultiaddrParts(addr) } else if (isMultiaddr(addr)) { // Multiaddr - this.bytes = codec.fromBytes(addr.bytes) // validate + copy buffer + parts = bytesToMultiaddrParts(addr.bytes) } else { throw new Error('addr must be a string, Buffer, or another Multiaddr') } + + this.bytes = parts.bytes + this.#string = parts.string + this.#tuples = parts.tuples + this.#stringTuples = parts.stringTuples + this.#path = parts.path } toString (): string { - if (this.#string == null) { - this.#string = codec.bytesToString(this.bytes) - } - return this.#string } @@ -587,44 +589,22 @@ class DefaultMultiaddr implements Multiaddr { } protos (): Protocol[] { - return this.protoCodes().map(code => Object.assign({}, getProtocol(code))) + return this.#tuples.map(([code]) => Object.assign({}, getProtocol(code))) } protoCodes (): number[] { - const codes: number[] = [] - const buf = this.bytes - let i = 0 - while (i < buf.length) { - const code = varint.decode(buf, i) - const n = varint.decode.bytes ?? 0 - - const p = getProtocol(code) - const size = codec.sizeForAddr(p, buf.slice(i + n)) - - i += (size + n) - codes.push(code) - } - - return codes + return this.#tuples.map(([code]) => code) } protoNames (): string[] { - return this.protos().map(proto => proto.name) + return this.#tuples.map(([code]) => getProtocol(code).name) } tuples (): Array<[number, Uint8Array?]> { - if (this.#tuples == null) { - this.#tuples = codec.bytesToTuples(this.bytes) - } - return this.#tuples } stringTuples (): Array<[number, string?]> { - if (this.#stringTuples == null) { - this.#stringTuples = codec.tuplesToStringTuples(this.tuples()) - } - return this.#stringTuples } @@ -647,7 +627,7 @@ class DefaultMultiaddr implements Multiaddr { const tuples = this.tuples() for (let i = tuples.length - 1; i >= 0; i--) { if (tuples[i][0] === code) { - return new DefaultMultiaddr(codec.tuplesToBytes(tuples.slice(0, i))) + return new DefaultMultiaddr(tuplesToBytes(tuples.slice(0, i))) } } return this @@ -684,26 +664,6 @@ class DefaultMultiaddr implements Multiaddr { } getPath (): string | null { - // on initialization, this.#path is undefined - // after the first call, it is either a string or null - if (this.#path === undefined) { - try { - this.#path = this.stringTuples().filter((tuple) => { - const proto = getProtocol(tuple[0]) - if (proto.path === true) { - return true - } - return false - })[0][1] - - if (this.#path == null) { - this.#path = null - } - } catch { - this.#path = null - } - } - return this.#path } @@ -771,7 +731,7 @@ class DefaultMultiaddr implements Multiaddr { * ``` */ [inspect] (): string { - return `Multiaddr(${codec.bytesToString(this.bytes)})` + return `Multiaddr(${this.#string})` } } diff --git a/test/codec.spec.ts b/test/codec.spec.ts index b5413012..3cf73357 100644 --- a/test/codec.spec.ts +++ b/test/codec.spec.ts @@ -1,38 +1,35 @@ /* eslint-env mocha */ import { expect } from 'aegir/chai' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import varint from 'varint' import * as codec from '../src/codec.js' +import { convertToBytes } from '../src/convert.js' +import type { StringTuple, Tuple } from '../src/index.js' describe('codec', () => { - describe('.stringToStringTuples', () => { + describe('.stringToMultiaddrParts', () => { it('throws on invalid addresses', () => { expect( - () => codec.stringToStringTuples('/ip4/0.0.0.0/ip4') + () => codec.stringToMultiaddrParts('/ip4/0.0.0.0/ip4') ).to.throw( /invalid address/ ) }) }) - describe('.stringTuplesToTuples', () => { - it('handles non array tuples', () => { - expect( - codec.stringTuplesToTuples([['ip4', '0.0.0.0'], 'utp']) - ).to.eql( - [[4, Uint8Array.from([0, 0, 0, 0])], [302]] - ) - }) - }) + 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' } + ] - describe('.tuplesToStringTuples', () => { - it('single element tuples', () => { - expect( - codec.tuplesToStringTuples([[302]]) - ).to.eql( - [[302]] - ) - }) + for (const { name, string, stringTuples, tuples, path } 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) + }) + } }) describe('.bytesToTuples', () => { @@ -44,26 +41,4 @@ describe('codec', () => { ) }) }) - - describe('.fromBytes', () => { - it('throws on invalid buffer', () => { - expect( - () => codec.fromBytes(uint8ArrayFromString('hello/world')) - ).to.throw() - }) - }) - - describe('.isValidBytes', () => { - it('returns true for valid buffers', () => { - expect( - codec.isValidBytes(Uint8Array.from(varint.encode(302))) - ).to.equal(true) - }) - - it('returns false for invalid buffers', () => { - expect( - codec.isValidBytes(Uint8Array.from(varint.encode(1234))) - ).to.equal(false) - }) - }) })