* refactor(domain): consolidate canonical ordering and digest helpers (#624) * refactor(domain): consolidate canonical ordering and digest helpers Single owner for code-point ordering (domain/canonicalOrdering.ts) and canonical JSON digests (domain/comparisonSemantics.ts). Delete all local compareCodePoints/compareText/digestCanonical duplicates and migrate every importer. No re-export shims. * fix(ci): allow slower real browser startup * chore(config): remove dead eslint, dedupe scripts, cacheable turbo tasks Delete eslint.config.js (oxlint is the linter), drop lint:dead alias, pin smol-toml, fold jscpd/scan:todos into check:pr, make test:run cacheable, align oxlint ignores. * fix(config): hash all test inputs for cached runs * docs: fix architecture diagram and pretty-print tool catalog Drop dangling REPLAY edges, add dotnet/reference/error/evaluation nodes. Emit 2-space JSON from the catalog generator and regenerate. * test: extract shared fixtures, drop knip carve-outs Dedupe android/apple session tests, bundler adapter headers, and reconstruction helpers into tests/support fixtures. Delete unused type aliases and the knip ignoreIssues block. Lower jscpd threshold 2.7 to 2.5. * refactor(app): split Setup and ProcessOwnership modules Extract SetupTypes/SetupHost from Setup.ts and move observation helpers into ProcessOwnershipObservation/ProcessTree. Migrate all importers including CLI commands. No re-export shims. * refactor(runtime): centralize safe JSON parsing and preserve error causes Add domain/safeJson.safeParseJson and migrate all raw JSON.parse call sites. Name catch causes and preserve failure reasons instead of swallowing; annotate genuine best-effort cleanup. * fix(runtime): retain JSON parse error causes
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { constants } from "node:fs";
|
|
import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
import type { Readable } from "node:stream";
|
|
|
|
import {
|
|
ArtifactReaderFailure,
|
|
type ArtifactEntry,
|
|
type ArtifactReader,
|
|
} from "./ArtifactReader.js";
|
|
|
|
/** Deterministic, non-symlink-following directory reader. */
|
|
export class DirectoryArtifactReader implements ArtifactReader {
|
|
readonly format = "directory" as const;
|
|
readonly #rootPromise: Promise<string>;
|
|
|
|
constructor(root: string) {
|
|
this.#rootPromise = realpath(root);
|
|
}
|
|
|
|
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
|
|
const root = await this.#rootPromise;
|
|
const pending = [root];
|
|
while (pending.length > 0) {
|
|
abortIfNeeded(signal);
|
|
const directory = pending.pop();
|
|
if (directory === undefined) break;
|
|
await assertContainedDirectory(root, directory);
|
|
const children: string[] = [];
|
|
const handle = await opendir(directory);
|
|
try {
|
|
for await (const child of handle) children.push(child.name);
|
|
} finally {
|
|
// best-effort cleanup: directory-handle close must not mask traversal.
|
|
await handle.close().catch(() => undefined);
|
|
}
|
|
await assertContainedDirectory(root, directory);
|
|
children.sort((left, right) => left.localeCompare(right, "en"));
|
|
const directories: string[] = [];
|
|
for (const name of children) {
|
|
abortIfNeeded(signal);
|
|
const absolute = join(directory, name);
|
|
const metadata = await lstat(absolute);
|
|
const path = relative(root, absolute).split(sep).join("/");
|
|
const kind = metadata.isSymbolicLink()
|
|
? "symlink"
|
|
: metadata.isDirectory()
|
|
? "directory"
|
|
: metadata.isFile()
|
|
? "file"
|
|
: undefined;
|
|
if (kind === undefined) continue;
|
|
if (kind === "directory") directories.push(absolute);
|
|
yield {
|
|
path,
|
|
kind,
|
|
declaredSize: kind === "file" ? metadata.size : null,
|
|
compressedSize: null,
|
|
executable: (metadata.mode & 0o111) !== 0,
|
|
encrypted: false,
|
|
byteOffset: null,
|
|
declaredSha256: null,
|
|
unpacked: false,
|
|
limitations:
|
|
kind === "symlink"
|
|
? ["Symlink target was not followed or disclosed."]
|
|
: [],
|
|
adapterKey: absolute,
|
|
...(kind === "file"
|
|
? { sourceIdentity: { device: metadata.dev, inode: metadata.ino } }
|
|
: {}),
|
|
};
|
|
}
|
|
directories.reverse();
|
|
pending.push(...directories);
|
|
}
|
|
}
|
|
|
|
async open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable> {
|
|
abortIfNeeded(signal);
|
|
if (entry.kind !== "file")
|
|
throw new ArtifactReaderFailure(
|
|
"format",
|
|
"Only regular files can be opened",
|
|
);
|
|
const handle = await open(
|
|
entry.adapterKey,
|
|
constants.O_RDONLY | constants.O_NOFOLLOW,
|
|
);
|
|
try {
|
|
const observed = await handle.stat();
|
|
if (
|
|
!observed.isFile() ||
|
|
entry.sourceIdentity === undefined ||
|
|
observed.dev !== entry.sourceIdentity.device ||
|
|
observed.ino !== entry.sourceIdentity.inode
|
|
) {
|
|
throw new ArtifactReaderFailure(
|
|
"integrity",
|
|
`Directory entry changed before open: ${entry.path}`,
|
|
);
|
|
}
|
|
return handle.createReadStream({ autoClose: true });
|
|
} catch (cause: unknown) {
|
|
// best-effort cleanup: file-handle close must not mask the open failure.
|
|
await handle.close().catch(() => undefined);
|
|
throw cause;
|
|
}
|
|
}
|
|
|
|
close(): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
provenance(): readonly [] {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
const abortIfNeeded = (signal?: AbortSignal): void => {
|
|
if (signal?.aborted === true)
|
|
throw new ArtifactReaderFailure(
|
|
"cancelled",
|
|
"Artifact traversal cancelled",
|
|
);
|
|
};
|
|
|
|
const assertContainedDirectory = async (
|
|
root: string,
|
|
directory: string,
|
|
): Promise<void> => {
|
|
const canonical = await realpath(directory);
|
|
const fromRoot = relative(root, canonical);
|
|
const metadata = await lstat(directory);
|
|
if (
|
|
metadata.isSymbolicLink() ||
|
|
!metadata.isDirectory() ||
|
|
fromRoot === ".." ||
|
|
fromRoot.startsWith(`..${sep}`) ||
|
|
isAbsolute(fromRoot)
|
|
)
|
|
throw new ArtifactReaderFailure(
|
|
"path",
|
|
"Directory traversal escaped or changed its artifact root",
|
|
);
|
|
};
|