UNPKG

archive-wasm

Version:

LibArchive compiled to WASM with a idiomatic JS api

288 lines (242 loc) 8.02 kB
/*! * archive-wasm - LibArchive compiled to WASM with a idiomatic JS API * Copyright (C) 2023 Spacedrive Technology Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** * @file Manage raw C pointers semi-automatically * @module archive-wasm/wasm/pointer.mjs */ import { NullError } from './errors.mjs' import { wasm } from './libarchive.mjs' const fallbackEncodings = Object.entries({ cp1251: ['ru', 'uk', 'be', 'bg', 'sr', 'bs', 'mk'], gb18030: ['zh'], cseuckr: ['ko'], csshiftjis: ['ja'], }).reduce((mapping, [encoding, languages]) => { for (const language of languages) mapping[language] = encoding return mapping }, /** @type {Record.<string, string>} */ ({})) /** * void * malloc(size_t size); * @callback MallocCB * @param {number} size Memory size to be allocated * @returns {number} Pointer to allocated memory */ const malloc = /** @type {MallocCB} */ (wasm.cwrap('malloc', 'number', ['number'])) /** * void free(void *ptr); * @callback FreeCB * @param {number} pointer Pointer to memory to be freed */ const free = /** @type {FreeCB} */ (wasm.cwrap('free', null, ['number'])) /** * Registry to automatically free any unreferenced {@link Pointer} * @type {FinalizationRegistry.<number>} */ const MemoryRegistry = new FinalizationRegistry(pointer => { free(pointer) }) /** @private */ export class Pointer { static NIL = new Pointer(0) static NULL = 0 /** * Free raw pointer * @param {number} pointer Raw C pointer */ static free(pointer) { if (pointer === Pointer.NULL) return free(pointer) } /** @type {number} */ #size /** @type {number} */ #pointer /** * High level representation of a WASM memory pointer * @param {number} [size] Pointer size, 0 means that pointer is managed * @param {number} [pointer] Raw C pointer */ constructor(size, pointer) { if (typeof size === 'number' && size < 0) throw new Error('Size must be >= 0') this.#size = size ?? 0 if (pointer == null && this.#size > 0) { this.#pointer = malloc(this.#size) if (this.isNull()) throw new NullError('Failed to allocate memory') } else { this.#pointer = pointer ?? Pointer.NULL } if (!(this.isNull() || this.isManaged())) MemoryRegistry.register(this, this.#pointer, this) } /** * Get underlining raw pointer * @returns {number} Raw C pointer */ get raw() { return this.#pointer } /** * Get possible allocated size for pointer * * > This can be null if pointer is externally managed * * > This will be zero when pointer is NULL * @returns {number?} Allocated pointer size */ get size() { return this.isManaged() ? null : this.#size } /** * Fill memory with data * * > When grow is false, this method throws when trying to fill a Pointer.NULL pointer, * otherwise it will realloc the Pointer so it can fit the given data * @param {bigint | number | string | ArrayLike.<number> | ArrayBufferView | ArrayBufferLike} data to copy to memory * @param {boolean} [grow] Wheter to alloc more data to make sure data fits inside {@link Pointer} * @returns {Pointer} This pointer */ fill(data, grow = false) { if (this.isManaged()) throw new Error("Can't modify managed Pointer") /** @type {Uint8Array} */ let array switch (typeof data) { case 'string': array = new TextEncoder().encode(data) break case 'number': array = new Uint8Array([data]) break case 'bigint': array = Uint8Array.from(new BigInt64Array([data])) break default: if (data instanceof ArrayBuffer) { array = new Uint8Array(data) } else if (ArrayBuffer.isView(data)) { array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength) } else if (data instanceof SharedArrayBuffer) { array = new Uint8Array(data) } else if (!(data instanceof Uint8Array)) { array = new Uint8Array(data) } else { array = data } } if (grow) { this.realloc(array.byteLength, true) } else if (array.byteLength > this.#size) { array = array.subarray(0, this.#size) } if (this.isNull()) throw new NullError('Failed to fill due to Pointer.NULL') wasm.HEAP8.set(array, this.#pointer) return this } /** * Copy data from WASM memory and return it * @param {number} [size] How much to read from memory * @returns {ArrayBuffer} Memory view */ read(size) { if (size == null) { size = this.#size } else if (size > this.#size) { throw new Error('Attempting to read past the pointer allocated memory') } else if (size < 0) { throw new Error('Size must be a positive number') } if (this.isNull()) { throw new NullError('failed to read due to Pointer.NULL') } else if (this.isManaged()) { throw new Error('Reading managed pointer') } return wasm.HEAP_DATA_VIEW.buffer.slice(this.#pointer, this.#pointer + size) } /** * Copy data from WASM memory and decode it as a string * @param {string} [encoding] Decoder label * @returns {string} String representation */ readString(encoding) { if (this.isNull()) return '' let dataView = new Uint8Array( wasm.HEAP_DATA_VIEW.buffer, this.#pointer, this.#size === 0 ? undefined : this.#size ) let endPointer = -1 while ((dataView[++endPointer] ?? 0) !== 0); if (endPointer === this.#pointer) return '' dataView = dataView.slice(0, endPointer) if (encoding) return new TextDecoder(encoding).decode(dataView) try { const lang = Intl.DateTimeFormat().resolvedOptions().locale.split('-')[0] if (lang) { const fallbackEncoding = fallbackEncodings[lang] if (fallbackEncoding) return new TextDecoder(fallbackEncoding, { fatal: true }).decode(dataView) } } catch {} try { return new TextDecoder('latin1', { fatal: true }).decode(dataView) } catch {} return new TextDecoder('utf8').decode(dataView) } /** * Free internal pointer */ free() { if (!this.isManaged()) { this.#size = 0 Pointer.free(this.#pointer) MemoryRegistry.unregister(this) } this.#pointer = Pointer.NULL } isNull() { return this.#pointer === Pointer.NULL } /** * Change pointer size * @param {number} size New pointer size, 0 frees the pointer * @param {boolean} avoidShrinking Don't reallocate when size is less then current allocated size * @returns {Pointer} This pointer */ realloc(size, avoidShrinking = false) { if (size < 0) throw new Error('Size must be >= 0') if (this.isManaged() || size === this.#size || (avoidShrinking && size < this.#size)) return this /** @type {number} */ let pointer if (size > 0) { pointer = malloc(size) if (pointer === Pointer.NULL) throw new NullError('Failed to allocate memory') if (!this.isNull()) { wasm.HEAP8.copyWithin(pointer, this.#pointer, this.#pointer + Math.min(size, this.#size)) this.free() } this.#pointer = pointer MemoryRegistry.register(this, this.#pointer, this) } else { this.free() } this.#size = size return this } isManaged() { return this.#size === 0 && !this.isNull() } }