Skip to content

Commit

Permalink
fix: precalculate multiaddr parts (#330)
Browse files Browse the repository at this point in the history
Taking #329 to its logical extreme, we're able to simplify the codebase (mostly `codec.ts` and its interaction with `DefaultMultiaddr`).

Precalculate a `MultiaddrParts` when constructing a `DefaultMultiaddr`.
`MultiaddrParts` is essentially an object containing all data which is stored in a `DefaultMultiaddr`.

This makes all `DefaultMultiaddr` methods cheap, at the expense of always storing the string, bytes, tuples, stringTuples, and path of the multiaddr.

The two important functions to review are `stringToMultiaddrParts` and `bytesToMultiaddrParts`, which convert untrusted string and bytes into `MultiaddrParts`.

---------

Co-authored-by: Tuyen Nguyen <vutuyen2636@gmail.com>
  • Loading branch information
wemeetagain and twoeths committed Jul 28, 2023
1 parent 76fa6f5 commit cf7e9c6
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 211 deletions.
207 changes: 98 additions & 109 deletions src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,38 @@ 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++) {
const part = parts[p]
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
}
Expand All @@ -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])
Expand All @@ -65,57 +140,26 @@ export function stringTuplesToString (tuples: StringTuple[]): string {
return cleanPath(parts.join('/'))
}

/**
* [[str name, str addr]... ] -> [[int code, Uint8Array]... ]
*/
export function stringTuplesToTuples (tuples: Array<string[] | string>): 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) {
buf = uint8ArrayConcat([buf, tup[1]]) // add address buffer
}

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) {
Expand Down Expand Up @@ -159,65 +203,10 @@ 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('/')
}

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
}
Loading

0 comments on commit cf7e9c6

Please sign in to comment.