Files
xahau.js/packages/isomorphic/src/utils/index.ts
Mayukha Vadari 9b3bb9c14b fix: throw error if hexToBytes or hexToString is provided a string that is not in hex (#2657)
* better error handling

* fix browser tests

* add shared variable

* re-add test case
2024-05-08 13:02:34 -04:00

94 lines
3.6 KiB
TypeScript

import { randomBytes as cryptoRandomBytes } from 'crypto'
import type { BytesToHexFn, HexToBytesFn, RandomBytesFn } from './types'
import { HexToStringFn, StringToHexFn } from './types'
import { HEX_REGEX } from './shared'
const OriginalBuffer = Symbol('OriginalBuffer')
/**
* An extended Uint8Array that incorporates a reference to the original Node.js Buffer.
*
* When converting a Node.js Buffer to a Uint8Array, there's an optimization that shares
* the memory of the original Buffer with the resulting Uint8Array instead of copying data.
* The Uint8ArrayWithReference interface is used to attach a reference to the original Buffer, ensuring
* its persistence in memory (preventing garbage collection) as long as the Uint8Array exists.
* This strategy upholds the ownership semantics of the slice of the ArrayBuffer.
*/
interface Uint8ArrayWithReference extends Uint8Array {
[OriginalBuffer]: Buffer
}
/**
* Converts a Node.js Buffer to a Uint8Array for uniform behavior with browser implementations.
*
* Choices:
* 1. Directly returning the Buffer:
* - Operation: Return Buffer as is (a Buffer *IS* an instanceof Uint8Array).
* - Pros: Most memory and performance efficient.
* - Cons: Violates strict Uint8Array typing and may lead to issues where Buffer-specific features are [ab]used.
*
* 2. Using `new Uint8Array(buffer)` or `Uint8Array.from(buffer)`:
* - Operation: Copies the buffer's data into a new Uint8Array.
* - Pros: Ensures data isolation; memory-safe.
* - Cons: Less performant due to data duplication.
*
* 3. Using buf.buffer slice:
* - Operation: Shares memory between Buffer and Uint8Array.
* - Pros: Performant.
* - Cons: Risks with shared memory and potential for invalid references.
*
* 4. Using buf.buffer slice and keeping a Buffer reference for ownership semantics:
* - Operation: Shares memory and associates the original Buffer with the resulting Uint8Array.
* - Pros: Performant while ensuring the original Buffer isn't garbage collected.
* - Cons: Risks with shared memory but mitigates potential for invalid references.
*
* The chosen method (4) prioritizes performance by sharing memory while ensuring buffer ownership.
*
* @param {Buffer} buffer - The Node.js Buffer to convert.
* @returns {Uint8Array} Resulting Uint8Array sharing the same memory as the Buffer and maintaining a reference to it.
*/
function toUint8Array(buffer: Buffer): Uint8Array {
const u8Array = new Uint8Array(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
),
) as Uint8ArrayWithReference
u8Array[OriginalBuffer] = buffer
return u8Array
}
/* eslint-disable func-style -- Typed to ensure uniformity between node and browser implementations and docs */
export const bytesToHex: typeof BytesToHexFn = (bytes) => {
const buf = Buffer.from(bytes)
return buf.toString('hex').toUpperCase()
}
export const hexToBytes: typeof HexToBytesFn = (hex) => {
if (!HEX_REGEX.test(hex)) {
throw new Error('Invalid hex string')
}
return toUint8Array(Buffer.from(hex, 'hex'))
}
export const randomBytes: typeof RandomBytesFn = (size) => {
return toUint8Array(cryptoRandomBytes(size))
}
export const hexToString: typeof HexToStringFn = (
hex: string,
encoding = 'utf8',
): string => {
if (!HEX_REGEX.test(hex)) {
throw new Error('Invalid hex string')
}
return new TextDecoder(encoding).decode(hexToBytes(hex))
}
export const stringToHex: typeof StringToHexFn = (string: string): string => {
return bytesToHex(new TextEncoder().encode(string))
}
/* eslint-enable func-style */
export * from './shared'