Files
reverse-engineering-anything/src/domain/javascript/electronStaticAnalysisValues.ts
T
morlutoandShuaixinHuang b46f4fcebd fix(javascript): preserve loader authority for exact suffixes (#9477)
* fix(javascript): require exact loader suffix case

* fix(javascript): share exact loader mode across import resolution

* test(process): align symlink fixture with filesystem checkpoints

* test(javascript): normalize the module resolution oracle path separators

* test(frida): assert the CLI spawn Evidence contract

* test(mcp): use an unsupported analysis-view parent

* test(browser): separate source-location line bounds from parity

* fix(javascript): preserve uncertain loader provenance

* test(javascript): cover uncertain require resolution

* fix(javascript): retain conditional Electron evidence

* fix(javascript): preserve timed semantic provenance

* fix(javascript): retain conditional builtin effects

* test(javascript): distinguish bundled host authority

* fix(javascript): preserve nested loader invocation identity

* test(javascript): qualify Electron fixture authority

* fix(javascript): preserve loader invocation authority

* fix(javascript): respect source order for resolver escapes

* fix(javascript): check nested loader callable mutations

* fix(javascript): retain conditional constructor requests

* fix(reference): retain constructor loader relationships

* fix(reference): follow nested loader callable paths

* refactor(javascript): share callable mutation interpretation

* fix(javascript): require exact package loader suffixes

* refactor(javascript): unify conditional relation evidence

* refactor(javascript): unify conditional node evidence

* fix(javascript): retain unknown authority for mixed case sources

* fix(javascript): preserve loader uncertainty for dotfiles

* fix(reference): classify javascript suffix dotfiles

* test(reference): cover suffix dotfiles through cli

* fix(javascript): retain equivalent conditional built-in origins

* fix(javascript): preserve wrapped resolver operation

* fix(javascript): retain nested conditional built-in receivers

* test(javascript): declare CommonJS in native addon fixtures

* fix(javascript): retain wrapped resolver candidates

* style: format merged native command schema

* fix(javascript): preserve uncertain loader authority

* test(javascript): require candidate preload evidence

* test(javascript): cover reference loader member writes

---------

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
2026-11-09 13:21:24 +08:00

182 lines
5.6 KiB
TypeScript

import * as t from "@babel/types";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import type { ElectronStaticValue } from "./electronStaticAnalysisTypes.js";
import {
readExactJavaScriptLiteral,
semanticStaticPropertyKey,
} from "./javascriptAstValues.js";
import { electronMemberPath } from "./javascriptElectronMemberWrites.js";
import { sourceSlice } from "./javascriptStaticAnalysisHelpers.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
/** Resolve a callee only when its root has a proven Electron module identity. */
export const electronCalleePath = (
callee: t.Node,
context: JavaScriptFindingContext,
): readonly string[] | undefined => {
const { root, members } = electronMemberPath(callee);
if (!members.every((member): member is string => member !== null))
return undefined;
const exported =
context.electronBindings?.get(root.start ?? -1) ??
context.electronCandidateBindings?.get(root.start ?? -1);
return exported === undefined ? undefined : [...exported, ...members];
};
/** Keep the identity uncertainty attached to each derived finding. */
export const electronCandidateAuthority = (
callee: t.Node,
context: JavaScriptFindingContext,
): { readonly loader_authority?: "candidate" } => {
const { root } = electronMemberPath(callee);
const offset = root.start ?? -1;
return context.electronBindings?.has(offset) !== true &&
context.electronCandidateBindings?.has(offset) === true
? { loader_authority: "candidate" }
: {};
};
/** Preserve one literal value or the exact inert expression. */
export const electronStaticValue = (
source: string,
node: t.Node | null | undefined,
): ElectronStaticValue => {
const literal = literalValue(node);
return literal.found
? { status: "literal", value: literal.value, expression: null }
: {
status: "dynamic",
value: null,
expression: boundedExpression(source, node),
};
};
/** Return an actionable expression without evaluating it. */
export const boundedExpression = (
source: string,
node: t.Node | null | undefined,
): string => {
if (node === null || node === undefined) return "[missing-expression]";
const expression = sourceSlice(source, node).trim();
return expression === "" ? `[${node.type}]` : expression;
};
type PresentHandlerKind =
| "inline-function"
| "identifier"
| "member-expression"
| "dynamic-expression";
/** Classify a handler argument while retaining its exact source range. */
export function handlerKind(node: t.Node): PresentHandlerKind;
export function handlerKind(node: null | undefined): "missing";
export function handlerKind(
node: t.Node | null | undefined,
): PresentHandlerKind | "missing" {
if (node === null || node === undefined) return "missing";
if (
t.isArrowFunctionExpression(node) ||
t.isFunctionExpression(node) ||
t.isFunctionDeclaration(node)
)
return "inline-function";
if (t.isIdentifier(node)) return "identifier";
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
return "member-expression";
return "dynamic-expression";
}
/**
* Nesting bound for dotted contextBridge member paths. Deeper objects count as
* one unknown member rather than being enumerated.
*/
export const contextBridgeMemberPathDepth = 8;
/** Collect statically visible dotted keys from one literal contextBridge API object. */
export const collectContextBridgeMembers = (
node: t.Node | null | undefined,
): {
readonly status: "object-literal" | "dynamic" | "missing";
readonly members: readonly string[];
readonly unknown: number;
} => {
if (node === null || node === undefined)
return {
status: "missing",
members: [],
unknown: 0,
};
if (!t.isObjectExpression(node))
return {
status: "dynamic",
members: [],
unknown: 1,
};
const state: { members: string[]; unknown: number } = {
members: [],
unknown: 0,
};
collectMembersAt(node, [], state);
const members = [...new Set(state.members)].sort(compareUnicodeCodePoints);
return {
status: "object-literal",
members,
unknown: state.unknown,
};
};
const collectMembersAt = (
object: t.ObjectExpression,
prefix: readonly string[],
state: { members: string[]; unknown: number },
): void => {
if (prefix.length >= contextBridgeMemberPathDepth) {
state.unknown += 1;
return;
}
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
state.unknown += 1;
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) {
state.unknown += 1;
continue;
}
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
)
continue;
const segments = [...prefix, name];
const path = segments.join(".");
state.members.push(path);
if (t.isObjectProperty(property) && t.isObjectExpression(property.value))
collectMembersAt(property.value, segments, state);
}
};
const literalValue = (
node: t.Node | null | undefined,
):
| { readonly found: true; readonly value: string | number | boolean | null }
| { readonly found: false } => {
if (node === null || node === undefined) return { found: false };
const literal = readExactJavaScriptLiteral(node);
if (literal.found) return literal;
if (
t.isUnaryExpression(node, { operator: "-" }) &&
t.isNumericLiteral(node.argument)
) {
const value = -node.argument.value;
return Number.isFinite(value) && !Object.is(value, -0)
? { found: true, value }
: { found: false };
}
return { found: false };
};