node/test/parallel/test-source-map-api.js
Justin Ridgewell 0214b90308 process: fix two overflow cases in SourceMap VLQ decoding
These both have to do with extremely large numbers, so it's unlikely to
cause a problem in practice. Still, correctness.

First, encoding `-2147483648` in VLQ returns the value `"B"`. When
decoding, we get the value `1` after reading the base64. We then check
if the first bit is set (it is) to see if we should negate it, then we
shift all bits right once. Now, `value` will be `0` and `negate` will
be `true`. So, we'd return `-0`. Which is a bug! `-0` isn't
`-2147483648`, and we've broken a round trip.

Second, encoding any number with the 31st bit set, we'd return the
opposite sign. Let's use `1073741824`. Encoding, we get `"ggggggC"`.
When decoding, we get the value `-2147483648` after reading the base64.
Notice, it's already negative (the 32nd bit is set, because the 31st was
set and we shifted everything left once). We'd then check the first bit
(it's not) and shift right. But we used `>>`, which does not shift the
sign bit. We actually wanted `>>>`, which will. Because of that bug, we
get back `-1073741824` instead of the positive `1073741824`. It's even
worse if the 32nd and 31st bits are set, `-1610612736` becomes
`536870912` after a round trip.

I recently fixed the same two bugs in Closure Compiler:
https://github.com/google/closure-compiler/commit/584418eb

PR-URL: https://github.com/nodejs/node/pull/31490
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ben Coe <bencoe@gmail.com>
Reviewed-By: David Carlier <devnexen@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
2020-01-26 17:03:39 -08:00

127 lines
3.6 KiB
JavaScript

// Flags: --enable-source-maps
'use strict';
require('../common');
const assert = require('assert');
const { findSourceMap, SourceMap } = require('module');
const { readFileSync } = require('fs');
// findSourceMap() can lookup source-maps based on URIs, in the
// non-exceptional case.
{
require('../fixtures/source-map/disk-relative-path.js');
const sourceMap = findSourceMap(
require.resolve('../fixtures/source-map/disk-relative-path.js')
);
const {
originalLine,
originalColumn,
originalSource
} = sourceMap.findEntry(0, 29);
assert.strictEqual(originalLine, 2);
assert.strictEqual(originalColumn, 4);
assert(originalSource.endsWith('disk.js'));
}
// findSourceMap() can be used in Error.prepareStackTrace() to lookup
// source-map attached to error.
{
let callSite;
let sourceMap;
Error.prepareStackTrace = (error, trace) => {
const throwingRequireCallSite = trace[0];
if (throwingRequireCallSite.getFileName().endsWith('typescript-throw.js')) {
sourceMap = findSourceMap(throwingRequireCallSite.getFileName(), error);
callSite = throwingRequireCallSite;
}
};
try {
// Require a file that throws an exception, and has a source map.
require('../fixtures/source-map/typescript-throw.js');
} catch (err) {
err.stack; // Force prepareStackTrace() to be called.
}
assert(callSite);
assert(sourceMap);
const {
generatedLine,
generatedColumn,
originalLine,
originalColumn,
originalSource
} = sourceMap.findEntry(
callSite.getLineNumber() - 1,
callSite.getColumnNumber() - 1
);
assert.strictEqual(generatedLine, 19);
assert.strictEqual(generatedColumn, 14);
assert.strictEqual(originalLine, 17);
assert.strictEqual(originalColumn, 10);
assert(originalSource.endsWith('typescript-throw.ts'));
}
// SourceMap can be instantiated with Source Map V3 object as payload.
{
const payload = JSON.parse(readFileSync(
require.resolve('../fixtures/source-map/disk.map'), 'utf8'
));
const sourceMap = new SourceMap(payload);
const {
originalLine,
originalColumn,
originalSource
} = sourceMap.findEntry(0, 29);
assert.strictEqual(originalLine, 2);
assert.strictEqual(originalColumn, 4);
assert(originalSource.endsWith('disk.js'));
// The stored payload should be a clone:
assert.strictEqual(payload.mappings, sourceMap.payload.mappings);
assert.notStrictEqual(payload, sourceMap.payload);
assert.strictEqual(payload.sources[0], sourceMap.payload.sources[0]);
assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
}
// Test various known decodings to ensure decodeVLQ works correctly.
{
function makeMinimalMap(column) {
return {
sources: ['test.js'],
// Mapping from the 0th line, 0th column of the output file to the 0th
// source file, 0th line, ${column}th column.
mappings: `AAA${column}`,
};
}
const knownDecodings = {
'A': 0,
'B': -2147483648,
'C': 1,
'D': -1,
'E': 2,
'F': -2,
// 2^31 - 1, maximum values
'+/////D': 2147483647,
'8/////D': 2147483646,
'6/////D': 2147483645,
'4/////D': 2147483644,
'2/////D': 2147483643,
'0/////D': 2147483642,
// -2^31 + 1, minimum values
'//////D': -2147483647,
'9/////D': -2147483646,
'7/////D': -2147483645,
'5/////D': -2147483644,
'3/////D': -2147483643,
'1/////D': -2147483642,
};
for (const column in knownDecodings) {
const sourceMap = new SourceMap(makeMinimalMap(column));
const { originalColumn } = sourceMap.findEntry(0, 0);
assert.strictEqual(originalColumn, knownDecodings[column]);
}
}