From 02cd0e9360cefcbf87226169fac027519c54095d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:07:39 -0800 Subject: [PATCH] fix(audit): keep a .map unsettled once a declaration was discarded --- src/artifacts/ReleaseExposureReader.test.ts | 21 +++++++++++++++++++++ src/artifacts/ReleaseExposureReader.ts | 16 ++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/artifacts/ReleaseExposureReader.test.ts b/src/artifacts/ReleaseExposureReader.test.ts index 34993046e1..cffe9c7951 100644 --- a/src/artifacts/ReleaseExposureReader.test.ts +++ b/src/artifacts/ReleaseExposureReader.test.ts @@ -3223,6 +3223,27 @@ describe("ReleaseExposureReader map target allowance", () => { expect(scanned.source_maps.embedded).toBe(0); expect(sourceMapSourcesStatus(scanned)).toBe("unknown"); }); + + it("does not read a .map past the target allowance as a linker map", async () => { + const root = await createTestTempDirectory("rea-release-exposure-maps-"); + const folder = join( + root, + ...Array.from({ length: 4 }, () => "d".repeat(200)), + ); + await mkdir(folder, { recursive: true }); + const declarations = Array.from( + { length: Math.ceil(MAX_RELEASE_EXPOSURE_MAP_TARGET_BYTES / 800) + 1 }, + (_, index) => `//# sourceMappingURL=m${String(index)}`, + ); + await writeFile( + join(folder, "a.js"), + `${declarations.join("\n")}\n//# sourceMappingURL=z.map\n`, + ); + await writeFile(join(folder, "z.map"), "not a map\n"); + const scanned = await inspect(root); + expect(scanned.debug_companions.paths).toEqual([]); + expect(scanned.source_maps.skipped).toBeGreaterThan(0); + }); }); describe("ReleaseExposureReader HTML script references", () => { diff --git a/src/artifacts/ReleaseExposureReader.ts b/src/artifacts/ReleaseExposureReader.ts index 6c63995579..0b0d3735d8 100644 --- a/src/artifacts/ReleaseExposureReader.ts +++ b/src/artifacts/ReleaseExposureReader.ts @@ -772,6 +772,11 @@ interface ScanState { /** Bytes of resolved path offered to `scriptTargets`, kept or refused. */ scriptTargetBytes: number; mapTargetBytes: number; + /** + * A declared map target was refused by the allowance. A later `.map` may be + * that target, so it can no longer settle as a linker map. + */ + mapTargetsDiscarded: boolean; /** Members already examined as a script. */ examinedScripts: Set; visitText: ReleaseExposureTextVisitor; @@ -817,6 +822,7 @@ const createState = (visitText: ReleaseExposureTextVisitor): ScanState => ({ scriptTargets: new Set(), scriptTargetBytes: 0, mapTargetBytes: 0, + mapTargetsDiscarded: false, examinedScripts: new Set(), visitText, }); @@ -1526,9 +1532,10 @@ const noteMapTarget = (state: ScanState, location: string): void => { return; } state.mapTargetBytes += Buffer.byteLength(target); - if (state.mapTargetBytes > MAX_RELEASE_EXPOSURE_MAP_TARGET_BYTES) + if (state.mapTargetBytes > MAX_RELEASE_EXPOSURE_MAP_TARGET_BYTES) { state.skippedMaps += 1; - else state.mapTargets.add(target); + state.mapTargetsDiscarded = true; + } else state.mapTargets.add(target); }; /** @@ -1540,6 +1547,11 @@ const memberLocationKey = (location: string): string => logicalPathNfc(resolve(location)); const noteLinkerMapGuess = (state: ScanState, location: string): void => { + // Once a declaration was discarded, this map may be its target. + if (state.mapTargetsDiscarded) { + state.skippedMaps += 1; + return; + } const key = memberLocationKey(location); const paths = state.linkerMapGuesses.get(key) ?? new Set(); state.linkerMapGuesses.set(key, paths.add(location));