mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
@@ -1,8 +1,5 @@
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
decodeValidatedSourceMapLeaves,
|
||||
isBeforeSourceMapLeafStop,
|
||||
} from "../javascript/sourceMaps/DecodedSourceMap.js";
|
||||
import { decodeValidatedSourceMapLeaves } from "../javascript/sourceMaps/DecodedSourceMap.js";
|
||||
|
||||
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
@@ -653,7 +650,6 @@ const normalizeSourceMap = (
|
||||
for (const [line, row] of rows.entries()) {
|
||||
if ((line & 255) === 0) check();
|
||||
for (const segment of row) {
|
||||
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0])) continue;
|
||||
if (segment.length === 1) continue;
|
||||
const rawSource = leaf.map.sources[segment[1]];
|
||||
if (rawSource === undefined) continue;
|
||||
@@ -741,7 +737,10 @@ const normalizeSourceMap = (
|
||||
return emptySourceMapItem(
|
||||
request,
|
||||
"invalid",
|
||||
"Source-map JSON mappings could not be decoded safely.",
|
||||
cause instanceof SourceMapFormatFailure &&
|
||||
cause.reason === "section-overlap"
|
||||
? cause.message
|
||||
: "Source-map JSON mappings could not be decoded safely.",
|
||||
fetchedUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "./SourceMapFormat.js";
|
||||
|
||||
/** Determine whether a leaf-local mapping falls before its enclosing boundary. */
|
||||
export const isBeforeSourceMapLeafStop = (
|
||||
const isBeforeSourceMapLeafStop = (
|
||||
leaf: SourceMapLeaf,
|
||||
line: number,
|
||||
column: number,
|
||||
@@ -40,6 +40,11 @@ export const decodeValidatedSourceMapLeaves = (
|
||||
const generatedLine = leaf.offset.line + line;
|
||||
const generatedColumn =
|
||||
segment[0] + (line === 0 ? leaf.offset.column : 0);
|
||||
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0]))
|
||||
throw new SourceMapFormatFailure(
|
||||
"section-overlap",
|
||||
"Embedded source-map mappings overlap the following section boundary.",
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(generatedLine) ||
|
||||
!Number.isSafeInteger(generatedColumn) ||
|
||||
|
||||
@@ -50,7 +50,11 @@ const execute = async (): Promise<string> => {
|
||||
throw cause;
|
||||
return JSON.stringify({
|
||||
state: "failure",
|
||||
reason: cause instanceof SourceMapFormatFailure ? cause.reason : "format",
|
||||
reason:
|
||||
cause instanceof SourceMapFormatFailure &&
|
||||
cause.reason !== "section-overlap"
|
||||
? cause.reason
|
||||
: "format",
|
||||
message: cause.message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface SourceMapStructureOptions {
|
||||
/** A decoder-profile failure with an actionable producer constraint. */
|
||||
export class SourceMapFormatFailure extends Error {
|
||||
constructor(
|
||||
readonly reason: "format" | "unsupported" | "limit",
|
||||
readonly reason: "format" | "unsupported" | "limit" | "section-overlap",
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
|
||||
@@ -310,7 +310,7 @@ describe("source-map layout admission", () => {
|
||||
{ offset: { line: 0, column: 5 }, map: leaf },
|
||||
],
|
||||
}),
|
||||
).toThrow(/overlap/u);
|
||||
).toThrow(expect.objectContaining({ reason: "section-overlap" }));
|
||||
});
|
||||
it("accepts absent optional names and null contents without changing reported identity", () => {
|
||||
expect(
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
SourceMapFormatFailure,
|
||||
type SourceMapSourceDeclaration,
|
||||
} from "./SourceMapFormat.js";
|
||||
import {
|
||||
decodeValidatedSourceMapLeaves,
|
||||
isBeforeSourceMapLeafStop,
|
||||
} from "./DecodedSourceMap.js";
|
||||
import { decodeValidatedSourceMapLeaves } from "./DecodedSourceMap.js";
|
||||
|
||||
/** Decode with the pinned upstream codec; preserve every equal-position candidate. */
|
||||
export const traceSourceMap = (
|
||||
@@ -35,14 +32,6 @@ export const traceSourceMap = (
|
||||
): WebSourceMapReport => {
|
||||
const inspected = inspectSourceMap(text);
|
||||
const decodedLeaves = decodeValidatedSourceMapLeaves(inspected.leaves, url);
|
||||
for (const { leaf, rows } of decodedLeaves)
|
||||
for (const [line, row] of rows.entries())
|
||||
for (const segment of row)
|
||||
if (!isBeforeSourceMapLeafStop(leaf, line, segment[0]))
|
||||
throw new SourceMapFormatFailure(
|
||||
"format",
|
||||
"Embedded source-map mappings overlap the following section boundary.",
|
||||
);
|
||||
let map: TraceMap;
|
||||
if (inspected.format === "regular") {
|
||||
const decoded = decodedLeaves[0];
|
||||
|
||||
@@ -127,6 +127,7 @@ const cases = [
|
||||
[1, 10, "b.ts"],
|
||||
],
|
||||
dedicatedControl: false,
|
||||
expectedStatus: "invalid",
|
||||
},
|
||||
{
|
||||
name: "nested gap clips previous parent leaf",
|
||||
@@ -177,6 +178,7 @@ const cases = [
|
||||
[1, 8, "b.ts"],
|
||||
],
|
||||
dedicatedControl: false,
|
||||
expectedStatus: "invalid",
|
||||
},
|
||||
{
|
||||
name: "empty indexed sibling clips previous leaf",
|
||||
@@ -210,6 +212,7 @@ const cases = [
|
||||
},
|
||||
expected: [[1, 0, "a.ts"]],
|
||||
dedicatedControl: false,
|
||||
expectedStatus: "invalid",
|
||||
},
|
||||
{
|
||||
name: "depth-three same-line composition",
|
||||
@@ -307,12 +310,16 @@ const cases = [
|
||||
[4, 1, "b.ts"],
|
||||
],
|
||||
dedicatedControl: false,
|
||||
expectedStatus: "invalid",
|
||||
},
|
||||
] as const;
|
||||
|
||||
it.each(cases)(
|
||||
"preserves indexed coordinate contract: $name",
|
||||
async ({ map, expected, dedicatedControl }) => {
|
||||
async (testCase) => {
|
||||
const { map, expected, dedicatedControl } = testCase;
|
||||
const expectedStatus =
|
||||
"expectedStatus" in testCase ? testCase.expectedStatus : "included";
|
||||
const origin = "https://example.test";
|
||||
const url = origin + "/app.js.map";
|
||||
const text = JSON.stringify(map);
|
||||
@@ -340,6 +347,15 @@ it.each(cases)(
|
||||
{ fetch: () => Promise.resolve(new Response(text, { status: 200 })) },
|
||||
);
|
||||
const item = result.items[0];
|
||||
if (expectedStatus === "invalid") {
|
||||
expect(item).toMatchObject({
|
||||
status: "invalid",
|
||||
artifact: null,
|
||||
mappings: [],
|
||||
limitation: expect.stringContaining("overlap"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (item?.status !== "included") throw new Error(JSON.stringify(result));
|
||||
expect(
|
||||
item.mappings.map((entry) => [
|
||||
@@ -358,3 +374,76 @@ it.each(cases)(
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a source mapping that crosses an indexed section boundary", async () => {
|
||||
const origin = "https://example.test";
|
||||
const url = `${origin}/app.js.map`;
|
||||
const makeMap = (firstMappings: string) => ({
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["a.js"],
|
||||
sourcesContent: ["x();"],
|
||||
names: [],
|
||||
mappings: firstMappings,
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 0, column: 1 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["b.js"],
|
||||
sourcesContent: ["y();"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = analyzeWebBundleInputSchema.parse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: [origin],
|
||||
target_id: "page-1",
|
||||
fetch_source_maps: true,
|
||||
});
|
||||
const request = {
|
||||
scriptKey: "scr_" + "1".repeat(64),
|
||||
declaredUrl: url,
|
||||
fetchUrl: url,
|
||||
};
|
||||
const fetchMap = (map: ReturnType<typeof makeMap>) =>
|
||||
fetchWebSourceMaps([request], input, undefined, {
|
||||
fetch: () =>
|
||||
Promise.resolve(new Response(JSON.stringify(map), { status: 200 })),
|
||||
});
|
||||
const overlapping = makeMap("AAAA,EAAA");
|
||||
const overlapText = JSON.stringify(overlapping);
|
||||
expect(() =>
|
||||
traceSourceMap(overlapText, url, { line: 1, column: 2 }),
|
||||
).toThrow(/overlap/u);
|
||||
const overlappingResult = await fetchMap(overlapping);
|
||||
expect(overlappingResult).toMatchObject({
|
||||
status: "unavailable",
|
||||
requested: 1,
|
||||
processed: 1,
|
||||
});
|
||||
expect(overlappingResult.items[0]).toMatchObject({
|
||||
status: "invalid",
|
||||
artifact: null,
|
||||
mappings: [],
|
||||
limitation: expect.stringContaining("overlap"),
|
||||
});
|
||||
|
||||
const controlResult = await fetchMap(makeMap("AAAA"));
|
||||
expect(controlResult.items[0]).toMatchObject({
|
||||
status: "included",
|
||||
limitation: null,
|
||||
mappings: [
|
||||
{ generated_line: 1, generated_column: 0, source: `${origin}/a.js` },
|
||||
{ generated_line: 1, generated_column: 1, source: `${origin}/b.js` },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { access } from "node:fs/promises";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { SourceMapDecoder } from "../../../src/javascript/sourceMaps/SourceMapDecoder.js";
|
||||
import {
|
||||
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
@@ -139,6 +140,47 @@ it.each([
|
||||
if (response.ok) throw new Error("expected failed map");
|
||||
expect(response.error._tag).toBe(tag);
|
||||
});
|
||||
|
||||
it("keeps indexed section overlap typed as a malformed source map", async () => {
|
||||
const text = JSON.stringify({
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["a.js"],
|
||||
sourcesContent: ["x();"],
|
||||
names: [],
|
||||
mappings: "AAAA,EAAA",
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 0, column: 1 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["b.js"],
|
||||
sourcesContent: ["y();"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const response = await realDecoder().trace({ ...input, text });
|
||||
if (response.ok) throw new Error("expected overlapping map rejection");
|
||||
expect(response.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!(response.error instanceof AnalysisInputError))
|
||||
throw new Error("expected malformed source-map input error");
|
||||
expect(response.error.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("overlap"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
it("reports cleanup failure with the actual owning operation", async () => {
|
||||
const response = await realDecoder({
|
||||
createRuntime: async () => {
|
||||
|
||||
Reference in New Issue
Block a user