UNPKG

@adguard/agtree

Version:
267 lines (264 loc) 9.79 kB
/* * AGTree v3.4.3 (build date: Thu, 11 Dec 2025 13:43:19 GMT) * (c) 2025 Adguard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme */ import { ByteBuffer } from './byte-buffer.js'; import { isArrayOfUint8Arrays } from './type-guards.js'; import { isChromium } from './is-chromium.js'; import { decodeTextPolyfill } from './text-decoder-polyfill.js'; import { BINARY_SCHEMA_VERSION } from './binary-schema-version.js'; import { BinarySchemaMismatchError } from '../errors/binary-schema-mismatch-error.js'; /* eslint-disable max-len */ /* eslint-disable no-plusplus */ /* eslint-disable no-bitwise */ /** * @file Input byte buffer for reading binary data. */ /** * Input byte buffer for reading binary data. * * @note Internally, this class uses a {@link ByteBuffer} instance, just providing a convenient API for reading data. */ class InputByteBuffer extends ByteBuffer { /** * Current offset in the buffer for reading. */ offset; /** * Shared native decoder for decoding strings. */ sharedNativeDecoder; /** * Flag indicating if the current environment is Chromium. * This is used for performance optimizations, because Chromium's TextEncoder/TextDecoder has a relatively * large marshalling overhead for small strings. */ isChromium; /** * Constructs a new `InputByteBuffer` instance. * * @param chunks Array of chunks to initialize the ByteBuffer with. * @param cloneChunks Flag indicating if the chunks should be cloned. For performance reasons, * its default value is `false`. If the original chunks are guaranteed not to change, * leave this flag as `false` to avoid unnecessary copying. * @param initialOffset Initial offset in the buffer for reading. * * @throws If the specified chunks array is empty. * @throws If the binary schema version in the buffer is not equal to the expected version. * @throws If the initial offset is out of bounds. */ constructor(chunks, cloneChunks = false, initialOffset = 0) { super(chunks, cloneChunks); // TODO: Consider accepting an empty array of chunks // Check binary schema version if (chunks.length === 0) { throw new Error('No data in the buffer'); } const actualVersion = this.readSchemaVersion(); if (actualVersion !== BINARY_SCHEMA_VERSION) { throw new BinarySchemaMismatchError(BINARY_SCHEMA_VERSION, actualVersion); } // Throw an error if the initial offset is out of bounds if (initialOffset < 0 || initialOffset > this.chunks.length * ByteBuffer.CHUNK_SIZE) { throw new Error(`Invalid offset: ${initialOffset}`); } // Schema version is always stored at the beginning of the buffer - skip it, because it is already processed this.offset = Math.max(4, initialOffset); this.sharedNativeDecoder = new TextDecoder(); this.isChromium = isChromium(); } /** * Creates a new InputByteBuffer instance from a Storage instance by reading chunks from the storage. * * @param storage Storage instance. * @param key Key to read from the storage. * @returns New InputByteBuffer instance. * @note For performance reasons, chunks are passed by reference and not copied. */ static async createFromStorage(storage, key) { const chunks = await storage.get(key); if (!isArrayOfUint8Arrays(chunks)) { throw new Error('The data from storage is not an array of Uint8Arrays'); } return new InputByteBuffer(chunks); } /** * Reads a 8-bit unsigned integer from the buffer. * * @returns 8-bit unsigned integer from the buffer. */ readUint8() { const result = this.readByte(this.offset++) ?? 0; return result; } /** * Reads a 16-bit unsigned integer from the buffer. * * @returns 16-bit unsigned integer from the buffer. */ readUint16() { const result = (((this.readByte(this.offset++) ?? 0) << 8) | ((this.readByte(this.offset++) ?? 0))) >>> 0; return result; } /** * Reads a 32-bit unsigned integer from the buffer at the specified index. * * @param index Index to read the 32-bit unsigned integer from. * * @returns 32-bit unsigned integer from the buffer. */ readUint32FromIndex(index) { const result = (((this.readByte(index) ?? 0) << 24) | ((this.readByte(index + 1) ?? 0) << 16) | ((this.readByte(index + 2) ?? 0) << 8) | ((this.readByte(index + 3) ?? 0))) >>> 0; return result; } /** * Reads a 32-bit unsigned integer from the buffer. * * @returns 32-bit unsigned integer from the buffer. */ readUint32() { const result = this.readUint32FromIndex(this.offset); this.offset += 4; return result; } /** * Reads schema version from the buffer. * * @returns 32-bit unsigned integer from the buffer. * @note Schema version is always stored at the beginning of the buffer. */ readSchemaVersion() { return this.readUint32FromIndex(0); } /** * Reads a 32-bit signed integer from the buffer. * * @returns 32-bit signed integer from the buffer. */ readInt32() { const result = this.readUint32(); return result > 0x7fffffff ? result - 0x100000000 : result; } /** * Reads an optimized unsigned integer from the buffer. * 'Optimized' means that the integer is stored in a variable number of bytes, depending on its value, * so that smaller numbers occupy less space. * * @returns Decoded unsigned integer from the buffer. */ readOptimizedUint() { let result = 0; let shift = 0; while (shift <= 28) { const byteValue = this.readByte(this.offset++) ?? 0; result |= (byteValue & 0x7F) << shift; shift += 7; if ((byteValue & 0x80) === 0) { break; } } return result; } /** * Reads a string from the buffer. * * @returns Decoded string from the buffer. */ readString() { const length = this.readOptimizedUint(); let chunkIndex = this.offset >>> 0x000F; const chunkOffset = this.offset & 0x7FFF; // offset is only relevant for the first chunk const endOffset = chunkOffset + length; // In most cases, the string is stored in the current chunk if (endOffset < ByteBuffer.CHUNK_SIZE) { this.offset += length; if (this.isChromium) { return decodeTextPolyfill(this.chunks[chunkIndex], chunkOffset, endOffset); } return this.sharedNativeDecoder.decode(this.chunks[chunkIndex].subarray(chunkOffset, endOffset)); } const result = []; result.push(this.sharedNativeDecoder.decode(this.chunks[chunkIndex++].subarray(chunkOffset), { stream: true })); let remaining = length - (ByteBuffer.CHUNK_SIZE - chunkOffset); while (remaining) { const chunk = this.chunks[chunkIndex]; if (!chunk) { break; } const toRead = Math.min(remaining, ByteBuffer.CHUNK_SIZE); result.push(this.sharedNativeDecoder.decode(chunk.subarray(0, toRead), { stream: true })); remaining -= toRead; chunkIndex += 1; } // Finish decoding, if something is left result.push(this.sharedNativeDecoder.decode()); this.offset += length; return result.join(''); } /** * Reads a 8-bit unsigned integer from the buffer without advancing the offset. * * @returns 8-bit unsigned integer from the buffer. */ peekUint8() { return this.readByte(this.offset) ?? 0; } /** * Helper method for asserting the next 8-bit unsigned integer in the buffer. * * @param value Expected value. * @throws If the next value in the buffer is not equal to the expected value. */ assertUint8(value) { const result = this.readUint8(); if (result !== value) { throw new Error(`Expected ${value}, but got ${result}`); } } /** * Creates a new `InputByteBuffer` instance with the given initial offset. * * @param initialOffset Initial offset for the new buffer. * @param cloneChunks Flag indicating if the chunks should be cloned. For performance reasons, * its default value is `false`. If the original chunks are guaranteed not to change, * leave this flag as `false` to avoid unnecessary copying. * * @returns New `InputByteBuffer` instance with the given initial offset. * * @note This method is useful if you want to read some data from a specific index. */ createCopyWithOffset(initialOffset, cloneChunks = false) { return new InputByteBuffer(this.chunks, cloneChunks, initialOffset); } /** * Gets the current offset in the buffer for reading. * * @returns Current offset in the buffer for reading. */ get currentOffset() { return this.offset; } /** * Gets the capacity of the buffer. * * @returns Capacity of the buffer. */ get capacity() { return this.chunks.length * ByteBuffer.CHUNK_SIZE; } /** * Gets the chunks of the buffer. * * @returns Chunks of the buffer. */ getChunks() { return this.chunks; } } export { InputByteBuffer };