Skip to content

Commit

Permalink
fix: don't try to grow the buffer more than allowed by the system
Browse files Browse the repository at this point in the history
  • Loading branch information
targos committed Feb 1, 2025
1 parent fc49c5b commit 86e612f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 8 deletions.
14 changes: 6 additions & 8 deletions src/IOBuffer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isHostBigEndian } from './env';
import { increaseBufferSize, isHostBigEndian } from './env';
import { decode, encode } from './text';

const defaultByteLength = 1024 * 8;
Expand Down Expand Up @@ -246,13 +246,11 @@ export class IOBuffer {
public ensureAvailable(byteLength = 1): this {
if (!this.available(byteLength)) {
const lengthNeeded = this.offset + byteLength;
const newLength = lengthNeeded * 2;
const newArray = new Uint8Array(newLength);
newArray.set(new Uint8Array(this.buffer));
this.buffer = newArray.buffer;
this.length = newLength;
this.byteLength = newLength;
this._data = new DataView(this.buffer);
const newBuffer = increaseBufferSize(this.buffer, lengthNeeded);
this.buffer = newBuffer;
this.length = newBuffer.byteLength;
this.byteLength = newBuffer.byteLength;
this._data = new DataView(newBuffer);
}
return this;
}
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ describe('write data', () => {
theBuffer.seek(20);
theBuffer.ensureAvailable(30);
expect(theBuffer.byteLength).toBeGreaterThanOrEqual(50);
expect(() => theBuffer.ensureAvailable(Number.MAX_SAFE_INTEGER)).toThrow(
RangeError,
);
});

it('writeUtf8', () => {
Expand Down
58 changes: 58 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,61 @@ function detectEndianness() {
const view = new Uint32Array(array.buffer);
return !((view[0] = 1) & array[0]);
}

/**
* Returns a new buffer with the same contents as the input buffer, but with a larger size.
* Try to at least double the size of the buffer to avoid frequent reallocations.
* @param buffer - The buffer to increase the size of.
* @param minimumSize - The minimum size of the new buffer.
* @returns The new buffer.
*/
export function increaseBufferSize(
buffer: ArrayBuffer,
minimumSize: number,
): ArrayBuffer {
let maxAllowedLength = 2 ** 29; // 512MB
if (minimumSize > maxAllowedLength) {
// The check is expensive, don't do it until a large buffer is requested.
maxAllowedLength = getMaxUint8ArrayLength();
if (minimumSize > maxAllowedLength) {
throw new RangeError(
`Cannot create a buffer with more than ${maxAllowedLength} bytes`,
);
}
}
// Don't try to allocate more than the maximum allowed length.
const doubleOrMax = Math.min(buffer.byteLength * 2, maxAllowedLength);
const newLength = Math.max(doubleOrMax, minimumSize);
const newBuffer = new ArrayBuffer(newLength);
new Uint8Array(newBuffer).set(new Uint8Array(buffer));
return newBuffer;
}

let maxUint8ArrayLength: number | undefined;

/**
* Detects and returns the maximum length that of an `ArrayBuffer`.
* @returns The maximum length that an `ArrayBuffer` can have.
*/
export function getMaxUint8ArrayLength() {
return (
maxUint8ArrayLength ?? (maxUint8ArrayLength = detectMaxUint8ArrayLength())
);
}

function detectMaxUint8ArrayLength() {
// Use a binary search to find the maximum length of an Uint8Array.
let low = 1;
let high = Number.MAX_SAFE_INTEGER - 1;
while (low < high) {
const mid = Math.trunc((low + high) / 2);
try {
// eslint-disable-next-line no-new
new Uint8Array(mid);
low = mid + 1;
} catch {
high = mid;
}
}
return low - 1;
}
11 changes: 11 additions & 0 deletions src/text.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
/**
* Decode a Uint8Array to a string.
* @param bytes - The Uint8Array to decode.
* @param encoding - The encoding to use. Defaults to 'utf8'.
* @returns The decoded string.
*/
export function decode(bytes: Uint8Array, encoding = 'utf8'): string {
const decoder = new TextDecoder(encoding);
return decoder.decode(bytes);
}

const encoder = new TextEncoder();

/**
* Encode a string as UTF-8 to a Uint8Array.
* @param str - The string to encode.
* @returns The encoded Uint8Array.
*/
export function encode(str: string): Uint8Array {
return encoder.encode(str);
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"sourceMap": true,
"strict": true,
"target": "es2022",
"lib": ["es2022"],
"skipLibCheck": true,
"allowJs": false,
"noEmit": true
Expand Down

0 comments on commit 86e612f

Please sign in to comment.