refactor(apple): normalize host paths at the filesystem boundary
This commit is contained in:
1 parent
fe3cac8aa8
commit
fb999f84b8
4 files changed
+103
-9
No files matched your search
@@ -17,6 +17,10 @@ import {
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { DyldSharedCache } from "./DyldSharedCacheReader.js";
|
||||
import {
|
||||
filesystemDyldLinkTarget,
|
||||
filesystemDyldLookupPath,
|
||||
} from "./FilesystemDyldPaths.js";
|
||||
import { hasMachoMagic, readMachoImage } from "./MachoLoadCommandReader.js";
|
||||
|
||||
const HASH_CHUNK_BYTES = 1024 * 1024;
|
||||
@@ -63,11 +67,14 @@ class FilesystemTreeView implements DylibTreeView {
|
||||
|
||||
async #readEntry(path: string): Promise<DylibTreeEntry | undefined> {
|
||||
cancelled(this.signal);
|
||||
const absolute = join(this.root, path);
|
||||
const absolute = filesystemDyldLookupPath(this.root, path);
|
||||
try {
|
||||
const metadata = await lstat(absolute);
|
||||
if (metadata.isSymbolicLink())
|
||||
return { kind: "symlink", target: await readlink(absolute) };
|
||||
return {
|
||||
kind: "symlink",
|
||||
target: filesystemDyldLinkTarget(await readlink(absolute)),
|
||||
};
|
||||
if (metadata.isDirectory()) return { kind: "directory" };
|
||||
return metadata.isFile() ? { kind: "file" } : undefined;
|
||||
} catch (cause: unknown) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { resolveTreePath } from "../../domain/apple/dyldPaths.js";
|
||||
import type { DylibTreeEntry } from "../../domain/apple/dylibResolution.js";
|
||||
import {
|
||||
filesystemDyldLinkTarget,
|
||||
filesystemDyldLookupPath,
|
||||
} from "./FilesystemDyldPaths.js";
|
||||
|
||||
it("normalizes Windows relative links before containment checks without probing an escaped path", async () => {
|
||||
const requested: string[] = [];
|
||||
const result = await resolveTreePath(
|
||||
{
|
||||
entry: async (path: string): Promise<DylibTreeEntry | undefined> => {
|
||||
requested.push(path);
|
||||
return path === "link"
|
||||
? {
|
||||
kind: "symlink",
|
||||
target: filesystemDyldLinkTarget("..\\outside.dylib", "win32"),
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
image: async () => ({ status: "not-mach-o" }),
|
||||
},
|
||||
"link",
|
||||
);
|
||||
expect(result).toEqual({ kind: "escapes" });
|
||||
expect(requested).toEqual(["link"]);
|
||||
});
|
||||
|
||||
it("keeps POSIX backslash bytes and preserves Windows dot segments for segment-wise traversal", () => {
|
||||
expect(filesystemDyldLinkTarget("..\\outside.dylib", "darwin")).toBe(
|
||||
"..\\outside.dylib",
|
||||
);
|
||||
expect(filesystemDyldLinkTarget("nested\\..\\target", "win32")).toBe(
|
||||
"nested/../target",
|
||||
);
|
||||
expect(filesystemDyldLinkTarget("C:\\outside", "win32")).toBe("/C:/outside");
|
||||
expect(filesystemDyldLinkTarget("C:/literal-posix-directory", "darwin")).toBe(
|
||||
"C:/literal-posix-directory",
|
||||
);
|
||||
expect(() => filesystemDyldLinkTarget("C:drive-relative", "win32")).toThrow(
|
||||
"Windows drive working directory",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not let Windows reinterpret a literal backslash-bearing loader name as traversal", () => {
|
||||
expect(() =>
|
||||
filesystemDyldLookupPath("C:\\root", "..\\outside.dylib", "win32"),
|
||||
).toThrow("cannot be represented on Windows");
|
||||
expect(
|
||||
filesystemDyldLookupPath("/root", "literal\\name.dylib", "darwin"),
|
||||
).toBe("/root/literal\\name.dylib");
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { posix, win32 } from "node:path";
|
||||
import { ArtifactReaderFailure } from "../ArtifactReader.js";
|
||||
|
||||
/**
|
||||
* Convert host filesystem separators into the tree view's POSIX traversal
|
||||
* namespace. Dot segments stay intact so they apply after symlink expansion;
|
||||
* POSIX backslash filename bytes are never reinterpreted as separators.
|
||||
*/
|
||||
export const filesystemDyldLinkTarget = (
|
||||
reported: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string => {
|
||||
if (platform !== "win32") return reported;
|
||||
const portable = reported.replaceAll("\\", "/");
|
||||
if (win32.isAbsolute(reported))
|
||||
return portable.startsWith("/") ? portable : `/${portable}`;
|
||||
if (/^[A-Za-z]:/u.test(reported))
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
`Drive-relative symlink target depends on a Windows drive working directory: ${reported}`,
|
||||
);
|
||||
return portable;
|
||||
};
|
||||
|
||||
/** Map a portable lookup to the host without allowing extra native separators to escape it. */
|
||||
export const filesystemDyldLookupPath = (
|
||||
root: string,
|
||||
lookup: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string => {
|
||||
if (platform === "win32" && lookup.includes("\\"))
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
`Literal backslashes in a POSIX dyld lookup cannot be represented on Windows: ${lookup}. Analyze this tree on a POSIX host.`,
|
||||
);
|
||||
return platform === "win32"
|
||||
? win32.join(root, lookup)
|
||||
: posix.join(root, lookup);
|
||||
};
|
||||
@@ -37,13 +37,8 @@ export const resolveTreePath = async (
|
||||
if (entry.kind === "symlink") {
|
||||
hops += 1;
|
||||
if (hops > MAX_SYMLINK_HOPS) return { kind: "absent" };
|
||||
if (
|
||||
entry.target.startsWith("/") ||
|
||||
/^[a-zA-Z]:[\\/]/u.test(entry.target) ||
|
||||
entry.target.startsWith("\\\\") ||
|
||||
entry.target.startsWith("//")
|
||||
)
|
||||
return { kind: "escapes" };
|
||||
// Tree providers convert host link syntax to this POSIX namespace.
|
||||
if (entry.target.startsWith("/")) return { kind: "escapes" };
|
||||
pending.unshift(...segmentsOf(entry.target));
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user