fix(javascript): preserve runtime path bytes and Electron ownership (#4941)

* fix(javascript): preserve literal percent runtime paths

* fix(javascript): ignore resolve prefixes before the final anchor

* fix(electron): retain application roots for loadFile declarations

* fix(electron): resolve loadFile from enclosing application package

* fix(electron): retain outer application root across package scopes

* fix(electron): derive application ownership from inventoried packages

* fix(electron): retain resolved package entrypoint ownership

* fix(electron): preserve competing application ownership

* fix(electron): retain invalid and omitted runtime ownership

* fix(electron): preserve omitted module ownership and runtime paths

* fix(electron): preserve proven roots before confining page paths

* fix(electron): retain possible owners behind missing imports

* fix(electron): distinguish absent imports and owned page candidates

* fix(electron): keep unavailable JSON out of owner gaps

* fix(electron): retain exact loader formats and default entry status

* fix(electron): retain unavailable entrypoint ownership candidates

* test(ghidra): preserve Windows Node probe prerequisites

* test(ghidra): assert cancellation during profile shutdown

* fix(electron): separate examined imports from frontend ownership

* fix(electron): retain unavailable directory entrypoint owners

* test(jeb): retain Linux ownership prerequisite in isolation

* fix(javascript): retain unchecked loader ownership and literal main paths

* fix(javascript): preserve runtime paths and call-site module aliases

* fix(javascript): bound module alias traversal and preserve projections

* fix(javascript): retain host module and nested artifact path boundaries

* fix(javascript): preserve ESM and archive module loader provenance

* test(javascript): distinguish unmapped containers from missing exports

* fix: retain default module aliases and Electron builtin imports

* fix: preserve host file URL and inward ASAR ownership

* fix: preserve CommonJS file URL classification in CI fixtures

* fix: retain bound module loaders and Windows entrypoint uncertainty

* fix: retain ASAR uncertainty and invalidate changed Electron app roots

* fix(javascript): retain ownership uncertainty across loader boundaries

* fix(javascript): preserve loader sources and bound alias queries

* fix(javascript): retain destructured loader sources

* fix(javascript): preserve loader uncertainty at resolution boundaries

* fix(javascript): preserve callable and package launch ownership

* fix(javascript): preserve captured values across member reads

* test(process): assert tagged filesystem cancellation failures

* fix(javascript): preserve inventory bases and invocation read timing

* fix(javascript): preserve ownership across indirect and unread loads

* test(javascript): order fixture directory creation on Windows

* test(javascript): share ordered filesystem fixture writer

* fix(javascript): unify loader provenance and package validation

* fix(javascript): preserve captured Node require loader values

* fix(javascript): preserve loader invocation authority

* fix(javascript): verify generic loader invocation authority

* fix(javascript): preserve conditional loader method alternatives

* fix(javascript): require proven factories for source association

* fix(javascript): honor replaced Electron app path setters

* fix(javascript): prove path and assignment source ownership

* fix(javascript): retain native delegate eligibility through shared writes

* fix(process): settle stale baseline errors after confirmed absence

* fix(javascript): preserve assignment and path boundary evidence

* fix(javascript): prove source-directory resolve anchors

* fix: preserve application path provenance and portable schemas

* fix(javascript): retain CommonJS exported setter effects

* fix: isolate Linux identity observations and retain native module exports

* fix(javascript): preserve published app and helper provenance

* fix(javascript): normalize typed stores and native path fixtures
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-10-20 17:25:19 +08:00
1 parent d532b18123
commit e9fc8fb30c
149 files changed
+16881 -988

No files matched your search

@@ -1,5 +1,6 @@
import { isBuiltin } from "node:module";
import { posix } from "node:path";
import { posix, win32 } from "node:path";
import { ELECTRON_MODULE } from "../../domain/javascript/javascriptElectronMemberWrites.js";
import {
normalizeJoinedLogicalPath,
@@ -27,6 +28,7 @@ import {
type TypeScriptEmittedSourceCandidate,
} from "../TypeScriptEmittedSourceCandidates.js";
import { isRelativeJavaScriptModuleSpecifier } from "../../domain/javascript/javascriptModuleSpecifier.js";
import { selectPackageSubpathPattern } from "../../domain/javascript/javascriptPackageSubpathPatterns.js";
import type { JavaScriptModuleResolutionMode } from "../../domain/javascript/javascriptModuleResolutionMode.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js";
@@ -46,6 +48,8 @@ type UnresolvedArtifactPathStatus =
interface ArtifactPathResolutionBase {
readonly declared_path: string;
readonly runtime_path_base?: "application-root";
readonly runtime_path_base_proven?: true;
readonly resolution_context: ArtifactPathResolutionContext;
readonly limitations: readonly string[];
readonly conditional_resolutions?: readonly ConditionalModuleResolution[];
@@ -87,7 +91,14 @@ export interface ResolveArtifactPathInput {
/** Import/require semantics; runtime resource paths omit this provenance. */
readonly moduleKind?: JavaScriptModuleResolutionMode;
/** Runtime paths retain their loader owner independently of source imports. */
readonly runtimePathKind?: "exact-file" | "node-entrypoint" | "url-resource";
readonly runtimePathKind?:
| "exact-file"
| "application-file"
| "node-entrypoint"
| "url-resource";
/** Logical root of an inventoried nested application container. */
readonly applicationRootPath?: string | null;
readonly applicationRootIsProven?: boolean;
}
/** Preserve an explicitly retained loader mode on graph relationships. */
@@ -193,6 +204,14 @@ export const resolveArtifactPathByContext = (
const resolution = resolvePath(input);
const facts = {
declared_path: input.declaredPath,
...(input.runtimePathKind === "application-file"
? { runtime_path_base: "application-root" as const }
: {}),
...(input.runtimePathKind === "application-file" &&
input.applicationRootIsProven === true &&
input.applicationRootPath !== null
? { runtime_path_base_proven: true as const }
: {}),
resolution_context: input.context,
limitations: resolution.limitations,
...(resolution.candidatePath === undefined
@@ -215,6 +234,12 @@ export const resolveArtifactPathByContext = (
export const artifactPathResolutionProperties = (
resolution: ArtifactPathResolution | undefined,
): Readonly<Record<string, JsonValue>> => ({
...(resolution?.runtime_path_base_proven === undefined
? {}
: { runtime_path_base_proven: resolution.runtime_path_base_proven }),
...(resolution?.runtime_path_base === undefined
? {}
: { runtime_path_base: resolution.runtime_path_base }),
...(resolution?.candidate_path === undefined
? {}
: { candidate_path: resolution.candidate_path }),
@@ -248,7 +273,50 @@ const conditionalResolution = (
: { candidate_path: result.candidatePath }),
});
/** Host path admission is shared by top-level and metadata-derived inputs. */
const resolveRuntimeHostPath = (
input: ResolveArtifactPathInput,
): CandidateResolution | null => {
if (
(input.runtimePathKind !== undefined || isNodeFilesystemLookup(input)) &&
input.runtimePathKind !== "url-resource" &&
(posix.isAbsolute(input.declaredPath) ||
win32.isAbsolute(input.declaredPath) ||
(input.context === "module-specifier" &&
input.moduleKind !== "require" &&
/^file:/iu.test(input.declaredPath)))
) {
const esm =
input.context === "module-specifier" && input.moduleKind === "import";
if (esm) {
const normalized = URL.parse(
htmlUrlText(input.declaredPath),
PACKAGE_ARTIFACT_ROOT_URL,
);
if (normalized === null)
return unresolvedCandidate("rejected", [
"The module URL cannot be parsed.",
]);
const admitted = decodeModuleUrlPath(normalized.pathname);
if (typeof admitted !== "string") return admitted;
}
// Host syntax is not a canonical logical path. In particular, Windows
// separators and drive letters must not be reclassified as invalid/URLs.
return input.declaredPath.includes("\0") ||
(!esm && fileUrlPath(input.declaredPath) === null)
? unresolvedCandidate("rejected", [
"NUL or unsafe file URL path syntax is not admitted in a runtime path.",
])
: unresolvedCandidate("unavailable", [
"An absolute runtime host path has no established mapping to the selected logical artifact root.",
]);
}
return null;
};
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
const hostPath = resolveRuntimeHostPath(input);
if (hostPath !== null) return hostPath;
if (input.runtimePathKind === "node-entrypoint")
return resolveNodeRuntimeEntrypoint(input);
if (input.moduleKind === "compiler-dependent") {
@@ -286,7 +354,11 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
}
if (input.runtimePathKind === "url-resource")
return resolveExactUrlResource(input);
if (input.runtimePathKind === "exact-file" && isRuntimePathDeclaration(input))
if (
(input.runtimePathKind === "exact-file" ||
input.runtimePathKind === "application-file") &&
isRuntimePathDeclaration(input)
)
return resolveRuntimeEntrypoint(input);
// Package import keys are literal lookup identities, not artifact paths.
// The selected target is admitted separately by resolveExportTarget.
@@ -365,18 +437,37 @@ const isRuntimePathDeclaration = (input: ResolveArtifactPathInput): boolean =>
const resolveNodeRuntimeEntrypoint = (
input: ResolveArtifactPathInput,
): CandidateResolution => {
const { runtimePathKind: _runtimePathKind, ...sourceInput } = input;
const runtimeInput = { ...sourceInput, moduleKind: "require" as const };
if (!isRuntimePathDeclaration(runtimeInput)) return resolvePath(runtimeInput);
const windowsPath = unknownWindowsRuntimePath(input.declaredPath);
if (windowsPath !== null) return windowsPath;
const runtimeInput = { ...input, moduleKind: "require" as const };
if (input.context === "package-entrypoint" && input.declaredPath === "")
return resolveNodePackageMain(
runtimeInput,
posix.dirname(input.sourcePath),
undefined,
);
if (
runtimeInput.context !== "package-entrypoint" &&
!isRuntimePathDeclaration(runtimeInput)
) {
const { runtimePathKind: _runtimePathKind, ...moduleInput } = runtimeInput;
return resolvePath(moduleInput);
}
const candidate = contextualCandidate(runtimeInput);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(candidate);
if (typeof confined !== "string") return confined;
// Node entrypoints use runtime file/directory probes. They do not originate
// in emitted TypeScript import syntax and cannot select a source substitute.
const resolution = resolveCandidate(runtimeInput, confined, new Set());
// Runtime entrypoint analysis has no extension or directory fallback, but
// retain the normalized loader path when that exact target is absent.
const resolution =
input.context === "package-entrypoint"
? resolveNodePackageMain(
runtimeInput,
posix.dirname(input.sourcePath),
confined,
)
: resolveCandidate(runtimeInput, confined, new Set());
// Retain the normalized declaration when Node's owned probes are exhausted.
return resolution.status === "not-found" &&
resolution.candidatePath === undefined
? { ...resolution, candidatePath: confined }
@@ -386,14 +477,30 @@ const resolveNodeRuntimeEntrypoint = (
const resolveRuntimeEntrypoint = (
input: ResolveArtifactPathInput,
): CandidateResolution => {
const rejected = rejectDeclaration(input.declaredPath);
if (input.runtimePathKind === "application-file") {
const windowsPath = unknownWindowsRuntimePath(input.declaredPath);
if (windowsPath !== null) return windowsPath;
}
const rejected = rejectDeclaration(
input.declaredPath,
declaresLiteralPercent(input),
);
if (rejected !== null) return rejected;
if (hasScheme(input.declaredPath) || looksExternal(input.declaredPath))
return unresolvedCandidate("external", [
"Electron runtime entrypoints must identify local paths, not external URLs.",
]);
if (
input.runtimePathKind === "application-file" &&
input.applicationRootPath === null
)
return unresolvedCandidate("unavailable", [
"Inventoried package metadata does not establish one application root for this source; the loadFile base is unknown.",
]);
const candidate = confineCandidate(
relativeCandidate(input.sourcePath, input.declaredPath),
input.runtimePathKind === "application-file"
? posix.join(input.applicationRootPath ?? "", input.declaredPath)
: relativeCandidate(input.sourcePath, input.declaredPath),
);
if (typeof candidate !== "string") return candidate;
return (
@@ -408,6 +515,17 @@ const resolveRuntimeEntrypoint = (
);
};
/** Host filesystem spellings need target-platform context before admission. */
const unknownWindowsRuntimePath = (
declared: string,
): CandidateResolution | null =>
!declared.includes("\0") &&
(declared.includes("\\") || /^[a-z]:/iu.test(declared))
? unresolvedCandidate("unavailable", [
"Windows-relative runtime paths require target-platform and drive context that the selected logical artifact inventory does not establish.",
])
: null;
const resolveRuntimeSourceCandidates = (
input: ResolveArtifactPathInput,
exactPath: string,
@@ -453,14 +571,19 @@ const rejectDeclaration = (
};
/**
* Node's CommonJS loader never percent-decodes a request or a legacy package
* main, so `%2e` and `%2f` there are literal filename bytes, not encoded dots
* or separators. Root confinement still applies to the joined candidate.
* Filesystem expressions, exact runtime paths, and CommonJS requests retain
* literal filename bytes. URL-to-path conversion belongs to the producer;
* these consumers must not decode or reject the resulting percent spelling.
* Root confinement still applies to the joined candidate.
*/
const declaresLiteralPercent = (input: ResolveArtifactPathInput): boolean =>
input.moduleKind === "require" &&
(input.context === "module-specifier" ||
input.context === "package-entrypoint");
input.context === "filesystem-expression" ||
(input.runtimePathKind === "exact-file" &&
isRuntimePathDeclaration(input) &&
!looksExternal(input.declaredPath)) ||
(input.moduleKind === "require" &&
(input.context === "module-specifier" ||
input.context === "package-entrypoint"));
const contextualCandidate = (
input: ResolveArtifactPathInput,
@@ -497,6 +620,15 @@ const contextualCandidate = (
"File URL authorities or invalid file URL forms cannot be mapped to artifact-local paths.",
]);
}
if (
context === "module-specifier" &&
input.moduleKind === "require" &&
declared.includes("\\") &&
(!hasScheme(declared) || /^[a-z]:/iu.test(declared))
) {
const windowsPath = unknownWindowsRuntimePath(declared);
if (windowsPath !== null) return windowsPath;
}
const hasUrlPathSemantics =
context === "module-specifier" && input.moduleKind === "import";
if (hasUrlPathSemantics) declared = htmlUrlText(declared);
@@ -516,15 +648,59 @@ const contextualCandidate = (
)
return bareModuleCandidate(input);
if (input.moduleKind !== "require") {
if (/%(?:2f|5c)/iu.test(stripQueryAndFragment(declared)))
return unresolvedCandidate("rejected", [
"ES module URL paths cannot contain percent-encoded slash or backslash bytes.",
]);
if (hasUrlPathSemantics) {
// URL normalization can leave and then re-enter the synthetic namespace.
// Preserve the shared URL owner's above-root markers before that loss.
if (
applyUrlReference(
urlDirectoryState(posix.dirname(input.sourcePath)),
stripQueryAndFragment(declared),
true,
).escapes > 0
)
return unresolvedCandidate("rejected", [
"The module URL escapes the selected logical artifact root.",
]);
const sourceUrl = packageMetadataUrl(input.sourcePath, declared);
const base = sourceUrl.ok
? sourceUrl.value
: sourceUrl.error.structuralUrl;
const normalized = URL.parse(declared, base);
if (normalized === null)
return unresolvedCandidate("rejected", [
"The module URL cannot be parsed.",
]);
if (!normalized.pathname.startsWith(PACKAGE_ARTIFACT_ROOT_URL.pathname))
return unresolvedCandidate("rejected", [
"The module URL escapes the selected logical artifact root.",
]);
// Node validates the complete normalized file URL, including encoded
// separator bytes inherited from an inventoried source directory.
const decoded = decodeModuleUrlPath(
normalized.pathname.slice(PACKAGE_ARTIFACT_ROOT_URL.pathname.length),
);
if (typeof decoded !== "string") return decoded;
if (
!sourceUrl.ok &&
decoded
.split("/")
.some((segment) => sourceUrl.error.structuralSegments.has(segment))
)
return unresolvedCandidate("unavailable", [
sourceUrl.error.limitation,
]);
const candidate = urlRelativeCandidate("", `/${decoded}`);
return typeof candidate === "string"
? exactUrlPathCandidate(decoded, candidate, "module URL")
: candidate;
}
const decoded = decodeModuleUrlPath(stripQueryAndFragment(declared));
if (typeof decoded !== "string") return decoded;
declared = decoded;
}
} else if (looksExternal(declared))
} else if (context !== "package-entrypoint" && looksExternal(declared))
// Package main is a filesystem declaration. Scheme-like filename bytes
// remain literal; URI classification belongs to module and URL owners.
return unresolvedCandidate("external", [
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
]);
@@ -574,6 +750,10 @@ const contextualCandidate = (
const decodeModuleUrlPath = (
declared: string,
): string | CandidateResolution => {
if (/%(?:2f|5c)/iu.test(declared))
return unresolvedCandidate("rejected", [
"ES module URL paths cannot contain percent-encoded slash or backslash bytes.",
]);
let decoded: string;
try {
decoded = decodeURIComponent(declared);
@@ -618,17 +798,19 @@ const urlDirectoryState = (directory: string): UrlPathState => ({
const applyUrlReference = (
base: UrlPathState,
reference: string,
encodedDotSegments = false,
): UrlPathState => {
const absolute = reference.startsWith("/");
const segments = absolute ? [] : [...base.segments];
let escapes = absolute ? 0 : base.escapes;
const parts = (absolute ? reference.slice(1) : reference).split("/");
for (const [index, part] of parts.entries()) {
if (part !== "." && part !== "..") {
const dot = encodedDotSegments ? part.replace(/%2e/giu, ".") : part;
if (dot !== "." && dot !== "..") {
segments.push(part);
continue;
}
if (part === "..")
if (dot === "..")
if (segments.length > 0) segments.pop();
else escapes += 1;
if (index === parts.length - 1) segments.push("");
@@ -704,7 +886,6 @@ const bareModuleCandidate = (
if (typeof confined !== "string") return confined;
dependencyDirectory = posix.dirname(confined);
}
const source = input.files.get(input.sourcePath);
let nearestNotFound: CandidateResolution | null = null;
for (const directory of packageLookupDirectories(input)) {
const candidate = posix.join(directory, dependencyDirectory);
@@ -716,14 +897,7 @@ const bareModuleCandidate = (
(!request.dependencyExportsEligible ||
metadata.exports === undefined)));
const packageResolution =
metadata.kind !== "absent" ||
hasContainerDirectory(
input.files,
candidate,
source?.container_sha256,
input.directoryPaths,
input.unanalyzedFiles,
)
metadata.kind !== "absent" || hasContainerDirectory(input, candidate)
? resolvePackageDependency(input, candidate, request, metadata)
: request.packageSubpath === "." && input.moduleKind !== "import"
? resolveFileCandidates(
@@ -795,10 +969,6 @@ const resolvePackageImportAlias = (
return unresolvedCandidate("rejected", [
`The package import specifier ${JSON.stringify(specifier)} is invalid; "#" and specifiers that end with "/" are not valid import aliases.`,
]);
if (classified.kind === "pattern")
return unresolvedCandidate("external", [
`Wildcard and directory package import aliases remain unresolved; ${JSON.stringify(specifier)} is not an exact imports key.`,
]);
const metadata = enclosingPackageMetadata(input);
if (metadata.kind === "none")
return unresolvedCandidate("external", [
@@ -816,7 +986,7 @@ const resolvePackageImportAlias = (
`Package metadata ${metadata.path} does not define package import alias ${specifier}; it declares ${lookup.declared.join(", ") || "no imports keys"}.`,
]);
if (lookup.kind === "pattern-unresolved")
return unresolvedCandidate("external", [
return unresolvedCandidate("unavailable", [
`Wildcard package import aliases remain unresolved; package metadata ${metadata.path} has no exact imports key ${specifier}.`,
]);
const selected = selectExportTarget(
@@ -839,11 +1009,18 @@ const resolvePackageImportAlias = (
]);
if (selected.kind === "non-relative") {
const target = classifyPackageImportTargetString(selected.value);
return unresolvedCandidate("external", [
target.kind === "recursive"
? `Recursive package import aliases remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`
: `External-package import targets remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`,
]);
// Unsupported lookup may still reach a selected module and another launch
// owner. Only a known builtin establishes that no local imports follow.
return unresolvedCandidate(
isBuiltin(selected.value) || ELECTRON_MODULE.test(selected.value)
? "external"
: "unavailable",
[
target.kind === "recursive"
? `Recursive package import aliases remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`
: `External-package import targets remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`,
],
);
}
return resolveExportTarget(input, metadata.path, selected, "imports");
};
@@ -958,16 +1135,7 @@ const resolveLegacyEsmSubpath = (
return unresolvedCandidate("unavailable", [unrepresentableBase.limitation]);
const file = resolveFileCandidates(input, [candidate]);
if (file !== null) return file;
const source = input.files.get(input.sourcePath);
if (
hasContainerDirectory(
input.files,
candidate,
source?.container_sha256,
input.directoryPaths,
input.unanalyzedFiles,
)
)
if (hasContainerDirectory(input, candidate))
return unresolvedCandidate("rejected", [
`ESM package subpath ${candidate} names a directory; directory imports are unsupported and package main or index lookup was not attempted.`,
]);
@@ -998,6 +1166,10 @@ const resolvePackageExports = (
return unresolvedCandidate("external", [
`Directory package metadata ${packagePath} blocks subpath ${subpath} with ${exported.cause === "null" ? "an explicit null exports target" : "an empty exports target array"}.`,
]);
if (exported.status === "pattern-unresolved")
return unresolvedCandidate("unavailable", [
`Package metadata ${packagePath} selects wildcard exports key ${JSON.stringify(exported.pattern)} for subpath ${subpath}; wildcard target expansion remains unresolved.`,
]);
return resolveExportTarget(input, packagePath, exported);
};
@@ -1080,37 +1252,35 @@ const parseBarePackageRequest = (
};
const hasContainerDirectory = (
files: ReadonlyMap<string, JavaScriptArtifactFile>,
input: ResolveArtifactPathInput,
candidate: string,
containerSha256: string | undefined,
directoryPaths: ReadonlySet<string> | undefined,
unanalyzedFiles?: JavaScriptUnanalyzedArtifactLookup,
): boolean => {
if (directoryPaths?.has(candidate) === true) return true;
if (input.directoryPaths?.has(candidate) === true) return true;
if (
[...indexCandidates(candidate), posix.join(candidate, "package.json")].some(
(path) => {
const file = files.get(path);
const file = input.files.get(path);
return (
file !== undefined &&
(containerSha256 === undefined ||
file.container_sha256 === containerSha256)
admitsRuntimeContainer(input, path, file.container_sha256)
);
},
)
)
return true;
// Inventory has files rather than directory entries. Any same-container
// descendant establishes a package directory even without package.json.
// Inventory has files rather than directory entries. An admitted descendant
// establishes a package directory even without package.json.
const prefix = `${candidate}/`;
for (const [path, file] of files)
for (const [path, file] of input.files)
if (
path.startsWith(prefix) &&
(containerSha256 === undefined ||
file.container_sha256 === containerSha256)
admitsRuntimeContainer(input, path, file.container_sha256)
)
return true;
return unanalyzedFiles?.hasDirectory(candidate, containerSha256) ?? false;
const container = crossesRuntimeArchiveBoundary(input, `${candidate}/`)
? undefined
: input.files.get(input.sourcePath)?.container_sha256;
return input.unanalyzedFiles?.hasDirectory(candidate, container) ?? false;
};
const htmlCandidate = (
@@ -1228,12 +1398,12 @@ const readPackageMetadata = (
): PackageMetadata => {
const path = posix.join(directory, "package.json");
const file = input.files.get(path);
const source = input.files.get(input.sourcePath);
if (file === undefined) {
const unavailable = input.unanalyzedFiles?.get(path);
if (
source !== undefined &&
unavailable?.container_sha256 === source.container_sha256
unavailable !== undefined &&
(admitsRuntimeContainer(input, path, unavailable.container_sha256) ||
input.moduleKind !== undefined)
)
return {
kind: "unavailable",
@@ -1243,8 +1413,13 @@ const readPackageMetadata = (
};
return { kind: "absent" };
}
if (source !== undefined && file.container_sha256 !== source.container_sha256)
return { kind: "absent" };
if (!admitsRuntimeContainer(input, path, file.container_sha256))
return input.moduleKind === undefined
? { kind: "absent" }
: {
kind: "unavailable",
limitation: `Inventoried package metadata ${path} crosses an artifact container boundary without an established runtime loader mapping.`,
};
if (!file.text.included)
return {
kind: "unavailable",
@@ -1260,7 +1435,7 @@ const readPackageMetadata = (
limitation: `Directory package metadata ${path} is not valid package JSON.`,
};
}
if (typeof value !== "object" || value === null)
if (typeof value !== "object" || value === null || Array.isArray(value))
return {
kind: "unavailable",
limitation: `Directory package metadata ${path} is not valid package JSON.`,
@@ -1351,12 +1526,12 @@ const resolveDirectory = (
? metadata.module
: undefined;
if (entry === undefined)
return (
resolveFileCandidates(
input,
indexCandidates(directory, input.moduleKind),
) ?? notFoundCandidate()
);
return input.moduleKind !== undefined
? resolveNodePackageMain(input, directory, undefined)
: (resolveFileCandidates(
input,
indexCandidates(directory, input.moduleKind),
) ?? notFoundCandidate());
if (typeof entry !== "string" || entry.length === 0)
return unresolvedCandidate("unavailable", [
`Directory package metadata ${metadata.path} is not valid package JSON.`,
@@ -1374,6 +1549,12 @@ const resolveDirectory = (
: { unanalyzedFiles: input.unanalyzedFiles }),
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
};
const hostPath = resolveRuntimeHostPath(entryInput);
if (hostPath !== null) return hostPath;
if (entryInput.moduleKind !== undefined) {
const windowsPath = unknownWindowsRuntimePath(entry);
if (windowsPath !== null) return windowsPath;
}
const candidate = contextualCandidate(entryInput);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(candidate);
@@ -1397,55 +1578,47 @@ const resolveDirectory = (
const resolveNodePackageMain = (
input: ResolveArtifactPathInput,
directory: string,
mainPath: string,
mainPath: string | undefined,
): CandidateResolution => {
const normalizedMain = mainPath.replace(/\/$/u, "");
for (const suffix of NODE_PACKAGE_MAIN_SUFFIXES) {
const isDirectoryIndex = suffix.startsWith("/");
if (normalizedMain === "" && !isDirectoryIndex) continue;
const selected = resolveFileCandidates(input, [
isDirectoryIndex
? posix.join(normalizedMain, suffix.slice(1))
: posix.normalize(`${normalizedMain}${suffix}`),
]);
if (selected === null) continue;
if (input.moduleKind === "require") return selected;
// ESM probes the normalized main, then appends the selected suffix to
// the original declaration before finalizing the resulting file path.
const finalized = confineCandidate(
relativeCandidate(input.sourcePath, `${input.declaredPath}${suffix}`),
);
if (typeof finalized !== "string") return finalized;
const finalizedFile = resolveFileCandidates(input, [
finalized.replace(/\/$/u, ""),
]);
if (finalizedFile !== null) return finalizedFile;
const source = input.files.get(input.sourcePath);
if (
hasContainerDirectory(
input.files,
finalized,
source?.container_sha256,
input.directoryPaths,
input.unanalyzedFiles,
)
)
return {
...unresolvedCandidate("rejected", [
`The selected legacy ESM package main finalizes to directory ${finalized}; directory imports are unsupported and no later main or index candidate was tried.`,
]),
candidatePath: finalized,
};
return unresolvedCandidate("not-found", [
`The selected legacy ESM package main finalizes to ${finalized}, which is not an inventoried file; no later main or index candidate was tried.`,
]);
}
const normalizedMain = mainPath?.replace(/\/$/u, "");
if (normalizedMain !== undefined)
for (const suffix of NODE_PACKAGE_MAIN_SUFFIXES) {
const isDirectoryIndex = suffix.startsWith("/");
if (normalizedMain === "" && !isDirectoryIndex) continue;
const selected = resolveFileCandidates(input, [
isDirectoryIndex
? posix.join(normalizedMain, suffix.slice(1))
: posix.normalize(`${normalizedMain}${suffix}`),
]);
if (selected === null) continue;
if (input.moduleKind === "require") return selected;
// ESM probes the normalized main, then appends the selected suffix to
// the original declaration before finalizing the resulting file path.
const finalized = confineCandidate(
relativeCandidate(input.sourcePath, `${input.declaredPath}${suffix}`),
);
if (typeof finalized !== "string") return finalized;
const finalizedFile = resolveFileCandidates(input, [
finalized.replace(/\/$/u, ""),
]);
if (finalizedFile !== null) return finalizedFile;
if (hasContainerDirectory(input, finalized))
return {
...unresolvedCandidate("rejected", [
`The selected legacy ESM package main finalizes to directory ${finalized}; directory imports are unsupported and no later main or index candidate was tried.`,
]),
candidatePath: finalized,
};
return unresolvedCandidate("not-found", [
`The selected legacy ESM package main finalizes to ${finalized}, which is not an inventoried file; no later main or index candidate was tried.`,
]);
}
const fallback = resolveFileCandidates(
input,
indexCandidates(directory, input.moduleKind),
);
if (fallback !== null) return fallback;
return input.moduleKind === "require"
return input.moduleKind === "require" && mainPath !== undefined
? { ...notFoundCandidate(), stopsAncestorLookup: true }
: notFoundCandidate();
};
@@ -1472,24 +1645,83 @@ const resolveFileCandidates = (
return null;
};
/** Classify the extension as inventory does; retain exact archive occurrence bytes. */
const runtimeArchiveRoot = (path: string): string | undefined => {
const components = path.split("/");
const archive = components.findLastIndex(
(component, index) =>
index < components.length - 1 &&
component.toLowerCase().endsWith(".asar"),
);
return archive < 0
? undefined
: `${components.slice(0, archive + 1).join("/")}/`;
};
/** One archive-path admission rule serves files, directories and their metadata. */
const isNodeFilesystemLookup = (input: ResolveArtifactPathInput): boolean =>
input.runtimePathKind === "node-entrypoint" ||
(input.moduleKind !== undefined &&
(input.context === "module-specifier" ||
input.context === "package-entrypoint"));
/** Runtime launch and module lookup share archive occurrence admission. */
const crossesRuntimeArchiveBoundary = (
input: ResolveArtifactPathInput,
path: string,
): boolean => {
// Electron's archive filesystem normalizes a relative module path before
// opening it, including entry into an inventoried archive occurrence and
// escape from one, after the existing artifact-root confinement check.
const sourceArchive = runtimeArchiveRoot(input.sourcePath);
const targetArchive = runtimeArchiveRoot(path);
return (
isNodeFilesystemLookup(input) &&
((sourceArchive !== undefined && !path.startsWith(sourceArchive)) ||
(targetArchive !== undefined &&
!input.sourcePath.startsWith(targetArchive)))
);
};
const admitsRuntimeContainer = (
input: ResolveArtifactPathInput,
path: string,
containerSha256: string,
): boolean => {
const source = input.files.get(input.sourcePath);
return (
crossesRuntimeArchiveBoundary(input, path) ||
(input.runtimePathKind === "application-file" &&
input.applicationRootIsProven === true) ||
source === undefined ||
containerSha256 === source.container_sha256
);
};
/** Own candidate presence and container identity independently of loader policy. */
const findFileCandidate = (
input: ResolveArtifactPathInput,
path: string,
): CandidateResolution | null => {
const source = input.files.get(input.sourcePath);
const target = input.files.get(path);
if (
target !== undefined &&
(source === undefined ||
target.container_sha256 === source.container_sha256)
admitsRuntimeContainer(input, path, target.container_sha256)
)
return { resolvedPath: path, status: "resolved", limitations: [] };
if (target !== undefined && isNodeFilesystemLookup(input))
return {
resolvedPath: null,
status: "unavailable",
candidatePath: path,
limitations: [
"The inventoried module crosses an artifact container boundary; its runtime loader mapping has not been established.",
],
};
const unanalyzed = input.unanalyzedFiles?.get(path);
if (
unanalyzed !== undefined &&
(source === undefined ||
unanalyzed.container_sha256 === source.container_sha256)
admitsRuntimeContainer(input, path, unanalyzed.container_sha256)
)
return {
resolvedPath: null,
@@ -1542,9 +1774,10 @@ type PackageExportOutcome =
| ({ readonly status: "value" } & PackageFileTarget)
| { readonly status: "rejected"; readonly limitation: string }
| { readonly status: "blocked"; readonly cause: PackageTargetBlockCause }
| { readonly status: "pattern-unresolved"; readonly pattern: string }
| { readonly status: "unmatched"; readonly declared: readonly string[] };
/** Only exact non-directory keys participate; wildcard expansion is unsupported. */
/** Exact keys precede the best matching pattern; pattern expansion remains unknown. */
const selectPackageExport = (
value: unknown,
subpath: string,
@@ -1552,6 +1785,7 @@ const selectPackageExport = (
packagePath: string,
): PackageExportOutcome => {
let target = value;
let pattern: string | undefined;
let declared: string[] = [];
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
const keys = Object.keys(value);
@@ -1564,13 +1798,14 @@ const selectPackageExport = (
};
declared = keys;
if (hasSubpaths) {
if (
subpath.endsWith("/") ||
subpath.includes("*") ||
!Object.hasOwn(value, subpath)
)
return { status: "unmatched", declared };
target = Reflect.get(value, subpath);
if (subpath.endsWith("/")) return { status: "unmatched", declared };
if (!subpath.includes("*") && Object.hasOwn(value, subpath))
target = Reflect.get(value, subpath);
else {
pattern = selectPackageSubpathPattern(keys, subpath);
if (pattern === undefined) return { status: "unmatched", declared };
target = Reflect.get(value, pattern);
}
} else if (subpath !== ".") return { status: "unmatched", declared };
} else if (subpath !== ".") return { status: "unmatched", declared };
const selected = selectExportTarget(
@@ -1592,6 +1827,7 @@ const selectPackageExport = (
status: "rejected",
limitation: `The package exports target ${JSON.stringify(selected.value)} must start with "./".`,
};
if (pattern !== undefined) return { status: "pattern-unresolved", pattern };
return {
status: "value",
value: selected.value,