From da25b3a691836b4cb6d9e961bf76e91c0d8a1aa0 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:27:25 -0800 Subject: [PATCH] fix(audit): keep embedded leaves beside schema-invalid map fields The reader recovers independent indexed leaves only after a SourceMapFormatFailure, but inspectSourceMap let the schema's ZodError through. A sibling with mappings "!", a section with a bad offset or a root with a bad field skipped recovery, so a valid leaf with sourcesContent reported javascript.source_map_sources as unknown. Normalize schema rejection in SourceMapFormat, which owns these parses, so every structural failure reaches consumers as a format failure. The message is unchanged, and the codec and web fetcher already map both cases to the same result. --- src/artifacts/ReleaseExposureReader.test.ts | 83 +++++++++++++------ src/javascript/sourceMaps/SourceMapFormat.ts | 18 +++- .../sourceMaps/TraceSourceMap.test.ts | 17 ++++ 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/artifacts/ReleaseExposureReader.test.ts b/src/artifacts/ReleaseExposureReader.test.ts index 67720d2a69..1e422aae8b 100644 --- a/src/artifacts/ReleaseExposureReader.test.ts +++ b/src/artifacts/ReleaseExposureReader.test.ts @@ -933,33 +933,55 @@ describe("ReleaseExposureReader mixed maps and sibling paths", () => { }, ); - it("keeps an embedded leaf beside a leaf with malformed mappings", async () => { - const root = await createTestTempDirectory("rea-release-malformed-leaf-"); - const path = join(root, "app.js.map"); - await writeFile( - path, - JSON.stringify({ - version: 3, - sections: [ - { - offset: { line: 0, column: 0 }, - map: JSON.parse(embeddedSourceMap().toString("utf8")) as unknown, - }, - { - offset: { line: 1, column: 0 }, - map: { version: 3, sources: ["b.js"], names: [], mappings: "AA" }, - }, - ], - }), - ); - const scanned = await inspect(path); - expect(scanned.source_maps).toMatchObject({ - embedded: 1, - embedded_paths: [path], - unparsed: 1, - }); - expect(sourceMapSourcesStatus(scanned)).toBe("exposed"); - }); + it.each([ + { + name: "a truncated mapping segment", + root: {}, + second: { offset: { line: 1, column: 0 }, map: malformedLeaf("AA") }, + }, + { + name: "a schema-invalid mappings string", + root: {}, + second: { offset: { line: 1, column: 0 }, map: malformedLeaf("!") }, + }, + { + name: "a schema-invalid section offset", + root: {}, + second: { offset: { line: "1" }, map: malformedLeaf("AAAA") }, + }, + { + name: "a schema-invalid root field", + root: { file: 5 }, + second: { offset: { line: 1, column: 0 }, map: malformedLeaf("AAAA") }, + }, + ])( + "keeps an embedded leaf beside $name", + async ({ root: fields, second }) => { + const root = await createTestTempDirectory("rea-release-malformed-leaf-"); + const path = join(root, "app.js.map"); + await writeFile( + path, + JSON.stringify({ + version: 3, + ...fields, + sections: [ + { + offset: { line: 0, column: 0 }, + map: JSON.parse(embeddedSourceMap().toString("utf8")) as unknown, + }, + second, + ], + }), + ); + const scanned = await inspect(path); + expect(scanned.source_maps).toMatchObject({ + embedded: 1, + embedded_paths: [path], + unparsed: 1, + }); + expect(sourceMapSourcesStatus(scanned)).toBe("exposed"); + }, + ); it("does not expand sections past the decoder depth budget", async () => { const root = await createTestTempDirectory("rea-release-deep-map-"); @@ -2575,6 +2597,13 @@ describe("ReleaseExposureReader member identity", () => { }); }); +const malformedLeaf = (mappings: string) => ({ + version: 3, + sources: ["b.js"], + names: [], + mappings, +}); + const sourceMapSourcesStatus = (scan: ReleaseExposureScan) => assembleReleaseExposureReport(scan, { electron_family: "not_electron", diff --git a/src/javascript/sourceMaps/SourceMapFormat.ts b/src/javascript/sourceMaps/SourceMapFormat.ts index fc5e6eeb8b..163437ccd7 100644 --- a/src/javascript/sourceMaps/SourceMapFormat.ts +++ b/src/javascript/sourceMaps/SourceMapFormat.ts @@ -62,6 +62,16 @@ export class SourceMapFormatFailure extends Error { } } +/** Report schema rejection through the one failure type consumers classify. */ +const parseStructure = ( + schema: Schema, + value: unknown, +): z.output => { + const parsed = schema.safeParse(value); + if (parsed.success) return parsed.data; + throw new SourceMapFormatFailure("format", parsed.error.message); +}; + /** Raw declaration identity, separate from flattened engine URL identity. */ export interface SourceMapSourceDeclaration { readonly sectionPath: readonly number[]; @@ -139,7 +149,7 @@ const addSections = ({ }): void => { options.check?.(); consumeRecords(budget, rawSections.length); - const sections = z.array(sectionSchema).parse(rawSections); + const sections = parseStructure(z.array(sectionSchema), rawSections); const embeddedMaps: Extract< SourceMapSection, { readonly kind: "embedded" } @@ -266,7 +276,7 @@ const inspectLeaf = ( node.value === null || Array.isArray(node.value) ) - return leafSchema.parse(node.value); + return parseStructure(leafSchema, node.value); const rawMap = node.value as Readonly>; const rawSources = rawMap.sources; const rawNames = rawMap.names; @@ -275,7 +285,7 @@ const inspectLeaf = ( consumeRecords(budget, Array.isArray(rawNames) ? rawNames.length : 0); consumeRecords(budget, Array.isArray(rawContents) ? rawContents.length : 0); options.check?.(); - const leaf = leafSchema.parse(node.value); + const leaf = parseStructure(leafSchema, node.value); const layout = inspectMappingLayout(leaf.mappings, options.check); consumeRecords(budget, layout.segments + layout.lines); if ( @@ -309,7 +319,7 @@ export const inspectSourceMapValue = ( readonly leaves: readonly SourceMapLeaf[]; } => { options.check?.(); - const root = mapHeaderSchema.parse(value); + const root = parseStructure(mapHeaderSchema, value); const declarations: SourceMapSourceDeclaration[] = []; const leaves: SourceMapLeaf[] = []; const pending: PendingMap[] = [ diff --git a/src/javascript/sourceMaps/TraceSourceMap.test.ts b/src/javascript/sourceMaps/TraceSourceMap.test.ts index 03f1a8bb7c..5973009651 100644 --- a/src/javascript/sourceMaps/TraceSourceMap.test.ts +++ b/src/javascript/sourceMaps/TraceSourceMap.test.ts @@ -35,6 +35,23 @@ it("classifies external indexed sections as unsupported with their exact URL", ( } }); +it.each([ + ["root field", { ...leaf, version: 2 }], + ["leaf field", { ...leaf, mappings: "!" }], + [ + "section field", + { version: 3, sections: [{ offset: { line: "0" }, map: leaf }] }, + ], +])("reports a %s schema rejection as a format failure", (_, map) => { + try { + inspectSourceMap(JSON.stringify(map)); + throw new Error("Expected schema rejection"); + } catch (error: unknown) { + expect(error).toBeInstanceOf(SourceMapFormatFailure); + expect(error).toMatchObject({ reason: "format" }); + } +}); + it("uses the same conservative section classification for URL-plus-map and malformed URLs", () => { const externalUrl = "https://app.test/maps/part.map"; const external = () =>