tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

source-map.js (18952B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 "use strict";
      6 
      7 /**
      8 * Source Map Worker
      9 *
     10 * @module utils/source-map-worker
     11 */
     12 
     13 const {
     14  SourceMapConsumer,
     15 } = require("resource://devtools/client/shared/vendor/source-map/source-map.js");
     16 
     17 // Initialize the source-map library right away so that all other code can use it.
     18 SourceMapConsumer.initialize({
     19  "lib/mappings.wasm":
     20    "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm",
     21 });
     22 
     23 const {
     24  networkRequest,
     25 } = require("resource://devtools/client/shared/source-map-loader/utils/network-request.js");
     26 const assert = require("resource://devtools/client/shared/source-map-loader/utils/assert.js");
     27 const {
     28  fetchSourceMap,
     29  resolveSourceMapURL,
     30  hasOriginalURL,
     31  clearOriginalURLs,
     32 } = require("resource://devtools/client/shared/source-map-loader/utils/fetchSourceMap.js");
     33 const {
     34  getSourceMap,
     35  getSourceMapWithMetadata,
     36  setSourceMap,
     37  clearSourceMapForSources,
     38  clearSourceMaps: clearSourceMapsRequests,
     39 } = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests.js");
     40 const {
     41  originalToGeneratedId,
     42  generatedToOriginalId,
     43  isGeneratedId,
     44  isOriginalId,
     45  getContentType,
     46 } = require("resource://devtools/client/shared/source-map-loader/utils/index.js");
     47 const {
     48  clearWasmXScopes,
     49 } = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js");
     50 
     51 /**
     52 * Create "original source info" objects being handed over to the main thread
     53 * to describe original sources referenced in a source map
     54 */
     55 function mapToOriginalSourceInfos(generatedId, urls) {
     56  return urls.map(url => {
     57    return {
     58      id: generatedToOriginalId(generatedId, url),
     59      url,
     60    };
     61  });
     62 }
     63 
     64 /**
     65 * Load the source map and retrieved infos about all the original sources
     66 * referenced in that source map.
     67 *
     68 * @param {object} generatedSource
     69 *        Source object for a bundle referencing a source map
     70 * @return {Array<object> | null}
     71 *        List of object with id and url attributes describing the original sources.
     72 */
     73 async function getOriginalURLs(generatedSource) {
     74  const { resolvedSourceMapURL, baseURL } =
     75    resolveSourceMapURL(generatedSource);
     76  const map = await fetchSourceMap(
     77    generatedSource,
     78    resolvedSourceMapURL,
     79    baseURL
     80  );
     81  return map ? mapToOriginalSourceInfos(generatedSource.id, map.sources) : null;
     82 }
     83 
     84 /**
     85 * Load the source map for a given bundle and return information
     86 * about the related original sources and the source map itself.
     87 *
     88 * @param {object} generatedSource
     89 *        Source object for the bundle.
     90 * @return {object}
     91 *  - {Array<Object>} sources
     92 *    Object with id and url attributes, refering to the related original sources
     93 *    referenced in the source map.
     94 *  - [String} resolvedSourceMapURL
     95 *    Absolute URL for the source map file.
     96 *  - {Array<String>} ignoreListUrls
     97 *    List of URLs of sources, designated by the source map, to be ignored in the debugger.
     98 *  - {String} exception
     99 *    In case of error, a string describing the situation.
    100 */
    101 async function loadSourceMap(generatedSource) {
    102  const { resolvedSourceMapURL, baseURL } =
    103    resolveSourceMapURL(generatedSource);
    104  try {
    105    const map = await fetchSourceMap(
    106      generatedSource,
    107      resolvedSourceMapURL,
    108      baseURL
    109    );
    110    if (!map.sources.length) {
    111      throw new Error("No sources are declared in this source map.");
    112    }
    113    let ignoreListUrls = [];
    114    if (map.x_google_ignoreList?.length) {
    115      ignoreListUrls = map.x_google_ignoreList.map(
    116        sourceIndex => map.sources[sourceIndex]
    117      );
    118    }
    119    return {
    120      sources: mapToOriginalSourceInfos(generatedSource.id, map.sources),
    121      resolvedSourceMapURL,
    122      ignoreListUrls,
    123    };
    124  } catch (e) {
    125    return {
    126      sources: [],
    127      resolvedSourceMapURL,
    128      ignoreListUrls: [],
    129      exception: e.message,
    130    };
    131  }
    132 }
    133 
    134 const COMPUTED_SPANS = new WeakSet();
    135 
    136 const SOURCE_MAPPINGS = new WeakMap();
    137 async function getOriginalRanges(sourceId) {
    138  if (!isOriginalId(sourceId)) {
    139    return [];
    140  }
    141 
    142  const generatedSourceId = originalToGeneratedId(sourceId);
    143  const data = await getSourceMapWithMetadata(generatedSourceId);
    144  if (!data) {
    145    return [];
    146  }
    147  const { map } = data;
    148  const url = data.urlsById.get(sourceId);
    149 
    150  let mappings = SOURCE_MAPPINGS.get(map);
    151  if (!mappings) {
    152    mappings = new Map();
    153    SOURCE_MAPPINGS.set(map, mappings);
    154  }
    155 
    156  let fileMappings = mappings.get(url);
    157  if (!fileMappings) {
    158    fileMappings = [];
    159    mappings.set(url, fileMappings);
    160 
    161    const originalMappings = fileMappings;
    162    map.eachMapping(
    163      mapping => {
    164        if (mapping.source !== url) {
    165          return;
    166        }
    167 
    168        const last = originalMappings[originalMappings.length - 1];
    169 
    170        if (last && last.line === mapping.originalLine) {
    171          if (last.columnStart < mapping.originalColumn) {
    172            last.columnEnd = mapping.originalColumn;
    173          } else {
    174            // Skip this duplicate original location,
    175            return;
    176          }
    177        }
    178 
    179        originalMappings.push({
    180          line: mapping.originalLine,
    181          columnStart: mapping.originalColumn,
    182          columnEnd: Infinity,
    183        });
    184      },
    185      null,
    186      SourceMapConsumer.ORIGINAL_ORDER
    187    );
    188  }
    189 
    190  return fileMappings;
    191 }
    192 
    193 /**
    194 * Given an original location, find the ranges on the generated file that
    195 * are mapped from the original range containing the location.
    196 */
    197 async function getGeneratedRanges(location) {
    198  if (!isOriginalId(location.sourceId)) {
    199    return [];
    200  }
    201 
    202  const generatedSourceId = originalToGeneratedId(location.sourceId);
    203  const data = await getSourceMapWithMetadata(generatedSourceId);
    204  if (!data) {
    205    return [];
    206  }
    207  const { urlsById, map } = data;
    208 
    209  if (!COMPUTED_SPANS.has(map)) {
    210    COMPUTED_SPANS.add(map);
    211    map.computeColumnSpans();
    212  }
    213 
    214  // We want to use 'allGeneratedPositionsFor' to get the _first_ generated
    215  // location, but it hard-codes SourceMapConsumer.LEAST_UPPER_BOUND as the
    216  // bias, making it search in the wrong direction for this usecase.
    217  // To work around this, we use 'generatedPositionFor' and then look up the
    218  // exact original location, making any bias value unnecessary, and then
    219  // use that location for the call to 'allGeneratedPositionsFor'.
    220  const genPos = map.generatedPositionFor({
    221    source: urlsById.get(location.sourceId),
    222    line: location.line,
    223    column: location.column == null ? 0 : location.column,
    224    bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
    225  });
    226  if (genPos.line === null) {
    227    return [];
    228  }
    229 
    230  const positions = map.allGeneratedPositionsFor(
    231    map.originalPositionFor({
    232      line: genPos.line,
    233      column: genPos.column,
    234    })
    235  );
    236 
    237  return positions
    238    .map(mapping => ({
    239      line: mapping.line,
    240      columnStart: mapping.column,
    241      columnEnd: mapping.lastColumn,
    242    }))
    243    .sort((a, b) => {
    244      const line = a.line - b.line;
    245      return line === 0 ? a.column - b.column : line;
    246    });
    247 }
    248 
    249 async function getGeneratedLocation(location) {
    250  if (!isOriginalId(location.sourceId)) {
    251    return null;
    252  }
    253 
    254  const generatedSourceId = originalToGeneratedId(location.sourceId);
    255  const data = await getSourceMapWithMetadata(generatedSourceId);
    256  if (!data) {
    257    return null;
    258  }
    259  const { urlsById, map } = data;
    260 
    261  const positions = map.allGeneratedPositionsFor({
    262    source: urlsById.get(location.sourceId),
    263    line: location.line,
    264    column: location.column == null ? 0 : location.column,
    265  });
    266 
    267  // Prior to source-map 0.7, the source-map module returned the earliest
    268  // generated location in the file when there were multiple generated
    269  // locations. The current comparison fn in 0.7 does not appear to take
    270  // generated location into account properly.
    271  let match;
    272  for (const pos of positions) {
    273    if (!match || pos.line < match.line || pos.column < match.column) {
    274      match = pos;
    275    }
    276  }
    277 
    278  if (!match) {
    279    match = map.generatedPositionFor({
    280      source: urlsById.get(location.sourceId),
    281      line: location.line,
    282      column: location.column == null ? 0 : location.column,
    283      bias: SourceMapConsumer.LEAST_UPPER_BOUND,
    284    });
    285  }
    286 
    287  return {
    288    sourceId: generatedSourceId,
    289    line: match.line,
    290    column: match.column,
    291  };
    292 }
    293 
    294 /**
    295 * Map the breakable positions (line and columns) from generated to original locations.
    296 *
    297 * @param {object} breakpointPositions
    298 *        List of columns per line refering to the breakable columns per line
    299 *        for a given source:
    300 *        {
    301 *           1: [2, 6], // On line 1, column 2 and 6 are breakable.
    302 *           ...
    303 *        }
    304 * @param {string} sourceId
    305 *        The ID for the generated source.
    306 */
    307 async function getOriginalLocations(breakpointPositions, sourceId) {
    308  const map = await getSourceMap(sourceId);
    309  if (!map) {
    310    return null;
    311  }
    312  for (const line in breakpointPositions) {
    313    const breakableColumnsPerLine = breakpointPositions[line];
    314    for (let i = 0; i < breakableColumnsPerLine.length; i++) {
    315      const column = breakableColumnsPerLine[i];
    316      const mappedLocation = getOriginalLocationSync(map, {
    317        sourceId,
    318        line: parseInt(line, 10),
    319        column,
    320      });
    321      if (mappedLocation) {
    322        // As we replace the `column` with the mappedLocation,
    323        // also transfer the generated column so that we can compute both original and generated locations
    324        // in the main thread.
    325        mappedLocation.generatedColumn = column;
    326        breakableColumnsPerLine[i] = mappedLocation;
    327      }
    328    }
    329  }
    330  return breakpointPositions;
    331 }
    332 
    333 /**
    334 * Query the source map for a mapping from bundle location to original location.
    335 *
    336 * @param {SourceMapConsumer} map
    337 *        The source map for the bundle source.
    338 * @param {object} location
    339 *        A location within a bundle to map to an original location.
    340 * @param {object} options
    341 * @param {boolean} options.looseSearch
    342 *        Optional, if true, will do a loose search on first column and next lines
    343 *        until a mapping is found.
    344 * @return {location}
    345 *        The mapped location in the original source.
    346 */
    347 function getOriginalLocationSync(map, location, { looseSearch = false } = {}) {
    348  // First check for an exact match
    349  let match = map.originalPositionFor({
    350    line: location.line,
    351    column: location.column == null ? 0 : location.column,
    352  });
    353 
    354  // Then check for a loose match by sliding to first column and next lines
    355  if (match.sourceUrl == null && looseSearch) {
    356    let line = location.line;
    357    // if a non-0 column was passed, we want to do the search from the beginning of the line,
    358    // otherwise, we can start looking into next lines
    359    let firstLineChecked = (location.column || 0) !== 0;
    360 
    361    // Avoid looping through the whole file and limit the sliding search to the next 10 lines.
    362    while (match.sourceUrl === null && line < location.line + 10) {
    363      if (firstLineChecked) {
    364        line++;
    365      } else {
    366        firstLineChecked = true;
    367      }
    368      match = map.originalPositionFor({
    369        line,
    370        column: 0,
    371        bias: SourceMapConsumer.LEAST_UPPER_BOUND,
    372      });
    373    }
    374  }
    375 
    376  const { source: sourceUrl, line, column } = match;
    377 
    378  if (sourceUrl == null) {
    379    // No url means the location didn't map.
    380    return null;
    381  }
    382 
    383  return {
    384    sourceId: generatedToOriginalId(location.sourceId, sourceUrl),
    385    sourceUrl,
    386    line,
    387    column,
    388  };
    389 }
    390 
    391 /**
    392 * Map a bundle location to an original one.
    393 *
    394 * @param {object} location
    395 *        Bundle location
    396 * @param {object} options
    397 *        See getORiginalLocationSync.
    398 * @return {object}
    399 *        Original location
    400 */
    401 async function getOriginalLocation(location, options) {
    402  if (!isGeneratedId(location.sourceId)) {
    403    return null;
    404  }
    405 
    406  const map = await getSourceMap(location.sourceId);
    407  if (!map) {
    408    return null;
    409  }
    410 
    411  return getOriginalLocationSync(map, location, options);
    412 }
    413 
    414 async function getOriginalSourceText(originalSourceId) {
    415  assert(isOriginalId(originalSourceId), "Source is not an original source");
    416 
    417  const generatedSourceId = originalToGeneratedId(originalSourceId);
    418  const data = await getSourceMapWithMetadata(generatedSourceId);
    419  if (!data) {
    420    return null;
    421  }
    422  const { urlsById, map } = data;
    423 
    424  const url = urlsById.get(originalSourceId);
    425  let text = map.sourceContentFor(url, true);
    426  if (!text) {
    427    try {
    428      const response = await networkRequest(url, {
    429        sourceMapBaseURL: map.sourceMapBaseURL,
    430        loadFromCache: false,
    431        allowsRedirects: false,
    432      });
    433      text = response.content;
    434    } catch (err) {
    435      // Workers exceptions are processed by worker-utils module and
    436      // only metadata attribute is transferred between threads.
    437      // Notify the main thread about which url failed loading.
    438      err.metadata = {
    439        url,
    440      };
    441      throw err;
    442    }
    443  }
    444 
    445  return {
    446    text,
    447    contentType: getContentType(url || ""),
    448  };
    449 }
    450 
    451 /**
    452 * Find the set of ranges on the generated file that map from the original
    453 * file's locations.
    454 *
    455 * @param sourceId - The original ID of the file we are processing.
    456 * @param url - The original URL of the file we are processing.
    457 * @param mergeUnmappedRegions - If unmapped regions are encountered between
    458 *   two mappings for the given original file, allow the two mappings to be
    459 *   merged anyway. This is useful if you are more interested in the general
    460 *   contiguous ranges associated with a file, rather than the specifics of
    461 *   the ranges provided by the sourcemap.
    462 */
    463 const GENERATED_MAPPINGS = new WeakMap();
    464 async function getGeneratedRangesForOriginal(
    465  sourceId,
    466  mergeUnmappedRegions = false
    467 ) {
    468  assert(isOriginalId(sourceId), "Source is not an original source");
    469 
    470  const data = await getSourceMapWithMetadata(originalToGeneratedId(sourceId));
    471  // NOTE: this is only needed for Flow
    472  if (!data) {
    473    return [];
    474  }
    475  const { urlsById, map } = data;
    476  const url = urlsById.get(sourceId);
    477 
    478  if (!COMPUTED_SPANS.has(map)) {
    479    COMPUTED_SPANS.add(map);
    480    map.computeColumnSpans();
    481  }
    482 
    483  if (!GENERATED_MAPPINGS.has(map)) {
    484    GENERATED_MAPPINGS.set(map, new Map());
    485  }
    486 
    487  const generatedRangesMap = GENERATED_MAPPINGS.get(map);
    488  if (!generatedRangesMap) {
    489    return [];
    490  }
    491 
    492  if (generatedRangesMap.has(sourceId)) {
    493    // NOTE we need to coerce the result to an array for Flow
    494    return generatedRangesMap.get(sourceId) || [];
    495  }
    496 
    497  // Gather groups of mappings on the generated file, with new groups created
    498  // if we cross a mapping for a different file.
    499  let currentGroup = [];
    500  const originalGroups = [currentGroup];
    501  map.eachMapping(
    502    mapping => {
    503      if (mapping.source === url) {
    504        currentGroup.push({
    505          start: {
    506            line: mapping.generatedLine,
    507            column: mapping.generatedColumn,
    508          },
    509          end: {
    510            line: mapping.generatedLine,
    511            // The lastGeneratedColumn value is an inclusive value so we add
    512            // one to it to get the exclusive end position.
    513            column: mapping.lastGeneratedColumn + 1,
    514          },
    515        });
    516      } else if (typeof mapping.source === "string" && currentGroup.length) {
    517        // If there is a URL, but it is for a _different_ file, we create a
    518        // new group of mappings so that we can tell
    519        currentGroup = [];
    520        originalGroups.push(currentGroup);
    521      }
    522    },
    523    null,
    524    SourceMapConsumer.GENERATED_ORDER
    525  );
    526 
    527  const generatedMappingsForOriginal = [];
    528  if (mergeUnmappedRegions) {
    529    // If we don't care about excluding unmapped regions, then we just need to
    530    // create a range that is the fully encompasses each group, ignoring the
    531    // empty space between each individual range.
    532    for (const group of originalGroups) {
    533      if (group.length) {
    534        generatedMappingsForOriginal.push({
    535          start: group[0].start,
    536          end: group[group.length - 1].end,
    537        });
    538      }
    539    }
    540  } else {
    541    let lastEntry;
    542    for (const group of originalGroups) {
    543      lastEntry = null;
    544      for (const { start, end } of group) {
    545        const lastEnd = lastEntry
    546          ? wrappedMappingPosition(lastEntry.end)
    547          : null;
    548 
    549        // If this entry comes immediately after the previous one, extend the
    550        // range of the previous entry instead of adding a new one.
    551        if (
    552          lastEntry &&
    553          lastEnd &&
    554          lastEnd.line === start.line &&
    555          lastEnd.column === start.column
    556        ) {
    557          lastEntry.end = end;
    558        } else {
    559          const newEntry = { start, end };
    560          generatedMappingsForOriginal.push(newEntry);
    561          lastEntry = newEntry;
    562        }
    563      }
    564    }
    565  }
    566 
    567  generatedRangesMap.set(sourceId, generatedMappingsForOriginal);
    568  return generatedMappingsForOriginal;
    569 }
    570 
    571 function wrappedMappingPosition(pos) {
    572  if (pos.column !== Infinity) {
    573    return pos;
    574  }
    575 
    576  // If the end of the entry consumes the whole line, treat it as wrapping to
    577  // the next line.
    578  return {
    579    line: pos.line + 1,
    580    column: 0,
    581  };
    582 }
    583 
    584 async function getFileGeneratedRange(originalSourceId) {
    585  assert(isOriginalId(originalSourceId), "Source is not an original source");
    586 
    587  const data = await getSourceMapWithMetadata(
    588    originalToGeneratedId(originalSourceId)
    589  );
    590  if (!data) {
    591    return null;
    592  }
    593  const { urlsById, map } = data;
    594 
    595  const start = map.generatedPositionFor({
    596    source: urlsById.get(originalSourceId),
    597    line: 1,
    598    column: 0,
    599    bias: SourceMapConsumer.LEAST_UPPER_BOUND,
    600  });
    601 
    602  const end = map.generatedPositionFor({
    603    source: urlsById.get(originalSourceId),
    604    line: Number.MAX_SAFE_INTEGER,
    605    column: Number.MAX_SAFE_INTEGER,
    606    bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
    607  });
    608 
    609  return {
    610    start,
    611    end,
    612  };
    613 }
    614 
    615 /**
    616 * Set the sourceMap for multiple passed source ids.
    617 *
    618 * @param {Array<string>} generatedSourceIds
    619 * @param {object} map: An actual sourcemap (as generated with SourceMapGenerator#toJSON)
    620 */
    621 function setSourceMapForGeneratedSources(generatedSourceIds, map) {
    622  const sourceMapConsumer = new SourceMapConsumer(map);
    623  for (const generatedId of generatedSourceIds) {
    624    setSourceMap(generatedId, Promise.resolve(sourceMapConsumer));
    625  }
    626 }
    627 function clearSourceMapForGeneratedSources(generatedSourceIds) {
    628  clearSourceMapForSources(generatedSourceIds);
    629 }
    630 
    631 function clearSourceMaps() {
    632  clearSourceMapsRequests();
    633  clearWasmXScopes();
    634  clearOriginalURLs();
    635 }
    636 
    637 module.exports = {
    638  getOriginalURLs,
    639  loadSourceMap,
    640  hasOriginalURL,
    641  getOriginalRanges,
    642  getGeneratedRanges,
    643  getGeneratedLocation,
    644  getOriginalLocation,
    645  getOriginalLocations,
    646  getOriginalSourceText,
    647  getGeneratedRangesForOriginal,
    648  getFileGeneratedRange,
    649  setSourceMapForGeneratedSources,
    650  clearSourceMapForGeneratedSources,
    651  clearSourceMaps,
    652 };