Files
rea/src/filesystem/readStableFileBytes.ts
T
morluto e31aa72c9d fix(ghidra): retain executable header read and cleanup ownership
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
2026-10-17 12:17:44 +08:00

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);
};