Files
rea/src/reference/ReferenceSourceReaderEntries.ts
T
morluto 6636045c9c fix(reference): match exclusions against observed entry kinds
Inspect each entry without following links before applying directory-only patterns. Retain the winning rule and original entry path while pruning excluded directories before reading their children.

Validate directory exclusions, negation, symlinks, explicit secret rules, and compiled CLI provenance alongside reference reader failure and cancellation coverage.
2026-10-11 04:43:02 +08:00

317 lines
8.9 KiB
TypeScript

import type { BigIntStats, Dir, Dirent } from "node:fs";
import { opendir, readlink, realpath } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import {
cancelled,
entryFailure,
filesystemFailureDetail,
safeSize,
} from "./ReferenceSourceReaderErrors.js";
import { isPathWithinRoot } from "../domain/localPath.js";
import { pathFromRoot } from "./ReferenceSourceReaderPaths.js";
import { readStableFile } from "./ReferenceSourceReaderFile.js";
import {
isAborted,
sameFile,
validateDirectory,
bigLstat,
} from "./ReferenceSourceReaderValidate.js";
import {
type PendingDirectory,
type ReferenceSourceEntry,
type ReferenceSourceEntryKind,
type ReferenceSourceResult,
type TraversalState,
} from "./ReferenceSourceReaderTypes.js";
export const traverseDirectory = async (
state: TraversalState,
current: PendingDirectory,
): Promise<ReferenceSourceResult<undefined>> => {
const before = await validateDirectory(
state.root,
state.rootIdentity,
current.path,
state.signal,
);
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
if (!before.ok) {
state.entries.push(
entryFailure(
pathFromRoot(state.root, current.path),
"directory",
before.code,
before.message,
),
);
return { ok: true, value: undefined };
}
const directories: PendingDirectory[] = [];
let readFailure: string | undefined;
let handle: Dir | undefined;
try {
handle = await opendir(current.path);
} catch (cause: unknown) {
readFailure = directoryReadFailure(cause);
}
if (handle !== undefined) {
const iterator = handle[Symbol.asyncIterator]();
try {
for (;;) {
let child: IteratorResult<Dirent>;
try {
child = await iterator.next();
} catch (cause: unknown) {
readFailure = directoryReadFailure(cause);
break;
}
if (child.done) break;
const result = await processEntry(
state,
current,
child.value.name,
directories,
);
if (!result.ok) return result;
}
} finally {
await handle.close().catch(() => undefined);
}
}
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
if (readFailure !== undefined) {
state.entries.push(
entryFailure(
pathFromRoot(state.root, current.path),
"directory",
"io",
readFailure,
),
);
return { ok: true, value: undefined };
}
const after = await validateDirectory(
state.root,
state.rootIdentity,
current.path,
state.signal,
);
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
if (!after.ok || !sameFile(before.stats, after.stats)) {
state.entries.push(
entryFailure(
pathFromRoot(state.root, current.path),
"directory",
"changed",
"Directory changed while it was read",
),
);
return { ok: true, value: undefined };
}
if (current.path !== state.root)
state.entries.push({
status: "read",
kind: "directory",
path: pathFromRoot(state.root, current.path),
});
directories.reverse();
state.pending.push(...directories);
return { ok: true, value: undefined };
};
const directoryReadFailure = (cause: unknown): string => {
const message = filesystemFailureDetail(cause, "Directory could not be read");
if (message === undefined) throw cause;
return message;
};
const readMetadata = async (
path: string,
): Promise<
| { readonly ok: true; readonly value: BigIntStats }
| { readonly ok: false; readonly message: string }
> => {
try {
return { ok: true, value: await bigLstat(path) };
} catch (cause: unknown) {
const message = filesystemFailureDetail(
cause,
"Entry metadata could not be read",
);
if (message === undefined) throw cause;
return { ok: false, message };
}
};
const processEntry = async (
state: TraversalState,
current: PendingDirectory,
name: string,
directories: PendingDirectory[],
): Promise<ReferenceSourceResult<undefined>> => {
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
const absolute = join(current.path, name);
const path = pathFromRoot(state.root, absolute);
const metadata = await readMetadata(absolute);
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
if (!metadata.ok) {
state.entries.push(entryFailure(path, "unknown", "io", metadata.message));
return { ok: true, value: undefined };
}
const kind = metadata.value.isSymbolicLink()
? "symlink"
: metadata.value.isDirectory()
? "directory"
: metadata.value.isFile()
? "file"
: "other";
const excluded = applyExclusion(state.shouldExclude, path, kind);
if (!excluded.ok)
return {
ok: false,
error: {
tag: "reference-source-reader",
code: "io",
message: "Reference source exclusion check failed",
},
};
if (excluded.value) return { ok: true, value: undefined };
if (isAborted(state.signal)) return { ok: false, error: cancelled() };
if (kind === "symlink")
state.entries.push(
await describeSymlink(state.root, absolute, path, state.signal),
);
else if (kind === "directory") {
directories.push({ path: absolute });
} else if (kind !== "file")
state.entries.push(
entryFailure(
path,
"other",
"unsupported",
"Entry is not a regular file",
safeSize(metadata.value.size),
),
);
else await processFileEntry(state, absolute, path, metadata.value);
return { ok: true, value: undefined };
};
const processFileEntry = async (
state: TraversalState,
absolute: string,
path: string,
metadata: BigIntStats,
): Promise<void> => {
const result = await readStableFile({
root: state.root,
rootIdentity: state.rootIdentity,
absolute,
path,
expected: metadata,
...(state.signal === undefined ? {} : { signal: state.signal }),
});
if (result.status === "read" && result.kind === "file")
state.bytesRead += result.bytes.byteLength;
state.entries.push(result);
};
const describeSymlink = async (
root: string,
absolute: string,
path: string,
signal?: AbortSignal,
): Promise<ReferenceSourceEntry> => {
try {
const rawTarget = await readlink(absolute);
const lexicalTarget = resolve(dirname(absolute), rawTarget);
if (!isPathWithinRoot(root, lexicalTarget))
return {
status: "read",
kind: "symlink",
path,
target: lexicalTarget,
targetState: "external",
};
try {
const canonicalTarget = await realpath(lexicalTarget);
return isPathWithinRoot(root, canonicalTarget)
? {
status: "read",
kind: "symlink",
path,
target: pathFromRoot(root, canonicalTarget),
targetState: "internal",
}
: {
status: "read",
kind: "symlink",
path,
target: canonicalTarget,
targetState: "external",
};
} catch (cause: unknown) {
if (isAborted(signal))
return entryFailure(
path,
"symlink",
"cancelled",
"Symlink inspection cancelled",
);
const code =
cause instanceof Error ? Reflect.get(cause, "code") : undefined;
if (code !== "ENOENT") {
const message = filesystemFailureDetail(
cause,
"Symbolic link target could not be resolved",
);
if (message === undefined) throw cause;
return entryFailure(path, "symlink", "io", message);
}
const missingOutsideRoot = !isPathWithinRoot(root, lexicalTarget);
return {
status: "read",
kind: "symlink",
path,
target: missingOutsideRoot
? lexicalTarget
: pathFromRoot(root, lexicalTarget),
targetState: "missing",
};
}
} catch (cause: unknown) {
if (isAborted(signal))
return entryFailure(
path,
"symlink",
"cancelled",
"Symlink inspection cancelled",
);
const message = filesystemFailureDetail(
cause,
"Symbolic link target could not be read",
);
if (message === undefined) throw cause;
return entryFailure(path, "symlink", "io", message);
}
};
const applyExclusion = (
shouldExclude:
| ((path: string, kind: ReferenceSourceEntryKind) => boolean)
| undefined,
path: string,
kind: ReferenceSourceEntryKind,
): { readonly ok: true; readonly value: boolean } | { readonly ok: false } => {
try {
return { ok: true, value: shouldExclude?.(path, kind) === true };
} catch (cause: unknown) {
// Exclusion predicates are caller-supplied; a throwing predicate fails
// closed and the caller-visible `{ ok: false }` preserves the rejection
// without propagating an arbitrary predicate cause.
void cause;
return { ok: false };
}
};