mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(artifacts): mark Windows host executables by extension (#1823)
Windows has no POSIX execute bit; Node reports file modes from the read-only attribute alone, so every host file, including PE images, reported `executable: false` while the same occurrence reported `artifact_kind: executable`. Derive the host executable marking once: POSIX permission bits stay authoritative, and Windows marks regular files with the CreateProcess/cmd launchable extensions. Fixes #1819 Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
76e4153d34
commit
91f843bf38
@@ -14,6 +14,7 @@ import {
|
||||
type ArtifactEntry,
|
||||
type ArtifactReader,
|
||||
} from "./ArtifactReader.js";
|
||||
import { hostExecutableMarking } from "./hostExecutableMarking.js";
|
||||
import { OwnedDirectoryHandle } from "../filesystem/OwnedDirectoryHandle.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
|
||||
@@ -270,7 +271,7 @@ const artifactEntry = (
|
||||
kind,
|
||||
declaredSize: kind === "file" ? metadata.size : null,
|
||||
compressedSize: null,
|
||||
executable: (metadata.mode & 0o111) !== 0,
|
||||
executable: hostExecutableMarking(absolute, metadata),
|
||||
encrypted: false,
|
||||
byteOffset: null,
|
||||
declaredSha256: null,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { hostExecutableMarking } from "./hostExecutableMarking.js";
|
||||
|
||||
const file = (mode: number) => ({ mode, isFile: () => true });
|
||||
const directory = (mode: number) => ({ mode, isFile: () => false });
|
||||
|
||||
describe("hostExecutableMarking", () => {
|
||||
it("marks Windows launchable files by extension because NTFS has no execute bit", () => {
|
||||
expect(
|
||||
hostExecutableMarking("C:\\scratch\\whoami.exe", file(0o666), "win32"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hostExecutableMarking("C:\\scratch\\SETUP.CMD", file(0o444), "win32"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hostExecutableMarking("C:\\scratch\\library.dll", file(0o666), "win32"),
|
||||
).toBe(false);
|
||||
expect(
|
||||
hostExecutableMarking(
|
||||
"C:\\scratch\\tools.exe",
|
||||
directory(0o666),
|
||||
"win32",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps POSIX permission bits authoritative regardless of extension", () => {
|
||||
expect(hostExecutableMarking("/scratch/tool", file(0o755), "linux")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
hostExecutableMarking("/scratch/whoami.exe", file(0o644), "darwin"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Stats } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
|
||||
/** Extensions Windows launches directly through CreateProcess or cmd.exe. */
|
||||
const windowsLaunchableExtensions = new Set([".bat", ".cmd", ".com", ".exe"]);
|
||||
|
||||
/**
|
||||
* Whether a host filesystem entry carries the host's executable marking.
|
||||
*
|
||||
* POSIX hosts mark executability with permission bits. Windows has no execute
|
||||
* bit (Node reports file modes from the read-only attribute alone), so it marks
|
||||
* launchable files by extension instead.
|
||||
*/
|
||||
export const hostExecutableMarking = (
|
||||
path: string,
|
||||
metadata: Pick<Stats, "mode" | "isFile">,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): boolean =>
|
||||
platform === "win32"
|
||||
? metadata.isFile() &&
|
||||
windowsLaunchableExtensions.has(extname(path).toLowerCase())
|
||||
: (metadata.mode & 0o111) !== 0;
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
artifactManifestId,
|
||||
} from "../../domain/artifactIdentity.js";
|
||||
import { classifyAndHashRootForInventory } from "./classify.js";
|
||||
import { hostExecutableMarking } from "../hostExecutableMarking.js";
|
||||
import type { HashResult } from "../ArtifactHash.js";
|
||||
import {
|
||||
hashStableRootArtifact,
|
||||
@@ -211,7 +212,7 @@ const buildInventorySnapshot = async (
|
||||
rekeyOccurrences(rootNode.artifact_id, input.occurrences);
|
||||
const rootOccurrence = rootOccurrenceFor(rootNode, {
|
||||
size: metadata.size,
|
||||
executable: (metadata.mode & 0o111) !== 0,
|
||||
executable: hostExecutableMarking(path, metadata),
|
||||
format: rootFormat,
|
||||
path,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user