176 lines
7.0 KiB
JavaScript
176 lines
7.0 KiB
JavaScript
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
|
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
|
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
|
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
|
|
|
const MAX_SHEETS = 1024;
|
|
const MAX_SHEET_BYTES = 8 * 1024 * 1024;
|
|
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
|
|
/** Retain only stylesheet lifecycle metadata from the selected target session. */
|
|
export const ingestElectronStyleEvent = (event, stylesMap) => {
|
|
if (event?.method === "CSS.styleSheetRemoved") {
|
|
const styleSheetId = stringValue(recordValue(event.params)?.styleSheetId);
|
|
if (styleSheetId !== undefined)
|
|
stylesMap.delete(styleSheetId);
|
|
return;
|
|
}
|
|
if (event?.method !== "CSS.styleSheetAdded")
|
|
return;
|
|
const header = recordValue(recordValue(event.params)?.header);
|
|
const styleSheetId = stringValue(header?.styleSheetId);
|
|
if (styleSheetId === undefined || styleSheetId === "")
|
|
return;
|
|
stylesMap.set(styleSheetId, {
|
|
styleSheetId,
|
|
frameId: stringValue(header?.frameId) ?? null,
|
|
sourceURL: stringValue(header?.sourceURL) ?? null,
|
|
origin: stringValue(header?.origin) ?? null,
|
|
isInline: header?.isInline === true,
|
|
isConstructed: header?.isConstructed === true,
|
|
});
|
|
};
|
|
|
|
/** Capture CSS text only for authorized file frames and explicit source opt-in. */
|
|
export const captureElectronStyles = async (input) => {
|
|
const styles = [...input.styles.values()];
|
|
const items = [];
|
|
let excluded = 0;
|
|
let sourceFailures = 0;
|
|
let truncated = false;
|
|
let includedBytes = 0;
|
|
let sourceRequests = 0;
|
|
const limitationNotes = new Set();
|
|
void input.completeness;
|
|
|
|
limitationNotes.add("Stylesheet inventory includes only CSS.styleSheetAdded events still present at the capture snapshot.");
|
|
|
|
for (const style of styles) {
|
|
const scoped = await authorizedStyle(style, input.frameIds);
|
|
if (scoped === undefined) {
|
|
excluded += 1;
|
|
continue;
|
|
}
|
|
if (input.request?.include_style_sources !== true)
|
|
continue;
|
|
|
|
const identity = {
|
|
style_sheet_id: style.styleSheetId,
|
|
frame_id: scoped.frameId,
|
|
file_path: scoped.filePath,
|
|
origin: style.origin,
|
|
is_inline: style.isInline,
|
|
is_constructed: style.isConstructed,
|
|
};
|
|
const item = {
|
|
style_sheet_key: `electron_stylesheet_${digestCanonicalValue(identity, "CDP capture")}`,
|
|
style_sheet_id: style.styleSheetId,
|
|
frame_id: scoped.frameId,
|
|
file_path: scoped.filePath,
|
|
origin: style.origin,
|
|
is_inline: style.isInline,
|
|
is_constructed: style.isConstructed,
|
|
source: { included: false, reason: "CSS source was not requested" },
|
|
};
|
|
|
|
if (sourceRequests >= MAX_SHEETS) {
|
|
sourceFailures += 1;
|
|
truncated = true;
|
|
item.source = { included: false, reason: "stylesheet source count limit reached" };
|
|
limitationNotes.add(`CSS source capture reached the ${String(MAX_SHEETS)} stylesheet limit.`);
|
|
items.push(item);
|
|
continue;
|
|
}
|
|
sourceRequests += 1;
|
|
|
|
let sourceText;
|
|
try {
|
|
const result = await input.connection.send(
|
|
"CSS.getStyleSheetText",
|
|
{ styleSheetId: style.styleSheetId },
|
|
input.sessionId,
|
|
input.signal
|
|
);
|
|
sourceText = stringValue(recordValue(result)?.text);
|
|
}
|
|
catch (cause) {
|
|
if (input.signal?.aborted === true)
|
|
throw cause;
|
|
sourceFailures += 1;
|
|
item.source = { included: false, reason: "CSS.getStyleSheetText failed" };
|
|
limitationNotes.add("CSS text could not be retrieved for one or more authorized stylesheets.");
|
|
items.push(item);
|
|
continue;
|
|
}
|
|
|
|
if (sourceText === undefined) {
|
|
sourceFailures += 1;
|
|
item.source = { included: false, reason: "CSS.getStyleSheetText returned invalid text" };
|
|
limitationNotes.add("CSS text was invalid or absent for one or more authorized stylesheets.");
|
|
items.push(item);
|
|
continue;
|
|
}
|
|
|
|
const bytes = Buffer.byteLength(sourceText, "utf8");
|
|
if (bytes > MAX_SHEET_BYTES) {
|
|
sourceFailures += 1;
|
|
truncated = true;
|
|
item.source = { included: false, reason: "stylesheet source exceeds the per-stylesheet byte limit" };
|
|
limitationNotes.add(`CSS source capture omitted text larger than ${String(MAX_SHEET_BYTES)} bytes per stylesheet.`);
|
|
items.push(item);
|
|
continue;
|
|
}
|
|
if (includedBytes + bytes > MAX_TOTAL_BYTES) {
|
|
sourceFailures += 1;
|
|
truncated = true;
|
|
item.source = { included: false, reason: "aggregate stylesheet source byte limit reached" };
|
|
limitationNotes.add(`CSS source capture omitted text after reaching the ${String(MAX_TOTAL_BYTES)} byte aggregate limit.`);
|
|
items.push(item);
|
|
continue;
|
|
}
|
|
|
|
includedBytes += bytes;
|
|
item.source = { included: true, artifact: createWebTextArtifact(sourceText, "text/css") };
|
|
items.push(item);
|
|
}
|
|
|
|
if (input.request?.include_style_sources !== true)
|
|
limitationNotes.add("Stylesheet source capture was not selected; no CSS.getStyleSheetText requests were sent.");
|
|
if (excluded > 0)
|
|
limitationNotes.add(`${String(excluded)} stylesheet(s) were excluded because their origin or frame was outside the authorized local scope.`);
|
|
if (sourceFailures > 0)
|
|
limitationNotes.add(`${String(sourceFailures)} stylesheet source(s) failed retrieval or exceeded a source budget.`);
|
|
input.limitations.push(...limitationNotes);
|
|
|
|
return {
|
|
total: styles.length,
|
|
items: items.sort((left, right) => compareUnicodeCodePoints(left.style_sheet_key, right.style_sheet_key)),
|
|
excluded,
|
|
source_failures: sourceFailures,
|
|
truncated,
|
|
limits: {
|
|
max_sheets: MAX_SHEETS,
|
|
max_sheet_bytes: MAX_SHEET_BYTES,
|
|
max_total_bytes: MAX_TOTAL_BYTES,
|
|
},
|
|
};
|
|
};
|
|
|
|
const authorizedStyle = async (style, frameIds) => {
|
|
if (style.frameId === null || !frameIds.has(style.frameId) || style.origin !== "regular")
|
|
return undefined;
|
|
const sourceURL = style.sourceURL ?? "";
|
|
if (sourceURL === "") {
|
|
if (!style.isInline && !style.isConstructed)
|
|
return undefined;
|
|
return { frameId: style.frameId, filePath: null };
|
|
}
|
|
const filePath = await authorizedElectronFile(sourceURL);
|
|
return filePath === undefined ? undefined : { frameId: style.frameId, filePath };
|
|
};
|
|
|
|
const recordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
? value
|
|
: undefined;
|
|
const stringValue = (value) => typeof value === "string" ? value : undefined;
|