Files
rea/src/browser/CdpElectronScripts.ts
T
Itoje Dollar e4ac7bbfc2 fix: order remaining outputs independently of host locale (#1867)
* fix: order remaining outputs independently of host locale

String.prototype.localeCompare without a pinned collation follows the
host's ICU default locale, so the same input could be ordered differently
on different machines. 614fb768 moved canonical domain projections to
compareUnicodeCodePoints; provider, application, catalog, and setup
orderings still used locale collation.

Order every remaining source site by Unicode code point, including Mach-O
symbols, firmware extraction paths, ASAR entries (previously pinned to
"en" collation), evidence ID sets, browser and Electron target, script,
and resource inventories, reconstruction obligations, capability and
provider listings, and setup environment details.

The current tool and prompt catalog names order identically under both
comparators, so the catalog identity digest is unchanged.

* fix(artifacts): order ASAR entries by their portable path

@electron/asar lists host-spelled paths, so code-point order over the raw
spelling still differed by host: "a\b" sorts after "a0" on Windows while
"a/b" sorts before it on POSIX. Pair each listing with its portable
archive path once, order by that path, and keep the host spelling for
@electron/asar calls and adapter keys.
2026-10-12 01:10:01 +08:00

106 lines
3.4 KiB
TypeScript

import type {
ElectronPageInspection,
InspectElectronPageInput,
} from "../domain/javascript/electronObservation.js";
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import type { CdpConnection } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { requiredScriptSource } from "./CdpCaptureValues.js";
import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
interface ElectronScriptCaptureInput {
readonly connection: CdpConnection;
readonly sessionId: string | undefined;
readonly signal?: AbortSignal;
readonly request: InspectElectronPageInput;
readonly scripts: readonly ElectronScriptDraft[];
readonly executionContextFrames: ReadonlyMap<string, string>;
readonly frameIds: ReadonlySet<string>;
readonly completeness: CdpCaptureCompleteness;
}
/** Normalize local script metadata and optionally approved source. */
export const captureElectronScripts = async (
input: ElectronScriptCaptureInput,
): Promise<ElectronPageInspection["scripts"]> => {
let total = 0;
const items: ElectronPageInspection["scripts"]["items"] = [];
const seen = new Set<string>();
for (const script of input.scripts) {
const path = await authorizedElectronFile(script.rawUrl);
if (path === undefined) {
input.completeness.exclude("scripts", "out_of_target_scope");
continue;
}
const mappedFrame =
script.executionContextKey === null
? undefined
: input.executionContextFrames.get(script.executionContextKey);
const identity = {
frame_id:
mappedFrame !== undefined && input.frameIds.has(mappedFrame)
? mappedFrame
: null,
file_path: path,
cdp_hash: script.hash,
length: script.length,
is_module: script.isModule,
language: script.language,
};
const scriptKey = `electron_script_${digestCanonicalValue(identity, "CDP capture")}`;
if (seen.has(scriptKey)) continue;
seen.add(scriptKey);
total += 1;
const capturedSource = await captureScriptSource(input, script);
items.push({
script_key: scriptKey,
...identity,
source: capturedSource.source,
});
}
if (!input.request.include_script_sources)
input.completeness.exclude("script_sources", "not_approved", total);
return {
total,
items: items.sort((left, right) =>
compareUnicodeCodePoints(left.script_key, right.script_key),
),
};
};
type ElectronScriptSource =
ElectronPageInspection["scripts"]["items"][number]["source"];
const captureScriptSource = async (
input: ElectronScriptCaptureInput,
script: ElectronScriptDraft,
): Promise<{
readonly source: ElectronScriptSource;
}> => {
if (!input.request.include_script_sources)
return {
source: {
included: false,
reason: "source capture was not selected",
},
};
const text = requiredScriptSource(
await input.connection.send(
"Debugger.getScriptSource",
{ scriptId: script.scriptId },
input.sessionId,
input.signal,
),
"inspect_electron_page",
);
return {
source: {
included: true,
artifact: createWebTextArtifact(text, "text/javascript"),
},
};
};