can1357/oh-my-pi · error · ArchiveError

Invalid archive range

Error message

Invalid archive range

What it means

`assertValidRange` validates every `[start, end)` byte range before any read in the archive ByteSource layer. It rejects ranges where start or end is not a safe integer, start is negative, or end is less than start — i.e. ranges that could never name a valid byte span. This is a defensive pre-condition shared by memory and file sources so downstream code never sees nonsensical offsets.

Source

Thrown at packages/utils/src/ar/source.ts:17

import { LRUCache } from "../lru";
import { ArchiveError } from "./error";

/**
 * A byte window into an archive — file-backed (lazy, ranged reads) or
 * in-memory. Format readers index through this so ZIP/ASAR/RAR payloads are
 * only read when a member is actually extracted.
 */
export interface ByteSource {
	readonly size: number;
	read(start: number, end: number): Promise<Uint8Array>;
}

/** Reject a nonsensical `[start, end)` range before any read. */
export function assertValidRange(start: number, end: number): void {
	if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
		throw new ArchiveError("Invalid archive range");
	}
}

/** Read an exact in-memory range, throwing (not clamping) when it runs past the buffer. */
export function readMemoryRange(buffer: Uint8Array, start: number, end: number): Uint8Array {
	assertValidRange(start, end);
	if (end > buffer.byteLength) {
		throw new ArchiveError("Invalid archive: truncated data");
	}
	return buffer.subarray(start, end);
}

/** Wrap borrowed bytes as a {@link ByteSource}. */
export function memoryByteSource(buffer: Uint8Array): ByteSource {
	return {
		size: buffer.byteLength,
		async read(start, end) {
			return readMemoryRange(buffer, start, end);

View on GitHub (pinned to 9690622007)

Solutions

  1. Audit the caller's range math — confirm you pass (start, end) as an exclusive end, not (start, length)
  2. Validate offsets/sizes parsed from archive headers against source.size before use
  3. Clamp computed ranges to [0, size] with Math.max/Math.min and re-check before reading

Example fix

// before: passing length as end
await source.read(offset, entrySize);
// after: compute the exclusive end and validate
const end = offset + entrySize;
if (!Number.isSafeInteger(end) || end > source.size) throw new Error("bad entry range");
await source.read(offset, end);
Defensive patterns

Strategy: validation

Validate before calling

function safeRange(start: number, end: number, size?: number): void {
	if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) throw new Error(`invalid range [${start}, ${end})`);
	if (size !== undefined && end > size) throw new Error(`range end ${end} exceeds size ${size}`);
}

Type guard

function isValidRange(start: unknown, end: unknown): boolean {
	return typeof start === "number" && typeof end === "number" && Number.isSafeInteger(start) && Number.isSafeInteger(end) && start >= 0 && end >= start;
}

Try / catch

try {
	const bytes = await src.read(start, end);
} catch (err) {
	if (err instanceof ArchiveError && err.message === "Invalid archive range") {
		logger.error("bad read range", { start, end });
	}
	throw err;
}

Prevention

When it happens

Trigger: Calling `readMemoryRange`/`read` (or any archive API that reads ranges) with a negative offset, a fractional or NaN offset, an offset exceeding Number.MAX_SAFE_INTEGER, or an end before start — typically from bad arithmetic on header-derived sizes/offsets.

Common situations: Reading offsets from a corrupt archive header where a size field is huge (overflowing past MAX_SAFE_INTEGER or wrapping negative); buggy caller math passing (start, length) instead of (start, end).

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/32e38378896d6116. Report an issue: GitHub.