Files
reverse-engineering-anything/src/domain/webScriptExportPlan.ts
T
morlutoandCursor Agent b4db35f092 fix: bind identity and coverage to the producing rule (#1513)
* fix: bind identity and coverage to the producing rule

Logical paths, container families, examined bytes, path-segment conventions, destination collisions, file-id leases, Mach-O CPU selection, and firmware cleanup now follow the observation that owns each rule.

* fix: fail unmatched Mach-O slices and keep firmware roots independent

A fat target whose architecture lipo did not list now stops before slice tools run. Chained fixups still count unread LC_DYLD_INFO rebase bytes, and a firmware cleanup failure no longer blocks the next workspace.

* test: cover path identity and runtime families at the projection boundary

NFC joins and segment conventions are now asserted by the path resolver, the Apple projection, and a real APK inventory instead of helper unit tests.

* test: expect unread dyld rebase bytes in the Apple dispatch fixture

The legacy LC_DYLD_INFO fixture keeps a rebase range this reader does not decode, so the public CLI and MCP check now requires that partial coverage beside the bind opcodes.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-10 17:14:47 +08:00

133 lines
4.2 KiB
TypeScript

import { createHash } from "node:crypto";
import type {
CapturedWebScript,
ExportedWebScript,
} from "./webScriptExport.js";
const hash = (text: string): string =>
createHash("sha256").update(text).digest("hex");
const PORTABLE_SEGMENT = /^[A-Za-z0-9_~.-]+$/u;
const RESERVED_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu;
/** One planned file, or an explicit missing source with no filesystem effect. */
export interface PlannedWebScript {
readonly script: CapturedWebScript;
readonly record: ExportedWebScript;
}
/** Preserve safe URL layouts only when every captured path is unambiguous. */
export const planWebScriptExport = (
scripts: readonly CapturedWebScript[],
): readonly PlannedWebScript[] => {
const candidates = scripts.map(candidatePath);
const conflicts = conflictingPaths(candidates);
return scripts.map((script, index) => {
if (script.content.state === "unavailable")
return {
script,
record: {
source: script.source,
url: script.url,
content: script.content,
},
};
const candidate = candidates[index];
const reason = conflicts.has(index)
? "Captured paths collide by content version or file/directory prefix."
: (candidate?.reason ?? null);
const path = reason === null ? candidate?.path : undefined;
return {
script,
record: {
source: script.source,
url: script.url,
content: {
state: "exported",
relative_path:
path ??
`isolated/source-${index + 1}-${hash(JSON.stringify(script.source))}.js`,
layout: path === undefined ? "isolated" : "url-path",
layout_reason: reason,
sha256: script.content.sha256,
bytes: script.content.bytes.length,
media_type: script.content.media_type,
redacted: script.content.redacted,
representation: script.content.representation,
},
},
};
});
};
interface CandidatePath {
readonly path?: string;
readonly reason: string | null;
}
const candidatePath = (script: CapturedWebScript): CandidatePath => {
let url: URL;
try {
url = new URL(script.url);
} catch {
return {
reason: "Source has no absolute HTTP URL (for example an inline script).",
};
}
if (url.protocol !== "http:" && url.protocol !== "https:")
return { reason: "Source URL scheme has no local module layout mapping." };
const parts = url.pathname.slice(1).split("/");
if (
!/\.(?:js|mjs|cjs)$/u.test(url.pathname) ||
parts.some(
(part) =>
!PORTABLE_SEGMENT.test(part) ||
part === "." ||
part === ".." ||
part.endsWith(".") ||
RESERVED_SEGMENT.test(part),
)
)
return {
reason: "URL path is not an ordinary portable JavaScript module path.",
};
return {
path: `origins/${hash(url.origin)}/${parts.join("/")}`,
reason:
url.search !== "" ||
url.hash !== "" ||
url.username !== "" ||
url.password !== ""
? "URL query, fragment, or credentials cannot select a canonical local module."
: null,
};
};
// Register every exact prefix so a repeated path, or a file that is also a
// directory prefix, isolates every affected source. Case-distinct spellings
// stay distinct until the destination filesystem reports a collision.
const conflictingPaths = (
candidates: readonly CandidatePath[],
): Set<number> => {
const prefixes = new Map<string, { indices: number[]; files: number[] }>();
candidates.forEach(({ path }, index) => {
if (path === undefined) return;
const parts = path.split("/");
parts.forEach((_, position) => {
const spelling = parts.slice(0, position + 1).join("/");
const entry = prefixes.get(spelling) ?? { indices: [], files: [] };
entry.indices.push(index);
if (position === parts.length - 1) entry.files.push(index);
prefixes.set(spelling, entry);
});
});
const conflicts = new Set<number>();
for (const entry of prefixes.values())
if (
entry.files.length > 1 ||
(entry.files.length > 0 && entry.indices.length > entry.files.length)
)
for (const index of entry.indices) conflicts.add(index);
return conflicts;
};