mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
46f51ee6cc
* fix(flutter): name a missing or non-file APK selection identify_flutter_build and inspect_dart_aot reported a missing path or a directory as "The APK could not be read as a package: <path>: ENOENT ...", which blames the package bytes for a selection mistake. Report "Selected APK does not exist" for ENOENT/ENOTDIR and "Selected APK is not a regular file" for a non-regular selection, matching the SQLite inspector's wording. The issue path and invalid_format reason are unchanged; package format failures keep their existing message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(filesystem): classify unopenable non-regular selections at admission A Unix socket cannot be opened at all: open() fails with ENXIO on Linux and EOPNOTSUPP on macOS (surfaced by libuv as "Unknown system error -102") before openRegularFile can stat the descriptor, so such a selection escaped NonRegularFileReadError and the Flutter provider still blamed the package bytes. When open fails, classify the path by its own metadata (lstat when symlinks are rejected, stat otherwise): an existing non-regular file becomes NonRegularFileReadError. Missing paths, permission failures, regular files, and the O_NOFOLLOW ELOOP rejection keep their original errors. The Flutter selection test now covers a real Unix socket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(filesystem): reclassify only unopenable file-type errors at admission Checking the path's metadata after any open failure turned EACCES on an unreadable directory into NonRegularFileReadError, so callers reported invalid input instead of access_denied. Reclassify only errors that mean the file type cannot be opened (ENXIO, EOPNOTSUPP, ENOTSUP, matched by errno because libuv has no name for macOS EOPNOTSUPP 102); permission, symlink-rejection and resource failures keep their original errors. Run the Unix socket selection check in its own test, skipped on Windows where listen() takes named pipes, and keep the missing-path and directory checks cross-platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(filesystem): keep cancellation that arrives during the non-regular probe A caller abort while the post-open metadata probe ran was replaced by NonRegularFileReadError, so callers such as parseBinaryTarget reported an input-format error instead of cancellation. Recheck the signal before substituting the non-regular classification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
import type { BigIntStats, Stats } from "node:fs";
|
|
import type { FileHandle } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
NonRegularFileReadError,
|
|
openRegularFile,
|
|
RegularFileAdmissionFailure,
|
|
sameRegularFileState,
|
|
} from "../filesystem/RegularFile.js";
|
|
|
|
const { mockOpen } = vi.hoisted(() => ({ mockOpen: vi.fn() }));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("node:fs/promises")>()),
|
|
open: mockOpen,
|
|
}));
|
|
|
|
afterEach(() => mockOpen.mockReset());
|
|
|
|
it.each(["cancellation after open", "non-regular metadata", "stat failure"])(
|
|
"retains the exact owner when close fails after %s",
|
|
async (failureCase) => {
|
|
const closeFailure = new Error("native close failed");
|
|
const statFailure = new Error("descriptor stat failed");
|
|
const cancellation = new Error("cancelled after open");
|
|
const controller = new AbortController();
|
|
const file = {
|
|
fd: 17,
|
|
stat: vi.fn(async () => {
|
|
if (failureCase === "cancellation after open") {
|
|
controller.abort(cancellation);
|
|
return { isFile: () => true } as Stats;
|
|
}
|
|
if (failureCase === "non-regular metadata")
|
|
return {
|
|
isFile: () => false,
|
|
isDirectory: () => false,
|
|
} as Stats;
|
|
throw statFailure;
|
|
}),
|
|
close: vi.fn(async () => {
|
|
file.fd = -1;
|
|
throw closeFailure;
|
|
}),
|
|
};
|
|
mockOpen.mockResolvedValue(file as unknown as FileHandle);
|
|
|
|
const cause = await openRegularFile("/selected/input", {
|
|
symlinks: "reject",
|
|
signal: controller.signal,
|
|
}).catch((failure: unknown) => failure);
|
|
|
|
expect(cause).toBeInstanceOf(RegularFileAdmissionFailure);
|
|
if (!(cause instanceof RegularFileAdmissionFailure))
|
|
throw new Error("Expected an admission failure that retains its owner");
|
|
expect(cause.owner.handle).toBe(file);
|
|
expect(cause.cleanupCause).toBe(closeFailure);
|
|
expect(file.close).toHaveBeenCalledTimes(1);
|
|
await expect(cause.owner.close()).rejects.toBe(closeFailure);
|
|
expect(file.close).toHaveBeenCalledTimes(1);
|
|
|
|
if (failureCase === "non-regular metadata")
|
|
expect(cause.cause).toBeInstanceOf(NonRegularFileReadError);
|
|
else if (failureCase === "stat failure")
|
|
expect(cause.cause).toBe(statFailure);
|
|
else expect(cause.cause).toBe(cancellation);
|
|
},
|
|
);
|
|
|
|
it("compares available nanosecond timestamps without rounding away source changes", () => {
|
|
const before = {
|
|
dev: 1n,
|
|
ino: 2n,
|
|
size: 3n,
|
|
mode: 0o100400n,
|
|
mtimeMs: 10n,
|
|
ctimeMs: 20n,
|
|
mtimeNs: 10_000_001n,
|
|
ctimeNs: 20_000_001n,
|
|
isFile: () => true,
|
|
} as BigIntStats;
|
|
expect(sameRegularFileState(before, { ...before })).toBe(true);
|
|
expect(
|
|
sameRegularFileState(before, { ...before, mtimeNs: before.mtimeNs + 1n }),
|
|
).toBe(false);
|
|
expect(
|
|
sameRegularFileState(before, { ...before, ctimeNs: before.ctimeNs + 1n }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
["EACCES", -13, "keeps"],
|
|
["ENXIO", -6, "reclassifies"],
|
|
] as const)(
|
|
"%s from opening a non-regular path: admission %s the failure",
|
|
async (code, errno, outcome) => {
|
|
const failure = Object.assign(new Error(`open ${code}`), { code, errno });
|
|
mockOpen.mockRejectedValue(failure);
|
|
// The selected path itself is a directory that stat can still describe.
|
|
const cause = await openRegularFile(tmpdir(), { symlinks: "follow" }).catch(
|
|
(rejected: unknown) => rejected,
|
|
);
|
|
if (outcome === "keeps") expect(cause).toBe(failure);
|
|
else expect(cause).toBeInstanceOf(NonRegularFileReadError);
|
|
},
|
|
);
|
|
|
|
it("keeps cancellation that arrives during the non-regular metadata probe", async () => {
|
|
const controller = new AbortController();
|
|
const cancellation = new Error("cancelled during probe");
|
|
mockOpen.mockImplementation(async () => {
|
|
// The probe stats the path after this rejection; abort before it settles.
|
|
queueMicrotask(() => controller.abort(cancellation));
|
|
throw Object.assign(new Error("open ENXIO"), { code: "ENXIO", errno: -6 });
|
|
});
|
|
await expect(
|
|
openRegularFile(tmpdir(), {
|
|
symlinks: "follow",
|
|
signal: controller.signal,
|
|
}),
|
|
).rejects.toBe(cancellation);
|
|
});
|