diff --git a/packages/metro-source-map/package.json b/packages/metro-source-map/package.json index 7f72e7002b..c9807f9bbc 100644 --- a/packages/metro-source-map/package.json +++ b/packages/metro-source-map/package.json @@ -25,8 +25,7 @@ "metro-symbolicate": "0.87.1", "nullthrows": "^1.1.1", "ob1": "0.87.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" + "source-map": "^0.5.6" }, "license": "MIT", "devDependencies": { diff --git a/packages/metro-source-map/src/Consumer/MappingsConsumer.js b/packages/metro-source-map/src/Consumer/MappingsConsumer.js index 54777ef14a..f9c9b7cc08 100644 --- a/packages/metro-source-map/src/Consumer/MappingsConsumer.js +++ b/packages/metro-source-map/src/Consumer/MappingsConsumer.js @@ -30,7 +30,23 @@ import normalizeSourcePath from './normalizeSourcePath'; import {greatestLowerBound} from './search'; import invariant from 'invariant'; import {add, add0, get0, inc, sub} from 'ob1'; -import {decode as decodeVlq} from 'vlq'; + +/* eslint-disable no-bitwise */ + +const COMMA = 44; // ',' +const SEMICOLON = 59; // ';' +const VLQ_BASE_SHIFT = 5; +const VLQ_BASE_MASK = (1 << VLQ_BASE_SHIFT) - 1; +const VLQ_CONTINUATION_BIT = 1 << VLQ_BASE_SHIFT; +const BASE64_DECODE: Int8Array = (() => { + const table = new Int8Array(128).fill(-1); + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (let i = 0; i < chars.length; i++) { + table[chars.charCodeAt(i)] = i; + } + return table; +})(); /** * A source map consumer that supports "basic" source maps (that have a @@ -93,7 +109,11 @@ export default class MappingsConsumer return {...EMPTY_POSITION}; } - *_decodeMappings(): Generator { + _decodeMappings(): Array { + const normalizedSources = this._normalizeAndCacheSources(); + const {mappings: mappingsRaw, names} = this._sourceMap; + const result: Array = []; + let generatedLine = FIRST_LINE; let generatedColumn = FIRST_COLUMN; let originalLine = FIRST_LINE; @@ -101,77 +121,77 @@ export default class MappingsConsumer let nameIndex = add0(0); let sourceIndex = add0(0); - const normalizedSources = this._normalizeAndCacheSources(); - - const {mappings: mappingsRaw, names} = this._sourceMap; - let next; - const vlqCache = new Map(); - for (let i = 0; i < mappingsRaw.length; i = next) { - switch (mappingsRaw[i]) { - case ';': + // The VLQ fields of the segment being decoded, decoded in place from + // character codes rather than by slicing out each segment. + const fields = [0, 0, 0, 0, 0]; + let fieldCount = 0; + let value = 0; + let shift = 0; + + const length = mappingsRaw.length; + for (let i = 0; i <= length; i++) { + const charCode = i < length ? mappingsRaw.charCodeAt(i) : SEMICOLON; + if (charCode === COMMA || charCode === SEMICOLON) { + // A trailing field cut off mid-VLQ is dropped, as `vlq` does. + invariant( + fieldCount > 0 || shift === 0, + 'Invalid generated column delta', + ); + value = 0; + shift = 0; + if (fieldCount > 0) { + invariant(fieldCount !== 2, 'Invalid original line delta'); + invariant(fieldCount !== 3, 'Invalid original column delta'); + generatedColumn = add(generatedColumn, fields[0]); + let source = null; + let name = null; + let mappingOriginalLine = null; + let mappingOriginalColumn = null; + if (fieldCount >= 4) { + sourceIndex = add(sourceIndex, fields[1]); + source = normalizedSources[get0(sourceIndex)]; + originalLine = add(originalLine, fields[2]); + originalColumn = add(originalColumn, fields[3]); + mappingOriginalLine = originalLine; + mappingOriginalColumn = originalColumn; + if (fieldCount >= 5) { + nameIndex = add(nameIndex, fields[4]); + name = names[get0(nameIndex)]; + } + } + result.push({ + generatedLine, + generatedColumn, + source, + name, + originalLine: mappingOriginalLine, + originalColumn: mappingOriginalColumn, + }); + fieldCount = 0; + } + if (charCode === SEMICOLON && i < length) { generatedLine = inc(generatedLine); generatedColumn = FIRST_COLUMN; - /* falls through */ - case ',': - next = i + 1; - continue; - } - findNext: for (next = i + 1; next < mappingsRaw.length; ++next) { - switch (mappingsRaw[next]) { - case ';': - /* falls through */ - case ',': - break findNext; } + continue; } - const mappingRaw = mappingsRaw.slice(i, next); - let decodedVlqValues; - if (vlqCache.has(mappingRaw)) { - decodedVlqValues = vlqCache.get(mappingRaw); + const digit = charCode < 128 ? BASE64_DECODE[charCode] : -1; + invariant(digit !== -1, 'Invalid character in source map mappings'); + value = value + ((digit & VLQ_BASE_MASK) << shift); + if (digit & VLQ_CONTINUATION_BIT) { + shift = shift + VLQ_BASE_SHIFT; } else { - decodedVlqValues = decodeVlq(mappingRaw); - vlqCache.set(mappingRaw, decodedVlqValues); - } - invariant(Array.isArray(decodedVlqValues), 'Decoding VLQ tuple failed'); - const [ - generatedColumnDelta, - sourceIndexDelta, - originalLineDelta, - originalColumnDelta, - nameIndexDelta, - ] = decodedVlqValues; - invariant(generatedColumnDelta != null, 'Invalid generated column delta'); - generatedColumn = add(generatedColumn, generatedColumnDelta); - const mapping: {...Mapping, ...} = { - generatedLine, - generatedColumn, - source: null, - name: null, - originalLine: null, - originalColumn: null, - }; - - if (sourceIndexDelta != null) { - sourceIndex = add(sourceIndex, sourceIndexDelta); - mapping.source = normalizedSources[get0(sourceIndex)]; - - invariant(originalLineDelta != null, 'Invalid original line delta'); - invariant(originalColumnDelta != null, 'Invalid original column delta'); - - originalLine = add(originalLine, originalLineDelta); - originalColumn = add(originalColumn, originalColumnDelta); - - mapping.originalLine = originalLine; - mapping.originalColumn = originalColumn; - - if (nameIndexDelta != null) { - nameIndex = add(nameIndex, nameIndexDelta); - mapping.name = names[get0(nameIndex)]; + const negate = value & 1; + value = value >>> 1; + if (fieldCount < 5) { + fields[fieldCount] = negate ? -value : value; } + fieldCount++; + value = 0; + shift = 0; } - - yield mapping; } + return result; } _normalizeAndCacheSources(): ReadonlyArray { @@ -185,7 +205,7 @@ export default class MappingsConsumer _decodeAndCacheMappings(): ReadonlyArray { if (!this._decodedMappings) { - this._decodedMappings = [...this._decodeMappings()]; + this._decodedMappings = this._decodeMappings(); } return this._decodedMappings; } diff --git a/packages/metro-source-map/src/__tests__/Consumer-test.js b/packages/metro-source-map/src/__tests__/Consumer-test.js index 9a5e2baf8c..bb9aa64b78 100644 --- a/packages/metro-source-map/src/__tests__/Consumer-test.js +++ b/packages/metro-source-map/src/__tests__/Consumer-test.js @@ -217,6 +217,64 @@ describe('basic maps', () => { ] `); }); + + test('multi-digit and negative deltas, empty segments and lines', () => { + expect([ + ...new Consumer({ + version: 3, + mappings: 'AAAU,gBCoBHC,,w+B;;KDjBg9D', + names: ['name0', 'name1'], + sources: ['source0', 'source1'], + }).generatedMappings(), + ]).toEqual([ + { + generatedLine: 1, + generatedColumn: 0, + source: 'source0', + name: null, + originalLine: 1, + originalColumn: 10, + }, + { + generatedLine: 1, + generatedColumn: 16, + source: 'source1', + name: 'name1', + originalLine: 21, + originalColumn: 7, + }, + { + generatedLine: 1, + generatedColumn: 1016, + source: null, + name: null, + originalLine: null, + originalColumn: null, + }, + { + generatedLine: 3, + generatedColumn: 5, + source: 'source0', + name: null, + originalLine: 4, + originalColumn: 2007, + }, + ]); + }); + + test.each([ + ['AA', 'Invalid original line delta'], + ['AAAA,AAA', 'Invalid original column delta'], + ['AAAA,A!AA', undefined], + ])('throws on malformed mappings %s', (mappings, message) => { + const consumer = new Consumer({ + version: 3, + mappings, + names: [], + sources: ['source0'], + }); + expect(() => [...consumer.generatedMappings()]).toThrow(message); + }); }); describe('sourceContentFor', () => {