Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/decode/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,69 @@ describe("decode", () => {
assertEquals(info.ranges[0].end, { line: 0, column: 10 });
});

describe("nested index source maps", () => {
function nestedIndexMap(depth: number): IndexSourceMapJson {
const leaf = encode(
new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10).build(),
);
let map: IndexSourceMapJson = {
version: 3,
sections: [{ offset: { line: 0, column: 0 }, map: leaf }],
};
for (let i = 1; i < depth; ++i) {
map = {
version: 3,
// Nested index maps are not allowed by the spec/types.
sections: [{
offset: { line: 0, column: 0 },
map: map as unknown as SourceMapJson,
}],
};
}
return map;
}

it("skips nested index maps in lax mode", () => {
const map1 = encode(
new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10).build(),
);
const map: IndexSourceMapJson = {
version: 3,
sections: [
{ offset: { line: 0, column: 0 }, map: map1 },
{
offset: { line: 1, column: 0 },
map: nestedIndexMap(2) as unknown as SourceMapJson,
},
],
};

const info = decode(map, { mode: DecodeMode.LAX });

assertEquals(info.ranges.length, 1);
assertEquals(info.ranges[0].start, { line: 0, column: 0 });
});

it("throws for nested index maps in strict mode", () => {
assertThrows(
() => decode(nestedIndexMap(2), { mode: DecodeMode.STRICT }),
Error,
"Index source map sections must not be index maps",
);
});

it("does not overflow the stack for deeply nested index maps", () => {
const map = nestedIndexMap(100_000);

assertEquals(decode(map, { mode: DecodeMode.LAX }).ranges, []);
assertThrows(
() => decode(map, { mode: DecodeMode.STRICT }),
Error,
"Index source map sections must not be index maps",
);
});
});

it("ignores vendor extensions", () => {
const encoder = new ItemEncoder();
encoder.addUnsignedVLQs(Tag.VENDOR_EXTENSION, 0);
Expand Down
19 changes: 15 additions & 4 deletions src/decode/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { TokenIterator } from "../vlq.ts";
* - Miss-matches between the number of variables in a scope vs the number of value expressions in the ranges.
* - GENERATED_RANGE_SUBRANGE_BINDING items whose variable index is out of bounds of the range's bindings.
* - Out-of-bound indices into the "names" array.
* - Index source maps with sections that are index source maps themselves.
*/
export const enum DecodeMode {
STRICT = 1,
Expand Down Expand Up @@ -100,10 +101,20 @@ function decodeIndexMap(
};

for (const section of sourceMap.sections) {
const { scopes, ranges, hasVariableAndBindingInfo } = decode(section.map, {
...options,
generatedOffset: section.offset,
});
// Per spec, a section's "map" is a regular source map; nested index maps
// are not allowed. Don't recurse, so maliciously deep nesting can't
// overflow the stack.
if ("sections" in section.map) {
if (options.mode === DecodeMode.STRICT) {
throw new Error("Index source map sections must not be index maps");
}
continue;
}

const { scopes, ranges, hasVariableAndBindingInfo } = decodeMap(
section.map,
{ ...options, generatedOffset: section.offset },
);
for (const scope of scopes) scopeInfo.scopes.push(scope);
for (const range of ranges) scopeInfo.ranges.push(range);
scopeInfo.hasVariableAndBindingInfo ||= hasVariableAndBindingInfo;
Expand Down
Loading