mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
04fd41b288
Wrapper substitution keyed contextBridge findings only by the inner exposure call's range, so every helper invocation replaced the previous one while unioning its members: the last key survived with every invocation's members. Substituted members also listed only immediate object keys, dropping nested paths that direct syntax retains. Record substituted exposures per site and per world and key, and replace the site's syntax observation once after traversal. Invocations exposing one key at one site describe one API covering each alternative object; a traced invocation whose key, object or world stays unresolved or guarded keeps the site's own unresolved observation. Enumerate member paths through resolved objects with the direct syntax depth bound, which also retains the depth-limit unknown that substitution previously dropped. Fixes #5871 Fixes #5870 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
|
import type { ElectronStaticValue } from "./electronStaticAnalysisTypes.js";
|
|
import {
|
|
readExactJavaScriptLiteral,
|
|
semanticStaticPropertyKey,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptAstValues.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 members: string[] = [];
|
|
let root = callee;
|
|
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
|
|
const member = semanticStaticPropertyName(root.property, root.computed);
|
|
if (member === "" || !t.isNode(root.object)) return undefined;
|
|
members.push(member);
|
|
root = root.object;
|
|
}
|
|
const exported = context.electronBindings?.get(root.start ?? -1);
|
|
return exported === undefined
|
|
? undefined
|
|
: [...exported, ...members.toReversed()];
|
|
};
|
|
|
|
/** 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 };
|
|
};
|