@webbuf/webbuf
Version:
Rust/WASM optimized buffers for the web, node.js, deno, and bun.
499 lines (445 loc) • 13.1 kB
text/typescript
import {
encode_base64,
decode_base64,
decode_base64_strip_whitespace,
encode_hex,
decode_hex,
encode_base32_crockford,
decode_base32_crockford,
encode_base32_rfc4648,
decode_base32_rfc4648,
encode_base32_rfc4648_lower,
decode_base32_rfc4648_lower,
encode_base32_rfc4648_hex,
decode_base32_rfc4648_hex,
encode_base32_rfc4648_hex_lower,
decode_base32_rfc4648_hex_lower,
encode_base32_z,
decode_base32_z,
} from "./rs-webbuf-inline-base64/webbuf.js";
/**
* Base32 alphabet types matching the Rust base32 crate
*/
export type Base32Alphabet =
| "Crockford"
| "Rfc4648"
| "Rfc4648Lower"
| "Rfc4648Hex"
| "Rfc4648HexLower"
| "Z";
/**
* Options for base32 encoding/decoding
*/
export interface Base32Options {
/**
* The alphabet to use for encoding/decoding.
* @default "Crockford"
*/
alphabet?: Base32Alphabet;
/**
* Whether to use padding (only applies to Rfc4648* alphabets).
* @default true
*/
padding?: boolean;
}
function verifyOffset(offset: number, ext: number, length: number) {
if (offset % 1 !== 0 || offset < 0) {
throw new Error("offset is not uint");
}
if (offset + ext > length) {
throw new Error("Trying to access beyond buffer length");
}
}
export class WebBuf extends Uint8Array {
static concat(list: Uint8Array[]) {
const size = list.reduce((acc, buf) => acc + buf.length, 0);
const result = new WebBuf(size);
let offset = 0;
for (const buf of list) {
result.set(buf, offset);
offset += buf.length;
}
return result;
}
static alloc(size: number, fill = 0) {
const buf = new WebBuf(size);
if (fill !== 0) {
buf.fill(fill);
}
return buf;
}
fill(value: number, start = 0, end = this.length) {
for (let i = start; i < end; i++) {
this[i] = value;
}
return this;
}
// Override slice method to return WebBuf instead of Uint8Array
slice(start?: number, end?: number): WebBuf {
const slicedArray = super.slice(start, end); // Create a slice using Uint8Array's slice method
return new WebBuf(
slicedArray.buffer,
slicedArray.byteOffset,
slicedArray.byteLength,
); // Return a WebBuf instead
}
subarray(start?: number, end?: number): WebBuf {
const subArray = super.subarray(start, end);
return new WebBuf(
subArray.buffer,
subArray.byteOffset,
subArray.byteLength,
);
}
/**
* Reverse the buffer in place
* @returns webbuf
*/
reverse(): this {
super.reverse();
return this;
}
clone() {
return new WebBuf(this);
}
toReverse() {
const cloned = new WebBuf(this);
cloned.reverse();
return cloned;
}
copy(
target: WebBuf,
targetStart = 0,
sourceStart = 0,
sourceEnd = this.length,
) {
if (sourceStart >= sourceEnd) {
return 0;
}
if (targetStart >= target.length) {
throw new Error("targetStart out of bounds");
}
if (sourceEnd > this.length) {
throw new Error("sourceEnd out of bounds");
}
if (targetStart + sourceEnd - sourceStart > target.length) {
throw new Error("source is too large");
}
target.set(this.subarray(sourceStart, sourceEnd), targetStart);
return sourceEnd - sourceStart;
}
/**
* Return a WebBuf that is a view of the same data as the input Uint8Array
*
* @param buffer
* @returns WebBuf
*/
static view(buffer: Uint8Array): WebBuf {
return new WebBuf(
buffer.buffer as ArrayBuffer,
buffer.byteOffset,
buffer.byteLength,
);
}
/**
* Create a new WebBuf from a Uint8Array (copy)
* @param buffer
* @returns webbuf
*/
static fromUint8Array(buffer: Uint8Array) {
return new WebBuf(buffer);
}
static fromArray(array: number[]) {
return new WebBuf(array);
}
static fromUtf8(str: string) {
const encoder = new TextEncoder();
return new WebBuf(encoder.encode(str));
}
static fromString(str: string, encoding: "utf8" | "hex" | "base64" = "utf8") {
if (encoding === "hex") {
return WebBuf.fromHex(str);
}
if (encoding === "base64") {
return WebBuf.fromBase64(str);
}
return WebBuf.fromUtf8(str);
}
// we use wasm for big data, because small data is faster in js
// experiments show wasm is always faster
static FROM_BASE64_ALGO_THRESHOLD = 10; // str len
// experiments show wasm is always faster
static TO_BASE64_ALGO_THRESHOLD = 10; // buf len
// experimentally derived for optimal performance
static FROM_HEX_ALGO_THRESHOLD = 1_000; // str len
// experiments show wasm is always faster
static TO_HEX_ALGO_THRESHOLD = 10; // buf len
static fromHexPureJs(hex: string): WebBuf {
const result = new WebBuf(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
result[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
}
return result;
}
static fromHexWasm(hex: string): WebBuf {
const uint8array = decode_hex(hex);
return new WebBuf(
uint8array.buffer as ArrayBuffer,
uint8array.byteOffset,
uint8array.byteLength,
);
}
static fromHex(hex: string): WebBuf {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
if (hex.length < WebBuf.FROM_HEX_ALGO_THRESHOLD) {
return WebBuf.fromHexPureJs(hex);
}
return WebBuf.fromHexWasm(hex);
}
toHexPureJs(): string {
return Array.from(this)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
toHexWasm(): string {
return encode_hex(this);
}
toHex(): string {
// disabled: experiments show this is not faster, even for small buffers
// if (this.length < WebBuf.TO_HEX_ALGO_THRESHOLD) {
// return this.toHexPureJs();
// }
return this.toHexWasm();
}
static fromBase64PureJs(b64: string, stripWhitespace = false): WebBuf {
const uint8array = new Uint8Array(
atob(stripWhitespace ? b64.replace(/\s+/g, "") : b64)
.split("")
.map((c) => c.charCodeAt(0)),
);
return new WebBuf(
uint8array.buffer,
uint8array.byteOffset,
uint8array.byteLength,
);
}
static fromBase64Wasm(b64: string, stripWhitespace = false): WebBuf {
const uint8array = stripWhitespace
? decode_base64_strip_whitespace(b64)
: decode_base64(b64);
return new WebBuf(
uint8array.buffer as ArrayBuffer,
uint8array.byteOffset,
uint8array.byteLength,
);
}
/**
* Convert a base64 string to a Uint8Array. Tolerant of whitespace, but
* throws if the string has invalid characters.
*
* @param b64
* @returns Uint8Array
* @throws {Error} if the input string is not valid base64
*/
static fromBase64(b64: string, stripWhitespace = false): WebBuf {
// disabled: experiments show this is not faster, even for small buffers
// if (b64.length < WebBuf.FROM_BASE64_ALGO_THRESHOLD) {
// return WebBuf.fromBase64PureJs(b64, stripWhitespace);
// }
return WebBuf.fromBase64Wasm(b64, stripWhitespace);
}
toBase64PureJs(): string {
return btoa(String.fromCharCode(...new Uint8Array(this)));
}
toBase64Wasm(): string {
return encode_base64(this);
}
toBase64() {
// disabled: experiments show this is not faster, even for small buffers
// if (this.length < WebBuf.TO_BASE64_ALGO_THRESHOLD) {
// return this.toBase64PureJs();
// }
return this.toBase64Wasm();
}
/**
* Encode this buffer as a base32 string.
*
* @param options - Options for encoding
* @param options.alphabet - The alphabet to use (default: "Crockford")
* @param options.padding - Whether to use padding for Rfc4648* alphabets (default: true)
* @returns The base32 encoded string
*/
toBase32(options?: Base32Options): string {
const alphabet = options?.alphabet ?? "Crockford";
const padding = options?.padding ?? true;
switch (alphabet) {
case "Crockford":
return encode_base32_crockford(this);
case "Rfc4648":
return encode_base32_rfc4648(this, padding);
case "Rfc4648Lower":
return encode_base32_rfc4648_lower(this, padding);
case "Rfc4648Hex":
return encode_base32_rfc4648_hex(this, padding);
case "Rfc4648HexLower":
return encode_base32_rfc4648_hex_lower(this, padding);
case "Z":
return encode_base32_z(this);
}
}
/**
* Create a WebBuf from a base32 encoded string.
*
* @param str - The base32 encoded string
* @param options - Options for decoding
* @param options.alphabet - The alphabet to use (default: "Crockford")
* @param options.padding - Whether the string uses padding for Rfc4648* alphabets (default: true)
* @returns The decoded WebBuf
*/
static fromBase32(str: string, options?: Base32Options): WebBuf {
const alphabet = options?.alphabet ?? "Crockford";
const padding = options?.padding ?? true;
let uint8array: Uint8Array;
switch (alphabet) {
case "Crockford":
uint8array = decode_base32_crockford(str);
break;
case "Rfc4648":
uint8array = decode_base32_rfc4648(str, padding);
break;
case "Rfc4648Lower":
uint8array = decode_base32_rfc4648_lower(str, padding);
break;
case "Rfc4648Hex":
uint8array = decode_base32_rfc4648_hex(str, padding);
break;
case "Rfc4648HexLower":
uint8array = decode_base32_rfc4648_hex_lower(str, padding);
break;
case "Z":
uint8array = decode_base32_z(str);
break;
}
return new WebBuf(
uint8array.buffer as ArrayBuffer,
uint8array.byteOffset,
uint8array.byteLength,
);
}
/**
* Override Uint8Array.from to return a WebBuf
*
* @param source An array-like or iterable object to convert to WebBuf
* @param mapFn Optional map function to call on every element of the array
* @param thisArg Optional value to use as `this` when executing `mapFn`
* @returns WebBuf
*/
static from(
source: ArrayLike<number> | Iterable<number> | string,
mapFn?: ((v: number, k: number) => number) | string,
thisArg?: unknown,
): WebBuf {
if (typeof mapFn === "string") {
if (typeof source !== "string") {
throw new TypeError("Invalid mapFn");
}
if (mapFn === "hex") {
return WebBuf.fromHex(source);
}
if (mapFn === "base64") {
return WebBuf.fromBase64(source);
}
if (mapFn === "utf8") {
return WebBuf.fromUtf8(source);
}
throw new TypeError("Invalid mapFn");
}
if (typeof source === "string") {
return WebBuf.fromUtf8(source);
}
if (source instanceof Uint8Array) {
return WebBuf.view(source);
}
const sourceArray = Array.from(source);
// biome-ignore lint:
const uint8Array = super.from(sourceArray, mapFn, thisArg);
return new WebBuf(
uint8Array.buffer,
uint8Array.byteOffset,
uint8Array.byteLength,
);
}
toUtf8(): string {
const decoder = new TextDecoder();
return decoder.decode(this);
}
toString(encoding?: "utf8" | "hex" | "base64") {
if (encoding === "hex") {
return this.toHex();
}
if (encoding === "base64") {
return this.toBase64();
}
if (encoding === "utf8") {
const decoder = new TextDecoder();
return decoder.decode(this);
}
return this.toUtf8();
}
inspect() {
return `<WebBuf ${this.toHex().slice(0, 40) + (this.length > 40 ? "..." : "")}>`;
}
toArray() {
return Array.from(this);
}
compare(other: WebBuf): number {
const len = Math.min(this.length, other.length);
for (let i = 0; i < len; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const a = this[i]!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const b = other[i]!;
if (a !== b) {
return a < b ? -1 : 1;
}
}
if (this.length < other.length) {
return -1;
}
if (this.length > other.length) {
return 1;
}
return 0;
}
static compare(buf1: WebBuf, buf2: WebBuf): number {
return buf1.compare(buf2);
}
equals(other: WebBuf): boolean {
return this.compare(other) === 0;
}
write(buf: WebBuf, offset = 0): number {
verifyOffset(offset, buf.length, this.length);
this.set(buf, offset);
return buf.length;
}
read(offset: number, ext: number): WebBuf {
verifyOffset(offset, ext, this.length);
return this.subarray(offset, offset + ext);
}
/**
* Securely wipe the buffer by filling it with zeros.
*
* Call this method before releasing references to buffers containing
* sensitive data (keys, passwords, etc.) to minimize the window where
* sensitive data remains in memory.
*
* Note: This is a best-effort security measure. JavaScript's JIT compiler
* may optimize away the write if it detects the buffer isn't read afterward,
* and copies of the data may exist elsewhere in memory.
*/
wipe(): void {
this.fill(0);
}
}