mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
e31aa72c9d
Read bounded stable prefixes through the installation file owner, preserve unknown header diagnostics, and propagate cancellation or incomplete cleanup. Share extent and identity validation with full artifact reads. Validated with 269 focused tests, check:fast, and docs:check. Controlled Darwin file faults reproduce the original lost owner; real Ghidra and native Windows/Linux verification remain unperformed. Refs #4676
132 lines
3.9 KiB
TypeScript
132 lines
3.9 KiB
TypeScript
import type { BigIntStats } from "node:fs";
|
|
import { lstat, stat, type FileHandle } from "node:fs/promises";
|
|
import {
|
|
assertRegularFileActive,
|
|
RegularFileChangedError,
|
|
sameRegularFileState,
|
|
} from "./RegularFile.js";
|
|
|
|
/** Resource refusal carries the observed extent before allocating file bytes. */
|
|
export class RegularFileCapacityError extends Error {
|
|
readonly _tag = "RegularFileCapacityError";
|
|
constructor(
|
|
readonly path: string,
|
|
readonly maximumBytes: number,
|
|
readonly observedBytes: bigint,
|
|
) {
|
|
super(`Selected file must be at most ${maximumBytes} bytes: ${path}`);
|
|
this.name = this._tag;
|
|
}
|
|
}
|
|
|
|
interface StableReadOptions {
|
|
readonly signal?: AbortSignal;
|
|
readonly symlinks: "follow" | "reject";
|
|
}
|
|
|
|
/** Read an admitted descriptor completely, checking its extent and selected name. */
|
|
export const readStableFileBytes = async (
|
|
file: FileHandle,
|
|
path: string,
|
|
maximumBytes: number,
|
|
options: StableReadOptions,
|
|
): Promise<Buffer> => {
|
|
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 0)
|
|
throw new RangeError(
|
|
"Maximum file bytes must be a nonnegative safe integer",
|
|
);
|
|
assertRegularFileActive(options.signal);
|
|
const before = await file.stat({ bigint: true });
|
|
if (before.size > BigInt(maximumBytes))
|
|
throw new RegularFileCapacityError(path, maximumBytes, before.size);
|
|
const bytes = await readExtent(
|
|
file,
|
|
path,
|
|
Number(before.size),
|
|
options.signal,
|
|
);
|
|
assertRegularFileActive(options.signal);
|
|
const extra = Buffer.alloc(1);
|
|
if ((await file.read(extra, 0, 1, bytes.length)).bytesRead !== 0)
|
|
throw new RegularFileChangedError(path);
|
|
await assertStableIdentity(file, path, before, options);
|
|
return bytes;
|
|
};
|
|
|
|
/** Read only the requested prefix, retaining the validated full file extent. */
|
|
export const readStableFilePrefix = async (
|
|
file: FileHandle,
|
|
path: string,
|
|
maximumBytes: number,
|
|
options: StableReadOptions,
|
|
): Promise<{ readonly bytes: Buffer; readonly size: bigint }> => {
|
|
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 0)
|
|
throw new RangeError(
|
|
"Maximum prefix bytes must be a nonnegative safe integer",
|
|
);
|
|
assertRegularFileActive(options.signal);
|
|
const before = await file.stat({ bigint: true });
|
|
const length = Number(
|
|
before.size < BigInt(maximumBytes) ? before.size : BigInt(maximumBytes),
|
|
);
|
|
const bytes = await readExtent(file, path, length, options.signal);
|
|
await assertStableIdentity(file, path, before, options);
|
|
return { bytes, size: before.size };
|
|
};
|
|
|
|
const readExtent = async (
|
|
file: FileHandle,
|
|
path: string,
|
|
length: number,
|
|
signal?: AbortSignal,
|
|
): Promise<Buffer> => {
|
|
assertRegularFileActive(signal);
|
|
const bytes = Buffer.alloc(length);
|
|
let offset = 0;
|
|
while (offset < bytes.length) {
|
|
assertRegularFileActive(signal);
|
|
const read = await file.read({
|
|
buffer: bytes,
|
|
offset,
|
|
length: bytes.length - offset,
|
|
position: offset,
|
|
});
|
|
if (read.bytesRead === 0) throw new RegularFileChangedError(path);
|
|
offset += read.bytesRead;
|
|
}
|
|
assertRegularFileActive(signal);
|
|
return bytes;
|
|
};
|
|
|
|
const assertStableIdentity = async (
|
|
file: FileHandle,
|
|
path: string,
|
|
before: BigIntStats,
|
|
options: StableReadOptions,
|
|
): Promise<void> => {
|
|
assertRegularFileActive(options.signal);
|
|
const named = async () => {
|
|
try {
|
|
return await (options.symlinks === "follow" ? stat : lstat)(path, {
|
|
bigint: true,
|
|
});
|
|
} catch (cause: unknown) {
|
|
if (
|
|
cause instanceof Error &&
|
|
"code" in cause &&
|
|
(cause.code === "ENOENT" ||
|
|
cause.code === "ENOTDIR" ||
|
|
cause.code === "ELOOP")
|
|
)
|
|
throw new RegularFileChangedError(path, { cause });
|
|
throw cause;
|
|
}
|
|
};
|
|
if (
|
|
!sameRegularFileState(before, await file.stat({ bigint: true })) ||
|
|
!sameRegularFileState(before, await named())
|
|
)
|
|
throw new RegularFileChangedError(path);
|
|
assertRegularFileActive(options.signal);
|
|
};
|