mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
Pass typed map objects into the codec and reuse validated TraceMap instances for regular and indexed lookups. Remove whole-map serialization, duplicate leaf decoding, an array-copy wrapper, and the unused offset-validation mode in artifact declaration collection. Validation: source-map and browser consumer suites (126 tests), declaration tests, and artifact reconstruction boundaries pass. Section overlap checks and raw source declarations remain intact.
370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
|
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
|
|
import { sourceMapJsonText } from "../../domain/sourceMapEnvelope.js";
|
|
import type { EncodedSourceMapXInput } from "@jridgewell/trace-mapping";
|
|
|
|
const leafSchema = z.object({
|
|
version: z.literal(3),
|
|
file: z.string().nullable().optional(),
|
|
sourceRoot: z.string().nullable().optional(),
|
|
sources: z.array(z.string().nullable()),
|
|
sourcesContent: z.array(z.string().nullable()).nullable().optional(),
|
|
names: z.array(z.string()).default([]),
|
|
mappings: z.string().regex(/^[A-Za-z0-9+/,;]*$/u),
|
|
ignoreList: z.array(z.number().int().min(0)).optional(),
|
|
x_google_ignoreList: z.array(z.number().int().min(0)).optional(),
|
|
});
|
|
const mapHeaderSchema = z.object({
|
|
version: z.literal(3),
|
|
file: z.string().nullable().optional(),
|
|
sections: z.unknown().optional(),
|
|
});
|
|
const sectionSchema = z.object({
|
|
offset: z.object({
|
|
line: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
column: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
}),
|
|
map: z.unknown().optional(),
|
|
url: z.string().optional(),
|
|
});
|
|
const indexedSchema = z.object({
|
|
version: z.literal(3),
|
|
sections: z.array(sectionSchema),
|
|
});
|
|
|
|
type Offset = { readonly line: number; readonly column: number };
|
|
interface PendingMap {
|
|
readonly value: unknown;
|
|
readonly path: readonly number[];
|
|
readonly offset: Offset;
|
|
readonly stop: Offset | null;
|
|
}
|
|
|
|
/** Shared structural constraints configurable only for stricter consumers. */
|
|
export interface SourceMapStructureBudget {
|
|
records: number;
|
|
}
|
|
|
|
export interface SourceMapStructureOptions {
|
|
readonly profile?: "browser-collection";
|
|
readonly budget?: SourceMapStructureBudget;
|
|
readonly check?: () => void;
|
|
}
|
|
|
|
/** A decoder-profile failure with an actionable producer constraint. */
|
|
export class SourceMapFormatFailure extends Error {
|
|
constructor(
|
|
readonly reason: "format" | "unsupported" | "limit",
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
/** Raw declaration identity, separate from flattened engine URL identity. */
|
|
export interface SourceMapSourceDeclaration {
|
|
readonly sectionPath: readonly number[];
|
|
readonly sourceIndex: number;
|
|
readonly source: string | null;
|
|
readonly root: string | null;
|
|
readonly content: string | null;
|
|
readonly ignored: boolean;
|
|
}
|
|
|
|
/** Validated leaf with absolute first-line offsets and its enclosing section boundary. */
|
|
export interface SourceMapLeaf {
|
|
readonly map: z.output<typeof leafSchema>;
|
|
readonly codecMap: EncodedSourceMapXInput;
|
|
readonly offset: Offset;
|
|
readonly stop: Offset | null;
|
|
}
|
|
|
|
const codecLeaf = (
|
|
map: z.output<typeof leafSchema>,
|
|
): EncodedSourceMapXInput => ({
|
|
version: 3,
|
|
...(map.file === undefined ? {} : { file: map.file }),
|
|
...(typeof map.sourceRoot === "string" ? { sourceRoot: map.sourceRoot } : {}),
|
|
sources: map.sources,
|
|
...(map.sourcesContent == null ? {} : { sourcesContent: map.sourcesContent }),
|
|
names: map.names,
|
|
mappings: map.mappings,
|
|
...(map.ignoreList === undefined ? {} : { ignoreList: map.ignoreList }),
|
|
...(map.x_google_ignoreList === undefined
|
|
? {}
|
|
: { x_google_ignoreList: map.x_google_ignoreList }),
|
|
});
|
|
|
|
const compare = (a: Offset, b: Offset): number =>
|
|
a.line - b.line || a.column - b.column;
|
|
const addOffset = (base: Offset, offset: Offset): Offset => {
|
|
const result = {
|
|
line: base.line + offset.line,
|
|
column: offset.column + (offset.line === 0 ? base.column : 0),
|
|
};
|
|
if (
|
|
!Number.isSafeInteger(result.line) ||
|
|
!Number.isSafeInteger(result.column) ||
|
|
result.line >= WEB_SOURCE_MAP_LIMITS.decodedRows
|
|
)
|
|
throw new SourceMapFormatFailure(
|
|
"limit",
|
|
"Indexed offset exceeds the 262144 generated-row decoder budget.",
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const consumeRecords = (budget: { records: number }, count: number): void => {
|
|
if (count > WEB_SOURCE_MAP_LIMITS.decodedRecords - budget.records)
|
|
throw new SourceMapFormatFailure(
|
|
"limit",
|
|
`Source-map structure exceeds the ${WEB_SOURCE_MAP_LIMITS.decodedRecords}-record decoder budget.`,
|
|
);
|
|
budget.records += count;
|
|
};
|
|
|
|
const addSections = (
|
|
node: PendingMap,
|
|
pending: PendingMap[],
|
|
budget: { records: number },
|
|
options: SourceMapStructureOptions,
|
|
): void => {
|
|
options.check?.();
|
|
const header = mapHeaderSchema.parse(node.value);
|
|
if (!Array.isArray(header.sections))
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Indexed source-map sections must be an array.",
|
|
);
|
|
consumeRecords(budget, header.sections.length);
|
|
const sections = indexedSchema.parse(node.value).sections;
|
|
for (const [index, section] of sections.entries()) {
|
|
if ((index & 255) === 0) options.check?.();
|
|
const previous = sections[index - 1];
|
|
if (previous !== undefined && compare(section.offset, previous.offset) <= 0)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Indexed section offsets must be strictly increasing.",
|
|
);
|
|
if (section.url !== undefined)
|
|
throw new SourceMapFormatFailure(
|
|
"unsupported",
|
|
`External source-map section URL is not fetched: ${section.url}`,
|
|
);
|
|
if (section.map === undefined)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Indexed section requires an embedded map.",
|
|
);
|
|
}
|
|
for (let index = sections.length - 1; index >= 0; index -= 1) {
|
|
const section = sections[index];
|
|
if (section === undefined) continue;
|
|
const next = sections[index + 1];
|
|
const nextOffset =
|
|
next === undefined ? null : addOffset(node.offset, next.offset);
|
|
const stop =
|
|
nextOffset === null
|
|
? node.stop
|
|
: node.stop === null || compare(nextOffset, node.stop) < 0
|
|
? nextOffset
|
|
: node.stop;
|
|
const offset = addOffset(node.offset, section.offset);
|
|
if (node.stop !== null && compare(offset, node.stop) >= 0)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Nested source-map section overlaps its enclosing boundary.",
|
|
);
|
|
pending.push({
|
|
value: section.map,
|
|
path: [...node.path, index],
|
|
offset,
|
|
stop,
|
|
});
|
|
}
|
|
};
|
|
|
|
const inspectMappingLayout = (
|
|
mappings: string,
|
|
check?: () => void,
|
|
): { lines: number; segments: number } => {
|
|
let lines = 1;
|
|
let segments = 0;
|
|
let components = 0;
|
|
let inSegment = false;
|
|
let continued = false;
|
|
let digits = 0;
|
|
const finish = (): void => {
|
|
if (continued)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Source-map VLQ ends with an unfinished continuation.",
|
|
);
|
|
if (inSegment && ![1, 4, 5].includes(components))
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Source-map segment requires one, four or five VLQ components.",
|
|
);
|
|
};
|
|
let index = 0;
|
|
for (const character of mappings) {
|
|
if ((index++ & 0x3fff) === 0) check?.();
|
|
if (character === "," || character === ";") {
|
|
finish();
|
|
inSegment = false;
|
|
components = 0;
|
|
if (character === ";") lines += 1;
|
|
} else {
|
|
digits += 1;
|
|
if (
|
|
(digits === 7 && !/[A-Dg-j]/u.test(character)) ||
|
|
(digits > 7 && character !== "A" && character !== "g")
|
|
)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Source-map VLQ exceeds the format's 32-bit quantity representation.",
|
|
);
|
|
if (!inSegment) segments += 1;
|
|
inSegment = true;
|
|
continued = /[g-z0-9+/]/u.test(character);
|
|
if (!continued) {
|
|
components += 1;
|
|
digits = 0;
|
|
}
|
|
}
|
|
if (
|
|
segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
|
|
lines > WEB_SOURCE_MAP_LIMITS.decodedRows
|
|
)
|
|
throw new SourceMapFormatFailure(
|
|
"limit",
|
|
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
|
|
);
|
|
}
|
|
finish();
|
|
return { lines, segments };
|
|
};
|
|
|
|
const inspectLeaf = (
|
|
node: PendingMap,
|
|
budget: { records: number },
|
|
options: SourceMapStructureOptions,
|
|
): z.output<typeof leafSchema> => {
|
|
options.check?.();
|
|
consumeRecords(budget, 1);
|
|
if (
|
|
typeof node.value !== "object" ||
|
|
node.value === null ||
|
|
Array.isArray(node.value)
|
|
)
|
|
return leafSchema.parse(node.value);
|
|
const rawMap = node.value as Readonly<Record<string, unknown>>;
|
|
const rawSources = rawMap.sources;
|
|
const rawNames = rawMap.names;
|
|
const rawContents = rawMap.sourcesContent;
|
|
consumeRecords(budget, Array.isArray(rawSources) ? rawSources.length : 0);
|
|
consumeRecords(budget, Array.isArray(rawNames) ? rawNames.length : 0);
|
|
consumeRecords(budget, Array.isArray(rawContents) ? rawContents.length : 0);
|
|
options.check?.();
|
|
if (options.profile === "browser-collection" && !Array.isArray(rawNames))
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Source-map leaf requires a names array.",
|
|
);
|
|
if (
|
|
options.profile === "browser-collection" &&
|
|
!hasValidSourceMapContents(
|
|
Array.isArray(rawSources) ? rawSources.length : 0,
|
|
rawContents,
|
|
)
|
|
)
|
|
throw new SourceMapFormatFailure(
|
|
"format",
|
|
"Source-map sourcesContent must align with its source declarations.",
|
|
);
|
|
options.check?.();
|
|
const leaf = leafSchema.parse(node.value);
|
|
const layout = inspectMappingLayout(leaf.mappings, options.check);
|
|
consumeRecords(budget, layout.segments + layout.lines);
|
|
if (
|
|
layout.segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
|
|
node.offset.line + layout.lines > WEB_SOURCE_MAP_LIMITS.decodedRows
|
|
)
|
|
throw new SourceMapFormatFailure(
|
|
"limit",
|
|
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
|
|
);
|
|
return leaf;
|
|
};
|
|
|
|
/** Validate structure before allocation, retaining raw declarations and section boundaries. */
|
|
export const inspectSourceMap = (
|
|
text: string,
|
|
): ReturnType<typeof inspectSourceMapValue> => {
|
|
const jsonText = sourceMapJsonText(text);
|
|
const value: unknown = JSON.parse(jsonText);
|
|
return inspectSourceMapValue(value);
|
|
};
|
|
|
|
/** Return validated leaves when a consumer needs decoded rows after preflight. */
|
|
export const inspectSourceMapValue = (
|
|
value: unknown,
|
|
options: SourceMapStructureOptions = {},
|
|
): {
|
|
readonly format: "regular" | "indexed";
|
|
readonly reportedFile: string | null;
|
|
readonly declarations: readonly SourceMapSourceDeclaration[];
|
|
readonly leaves: readonly SourceMapLeaf[];
|
|
} => {
|
|
options.check?.();
|
|
const root = mapHeaderSchema.parse(value);
|
|
const declarations: SourceMapSourceDeclaration[] = [];
|
|
const leaves: SourceMapLeaf[] = [];
|
|
const pending: PendingMap[] = [
|
|
{ value, path: [], offset: { line: 0, column: 0 }, stop: null },
|
|
];
|
|
const budget = options.budget ?? { records: 0 };
|
|
while (pending.length > 0) {
|
|
options.check?.();
|
|
const node = pending.pop();
|
|
if (node === undefined) break;
|
|
if (node.path.length > WEB_SOURCE_MAP_LIMITS.sectionDepth)
|
|
throw new SourceMapFormatFailure(
|
|
"limit",
|
|
"Indexed source-map nesting exceeds the 64-level decoder stack budget.",
|
|
);
|
|
if (mapHeaderSchema.parse(node.value).sections !== undefined) {
|
|
addSections(node, pending, budget, options);
|
|
continue;
|
|
}
|
|
const leaf = inspectLeaf(node, budget, options);
|
|
leaves.push({
|
|
map: leaf,
|
|
codecMap: codecLeaf(leaf),
|
|
offset: node.offset,
|
|
stop: node.stop,
|
|
});
|
|
if (options.profile !== "browser-collection") {
|
|
const ignoredSources = new Set(
|
|
leaf.ignoreList ?? leaf.x_google_ignoreList ?? [],
|
|
);
|
|
for (const [sourceIndex, source] of leaf.sources.entries())
|
|
declarations.push({
|
|
sectionPath: node.path,
|
|
sourceIndex,
|
|
source,
|
|
root: leaf.sourceRoot ?? null,
|
|
content: leaf.sourcesContent?.[sourceIndex] ?? null,
|
|
ignored: ignoredSources.has(sourceIndex),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
format: root.sections === undefined ? "regular" : "indexed",
|
|
reportedFile: root.file ?? null,
|
|
declarations,
|
|
leaves,
|
|
};
|
|
};
|