1'use strict';
2
3const {
4  ArrayPrototypeMap,
5  JSONParse,
6  ObjectCreate,
7  ObjectKeys,
8  ObjectGetOwnPropertyDescriptor,
9  ObjectPrototypeHasOwnProperty,
10  RegExpPrototypeExec,
11  RegExpPrototypeSymbolSplit,
12  SafeMap,
13  StringPrototypeSplit,
14} = primordials;
15
16function ObjectGetValueSafe(obj, key) {
17  const desc = ObjectGetOwnPropertyDescriptor(obj, key);
18  return ObjectPrototypeHasOwnProperty(desc, 'value') ? desc.value : undefined;
19}
20
21// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
22const { Buffer } = require('buffer');
23let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
24  debug = fn;
25});
26
27const { validateBoolean } = require('internal/validators');
28const {
29  setSourceMapsEnabled: setSourceMapsNative,
30  setPrepareStackTraceCallback,
31} = internalBinding('errors');
32const { getLazy } = require('internal/util');
33
34// Since the CJS module cache is mutable, which leads to memory leaks when
35// modules are deleted, we use a WeakMap so that the source map cache will
36// be purged automatically:
37const getCjsSourceMapCache = getLazy(() => {
38  const { IterableWeakMap } = require('internal/util/iterable_weak_map');
39  return new IterableWeakMap();
40});
41
42// The esm cache is not mutable, so we can use a Map without memory concerns:
43const esmSourceMapCache = new SafeMap();
44// The generated sources is not mutable, so we can use a Map without memory concerns:
45const generatedSourceMapCache = new SafeMap();
46const kLeadingProtocol = /^\w+:\/\//;
47const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/g;
48const kSourceURLMagicComment = /\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/g;
49
50const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
51
52let SourceMap;
53
54// This is configured with --enable-source-maps during pre-execution.
55let sourceMapsEnabled = false;
56function getSourceMapsEnabled() {
57  return sourceMapsEnabled;
58}
59
60function setSourceMapsEnabled(val) {
61  validateBoolean(val, 'val');
62
63  setSourceMapsNative(val);
64  if (val) {
65    const {
66      prepareStackTrace,
67    } = require('internal/source_map/prepare_stack_trace');
68    setPrepareStackTraceCallback(prepareStackTrace);
69  } else if (sourceMapsEnabled !== undefined) {
70    // Reset prepare stack trace callback only when disabling source maps.
71    const {
72      prepareStackTrace,
73    } = require('internal/errors');
74    setPrepareStackTraceCallback(prepareStackTrace);
75  }
76
77  sourceMapsEnabled = val;
78}
79
80function extractSourceURLMagicComment(content) {
81  let match;
82  let matchSourceURL;
83  // A while loop is used here to get the last occurrence of sourceURL.
84  // This is needed so that we don't match sourceURL in string literals.
85  while ((match = RegExpPrototypeExec(kSourceURLMagicComment, content))) {
86    matchSourceURL = match;
87  }
88  if (matchSourceURL == null) {
89    return null;
90  }
91  let sourceURL = matchSourceURL.groups.sourceURL;
92  if (sourceURL != null && RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
93    sourceURL = pathToFileURL(sourceURL).href;
94  }
95  return sourceURL;
96}
97
98function extractSourceMapURLMagicComment(content) {
99  let match;
100  let lastMatch;
101  // A while loop is used here to get the last occurrence of sourceMappingURL.
102  // This is needed so that we don't match sourceMappingURL in string literals.
103  while ((match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content))) {
104    lastMatch = match;
105  }
106  if (lastMatch == null) {
107    return null;
108  }
109  return lastMatch.groups.sourceMappingURL;
110}
111
112function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
113  const sourceMapsEnabled = getSourceMapsEnabled();
114  if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
115  try {
116    const { normalizeReferrerURL } = require('internal/modules/helpers');
117    filename = normalizeReferrerURL(filename);
118  } catch (err) {
119    // This is most likely an invalid filename in sourceURL of [eval]-wrapper.
120    debug(err);
121    return;
122  }
123
124  if (sourceMapURL === undefined) {
125    sourceMapURL = extractSourceMapURLMagicComment(content);
126  }
127
128  // Bail out when there is no source map url.
129  if (typeof sourceMapURL !== 'string') {
130    return;
131  }
132
133  if (sourceURL === undefined) {
134    sourceURL = extractSourceURLMagicComment(content);
135  }
136
137  const data = dataFromUrl(filename, sourceMapURL);
138  const url = data ? null : sourceMapURL;
139  if (cjsModuleInstance) {
140    getCjsSourceMapCache().set(cjsModuleInstance, {
141      filename,
142      lineLengths: lineLengths(content),
143      data,
144      url,
145      sourceURL,
146    });
147  } else if (isGeneratedSource) {
148    const entry = {
149      lineLengths: lineLengths(content),
150      data,
151      url,
152      sourceURL,
153    };
154    generatedSourceMapCache.set(filename, entry);
155    if (sourceURL) {
156      generatedSourceMapCache.set(sourceURL, entry);
157    }
158  } else {
159    // If there is no cjsModuleInstance and is not generated source assume we are in a
160    // "modules/esm" context.
161    const entry = {
162      lineLengths: lineLengths(content),
163      data,
164      url,
165      sourceURL,
166    };
167    esmSourceMapCache.set(filename, entry);
168    if (sourceURL) {
169      esmSourceMapCache.set(sourceURL, entry);
170    }
171  }
172}
173
174function maybeCacheGeneratedSourceMap(content) {
175  const sourceMapsEnabled = getSourceMapsEnabled();
176  if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
177
178  const sourceURL = extractSourceURLMagicComment(content);
179  if (sourceURL === null) {
180    return;
181  }
182  try {
183    maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
184  } catch (err) {
185    // This can happen if the filename is not a valid URL.
186    // If we fail to cache the source map, we should not fail the whole process.
187    debug(err);
188  }
189}
190
191function dataFromUrl(sourceURL, sourceMappingURL) {
192  try {
193    const url = new URL(sourceMappingURL);
194    switch (url.protocol) {
195      case 'data:':
196        return sourceMapFromDataUrl(sourceURL, url.pathname);
197      default:
198        debug(`unknown protocol ${url.protocol}`);
199        return null;
200    }
201  } catch (err) {
202    debug(err);
203    // If no scheme is present, we assume we are dealing with a file path.
204    const mapURL = new URL(sourceMappingURL, sourceURL).href;
205    return sourceMapFromFile(mapURL);
206  }
207}
208
209// Cache the length of each line in the file that a source map was extracted
210// from. This allows translation from byte offset V8 coverage reports,
211// to line/column offset Source Map V3.
212function lineLengths(content) {
213  // We purposefully keep \r as part of the line-length calculation, in
214  // cases where there is a \r\n separator, so that this can be taken into
215  // account in coverage calculations.
216  return ArrayPrototypeMap(RegExpPrototypeSymbolSplit(/\n|\u2028|\u2029/, content), (line) => {
217    return line.length;
218  });
219}
220
221function sourceMapFromFile(mapURL) {
222  try {
223    const fs = require('fs');
224    const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
225    const data = JSONParse(content);
226    return sourcesToAbsolute(mapURL, data);
227  } catch (err) {
228    debug(err);
229    return null;
230  }
231}
232
233// data:[<mediatype>][;base64],<data> see:
234// https://tools.ietf.org/html/rfc2397#section-2
235function sourceMapFromDataUrl(sourceURL, url) {
236  const { 0: format, 1: data } = StringPrototypeSplit(url, ',');
237  const splitFormat = StringPrototypeSplit(format, ';');
238  const contentType = splitFormat[0];
239  const base64 = splitFormat[splitFormat.length - 1] === 'base64';
240  if (contentType === 'application/json') {
241    const decodedData = base64 ?
242      Buffer.from(data, 'base64').toString('utf8') : data;
243    try {
244      const parsedData = JSONParse(decodedData);
245      return sourcesToAbsolute(sourceURL, parsedData);
246    } catch (err) {
247      debug(err);
248      return null;
249    }
250  } else {
251    debug(`unknown content-type ${contentType}`);
252    return null;
253  }
254}
255
256// If the sources are not absolute URLs after prepending of the "sourceRoot",
257// the sources are resolved relative to the SourceMap (like resolving script
258// src in a html document).
259function sourcesToAbsolute(baseURL, data) {
260  data.sources = data.sources.map((source) => {
261    source = (data.sourceRoot || '') + source;
262    return new URL(source, baseURL).href;
263  });
264  // The sources array is now resolved to absolute URLs, sourceRoot should
265  // be updated to noop.
266  data.sourceRoot = '';
267  return data;
268}
269
270// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
271// shutdown. In particular, they also run when Workers are terminated, making
272// it important that they do not call out to any user-provided code, including
273// built-in prototypes that might have been tampered with.
274
275// Get serialized representation of source-map cache, this is used
276// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
277function sourceMapCacheToObject() {
278  const obj = ObjectCreate(null);
279
280  for (const { 0: k, 1: v } of esmSourceMapCache) {
281    obj[k] = v;
282  }
283
284  appendCJSCache(obj);
285
286  if (ObjectKeys(obj).length === 0) {
287    return undefined;
288  }
289  return obj;
290}
291
292function appendCJSCache(obj) {
293  for (const value of getCjsSourceMapCache()) {
294    obj[ObjectGetValueSafe(value, 'filename')] = {
295      lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
296      data: ObjectGetValueSafe(value, 'data'),
297      url: ObjectGetValueSafe(value, 'url'),
298    };
299  }
300}
301
302function findSourceMap(sourceURL) {
303  if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
304    sourceURL = pathToFileURL(sourceURL).href;
305  }
306  if (!SourceMap) {
307    SourceMap = require('internal/source_map/source_map').SourceMap;
308  }
309  let sourceMap = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
310  if (sourceMap === undefined) {
311    for (const value of getCjsSourceMapCache()) {
312      const filename = ObjectGetValueSafe(value, 'filename');
313      const cachedSourceURL = ObjectGetValueSafe(value, 'sourceURL');
314      if (sourceURL === filename || sourceURL === cachedSourceURL) {
315        sourceMap = {
316          data: ObjectGetValueSafe(value, 'data'),
317        };
318      }
319    }
320  }
321  if (sourceMap && sourceMap.data) {
322    return new SourceMap(sourceMap.data);
323  }
324  return undefined;
325}
326
327module.exports = {
328  findSourceMap,
329  getSourceMapsEnabled,
330  setSourceMapsEnabled,
331  maybeCacheSourceMap,
332  maybeCacheGeneratedSourceMap,
333  sourceMapCacheToObject,
334};
335