interface TextDecoder { decode(input?: any): string } interface TextEncoder { encode(input?: string): Uint8Array } /** * Tiny buffer class for UTF8 strings. * @internal * * It is a subclass of Uint8Array with the added capacity to encode/decode text from/into string and therefore requires to be initialised with BS.setupEncodeDecoder() as such: * * ```javascript * // Encode & Decode using Buffer class (nodejs) * BS.setupEncodeDecoder( * { * encode(s) { * return BS.create(Buffer.from(s || '')) * }, * }, * { * decode(bs) { * return Buffer.from(bs).toString() * }, * } * ) * ``` * * or * * ```javascript * // Encode & Decode using TextEncoder and TextDecoder class (nodejs) * // import { TextEncoder, TextDecoder } from 'util' // required in nodejs * BS.setupEncodeDecoder(new TextEncoder(), new TextDecoder()) * ``` * * @remarks * * Use `BS.create()` to create an instance. * `slice()` and `subarray()` will return a BS instance. * */ export class BS extends Uint8Array { /** * Test whether this byte string is the start of another one for a given length * * const abc = BS.create('abc') * const abcdef = BS.create('abcdef') * abc.isStartOf(abcdef, 3) === true * abc.isStartOf(abcdef, 4) === false */ isStartOf(bs: Uint8Array, length: number) { if (bs === this) return true if (!bs || length !== this.length) return false let i = 0 while (i < length) if (bs[i] !== this[i++]) return false return true } /** * Test strick equality * * const abc1 = BS.create('abc') * const abc2 = BS.create('abc') * abc1 !== abc2 * abc1.equals(abc2) === true */ equals(bs: Uint8Array) { return bs === this || (bs && bs.length === this.length && bs.every((b, i) => b === this[i])) } /** creates an istance from a string or a Uint8Array */ static create = (input: string | Uint8Array) => { const bs = typeof input === 'string' ? BS.encode(input) : input return new BS(bs.buffer, bs.byteOffset, bs.length) } /** * Extend a byte string with another one * * const abc = BS.create('abc') * const def = BS.create('def') * const abcdef = abc.append(def) * abcdef.toString() === 'abcdef' */ append(tail: BS) { if (this.length) { const length = tail.length + this.length const bs = new BS(length) bs.set(this) bs.set(tail, this.length) return bs } else { return tail } } /** converts to string */ toString(start?: number, length?: number) { if (start !== undefined && (start !== 0 || length !== this.byteOffset)) { return BS.decode(this.subarray(start, start + (length || this.byteOffset))) } else { return BS.decode(this) } } /** @internal */ static encode(_input?: string): BS { throw Error('Need to call BS.setupEncodeDecoder(new TextEncoder(), new TextDecoder())') } /** @internal */ static decode(_input?: BS): string { throw Error('Need to call BS.setupEncodeDecoder(new TextEncoder(), new TextDecoder())') } /** Defines encoder and decoder - must be called before use */ static setupEncodeDecoder(textEncoder: TextEncoder, textDecoder: TextDecoder) { BS.encode = (textEncoder.encode.bind(textEncoder)) as typeof BS.encode BS.decode = textDecoder.decode.bind(textDecoder) } /** @internal peeks the first few chars for debugging */ get _() { return this.length <= 50 ? this.toString() : `${this.subarray(0, 50)}…` } /** @internal singleton reffering to an empty array (used for initialisation of internal variables) */ static get EMPTY() { const empty = new BS() Object.defineProperty(BS, 'EMPTY', { value: empty }) return empty } } /** @internal */ export interface BS { slice(start?: number, end?: number): BS subarray(start?: number, end?: number): BS }