summaryrefslogtreecommitdiff
path: root/lib/internal/source_map
diff options
context:
space:
mode:
authorbcoe <bencoe@google.com>2019-09-29 14:15:39 -0700
committerBenjamin Coe <bencoe@google.com>2019-10-05 17:08:00 -0700
commite1e2f669f65fd53323b8a58d80ed3cee039706b7 (patch)
tree582baba1511e57a9915b379d0f263cb29fb6c38f /lib/internal/source_map
parent739f113ba63367a93e1567032d85573a079b97b5 (diff)
downloadandroid-node-v8-e1e2f669f65fd53323b8a58d80ed3cee039706b7.tar.gz
android-node-v8-e1e2f669f65fd53323b8a58d80ed3cee039706b7.tar.bz2
android-node-v8-e1e2f669f65fd53323b8a58d80ed3cee039706b7.zip
process: add source-map support to stack traces
PR-URL: https://github.com/nodejs/node/pull/29564 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
Diffstat (limited to 'lib/internal/source_map')
-rw-r--r--lib/internal/source_map/source_map.js301
-rw-r--r--lib/internal/source_map/source_map_cache.js237
2 files changed, 538 insertions, 0 deletions
diff --git a/lib/internal/source_map/source_map.js b/lib/internal/source_map/source_map.js
new file mode 100644
index 0000000000..9044521b6d
--- /dev/null
+++ b/lib/internal/source_map/source_map.js
@@ -0,0 +1,301 @@
+// This file is a modified version of:
+// https://cs.chromium.org/chromium/src/v8/tools/SourceMap.js?rcl=dd10454c1d
+// from the V8 codebase. Logic specific to WebInspector is removed and linting
+// is made to match the Node.js style guide.
+
+// Copyright 2013 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived
+// from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This is a copy from blink dev tools, see:
+// http://src.chromium.org/viewvc/blink/trunk/Source/devtools/front_end/SourceMap.js
+// revision: 153407
+
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+'use strict';
+
+let base64Map;
+
+const VLQ_BASE_SHIFT = 5;
+const VLQ_BASE_MASK = (1 << 5) - 1;
+const VLQ_CONTINUATION_MASK = 1 << 5;
+
+class StringCharIterator {
+ /**
+ * @constructor
+ * @param {string} string
+ */
+ constructor(string) {
+ this._string = string;
+ this._position = 0;
+ }
+
+ /**
+ * @return {string}
+ */
+ next() {
+ return this._string.charAt(this._position++);
+ }
+
+ /**
+ * @return {string}
+ */
+ peek() {
+ return this._string.charAt(this._position);
+ }
+
+ /**
+ * @return {boolean}
+ */
+ hasNext() {
+ return this._position < this._string.length;
+ }
+}
+
+/**
+ * Implements Source Map V3 model. See http://code.google.com/p/closure-compiler/wiki/SourceMaps
+ * for format description.
+ * @constructor
+ * @param {string} sourceMappingURL
+ * @param {SourceMapV3} payload
+ */
+class SourceMap {
+ #reverseMappingsBySourceURL = [];
+ #mappings = [];
+ #sources = {};
+ #sourceContentByURL = {};
+
+ /**
+ * @constructor
+ * @param {SourceMapV3} payload
+ */
+ constructor(payload) {
+ if (!base64Map) {
+ const base64Digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ base64Map = {};
+ for (let i = 0; i < base64Digits.length; ++i)
+ base64Map[base64Digits[i]] = i;
+ }
+ this.#parseMappingPayload(payload);
+ }
+
+ /**
+ * @param {SourceMapV3} mappingPayload
+ */
+ #parseMappingPayload = (mappingPayload) => {
+ if (mappingPayload.sections)
+ this.#parseSections(mappingPayload.sections);
+ else
+ this.#parseMap(mappingPayload, 0, 0);
+ }
+
+ /**
+ * @param {Array.<SourceMapV3.Section>} sections
+ */
+ #parseSections = (sections) => {
+ for (let i = 0; i < sections.length; ++i) {
+ const section = sections[i];
+ this.#parseMap(section.map, section.offset.line, section.offset.column);
+ }
+ }
+
+ /**
+ * @param {number} lineNumber in compiled resource
+ * @param {number} columnNumber in compiled resource
+ * @return {?Array}
+ */
+ findEntry(lineNumber, columnNumber) {
+ let first = 0;
+ let count = this.#mappings.length;
+ while (count > 1) {
+ const step = count >> 1;
+ const middle = first + step;
+ const mapping = this.#mappings[middle];
+ if (lineNumber < mapping[0] ||
+ (lineNumber === mapping[0] && columnNumber < mapping[1])) {
+ count = step;
+ } else {
+ first = middle;
+ count -= step;
+ }
+ }
+ const entry = this.#mappings[first];
+ if (!first && entry && (lineNumber < entry[0] ||
+ (lineNumber === entry[0] && columnNumber < entry[1]))) {
+ return null;
+ }
+ return entry;
+ }
+
+ /**
+ * @param {string} sourceURL of the originating resource
+ * @param {number} lineNumber in the originating resource
+ * @return {Array}
+ */
+ findEntryReversed(sourceURL, lineNumber) {
+ const mappings = this.#reverseMappingsBySourceURL[sourceURL];
+ for (; lineNumber < mappings.length; ++lineNumber) {
+ const mapping = mappings[lineNumber];
+ if (mapping)
+ return mapping;
+ }
+ return this.#mappings[0];
+ }
+
+ /**
+ * @override
+ */
+ #parseMap = (map, lineNumber, columnNumber) => {
+ let sourceIndex = 0;
+ let sourceLineNumber = 0;
+ let sourceColumnNumber = 0;
+
+ const sources = [];
+ const originalToCanonicalURLMap = {};
+ for (let i = 0; i < map.sources.length; ++i) {
+ const url = map.sources[i];
+ originalToCanonicalURLMap[url] = url;
+ sources.push(url);
+ this.#sources[url] = true;
+
+ if (map.sourcesContent && map.sourcesContent[i])
+ this.#sourceContentByURL[url] = map.sourcesContent[i];
+ }
+
+ const stringCharIterator = new StringCharIterator(map.mappings);
+ let sourceURL = sources[sourceIndex];
+
+ while (true) {
+ if (stringCharIterator.peek() === ',')
+ stringCharIterator.next();
+ else {
+ while (stringCharIterator.peek() === ';') {
+ lineNumber += 1;
+ columnNumber = 0;
+ stringCharIterator.next();
+ }
+ if (!stringCharIterator.hasNext())
+ break;
+ }
+
+ columnNumber += decodeVLQ(stringCharIterator);
+ if (isSeparator(stringCharIterator.peek())) {
+ this.#mappings.push([lineNumber, columnNumber]);
+ continue;
+ }
+
+ const sourceIndexDelta = decodeVLQ(stringCharIterator);
+ if (sourceIndexDelta) {
+ sourceIndex += sourceIndexDelta;
+ sourceURL = sources[sourceIndex];
+ }
+ sourceLineNumber += decodeVLQ(stringCharIterator);
+ sourceColumnNumber += decodeVLQ(stringCharIterator);
+ if (!isSeparator(stringCharIterator.peek()))
+ // Unused index into the names list.
+ decodeVLQ(stringCharIterator);
+
+ this.#mappings.push([lineNumber, columnNumber, sourceURL,
+ sourceLineNumber, sourceColumnNumber]);
+ }
+
+ for (let i = 0; i < this.#mappings.length; ++i) {
+ const mapping = this.#mappings[i];
+ const url = mapping[2];
+ if (!url)
+ continue;
+ if (!this.#reverseMappingsBySourceURL[url])
+ this.#reverseMappingsBySourceURL[url] = [];
+ const reverseMappings = this.#reverseMappingsBySourceURL[url];
+ const sourceLine = mapping[3];
+ if (!reverseMappings[sourceLine])
+ reverseMappings[sourceLine] = [mapping[0], mapping[1]];
+ }
+ };
+}
+
+/**
+ * @param {string} char
+ * @return {boolean}
+ */
+function isSeparator(char) {
+ return char === ',' || char === ';';
+}
+
+/**
+ * @param {SourceMap.StringCharIterator} stringCharIterator
+ * @return {number}
+ */
+function decodeVLQ(stringCharIterator) {
+ // Read unsigned value.
+ let result = 0;
+ let shift = 0;
+ let digit;
+ do {
+ digit = base64Map[stringCharIterator.next()];
+ result += (digit & VLQ_BASE_MASK) << shift;
+ shift += VLQ_BASE_SHIFT;
+ } while (digit & VLQ_CONTINUATION_MASK);
+
+ // Fix the sign.
+ const negative = result & 1;
+ result >>= 1;
+ return negative ? -result : result;
+}
+
+module.exports = {
+ SourceMap
+};
diff --git a/lib/internal/source_map/source_map_cache.js b/lib/internal/source_map/source_map_cache.js
new file mode 100644
index 0000000000..94a4165546
--- /dev/null
+++ b/lib/internal/source_map/source_map_cache.js
@@ -0,0 +1,237 @@
+'use strict';
+
+// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
+const { Buffer } = require('buffer');
+const debug = require('internal/util/debuglog').debuglog('source_map');
+const { dirname, resolve } = require('path');
+const fs = require('fs');
+const { getOptionValue } = require('internal/options');
+const {
+ normalizeReferrerURL,
+} = require('internal/modules/cjs/helpers');
+const { JSON, Object } = primordials;
+// For cjs, since Module._cache is exposed to users, we use a WeakMap
+// keyed on module, facilitating garbage collection.
+const cjsSourceMapCache = new WeakMap();
+// The esm cache is not exposed to users, so we can use a Map keyed
+// on filenames.
+const esmSourceMapCache = new Map();
+const { fileURLToPath, URL } = require('url');
+const { overrideStackTrace } = require('internal/errors');
+
+let experimentalSourceMaps;
+function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
+ if (experimentalSourceMaps === undefined) {
+ experimentalSourceMaps = getOptionValue('--enable-source-maps');
+ }
+ if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return;
+ let basePath;
+ try {
+ filename = normalizeReferrerURL(filename);
+ basePath = dirname(fileURLToPath(filename));
+ } catch (err) {
+ // This is most likely an [eval]-wrapper, which is currently not
+ // supported.
+ debug(err.stack);
+ return;
+ }
+
+ const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
+ if (match) {
+ if (cjsModuleInstance) {
+ cjsSourceMapCache.set(cjsModuleInstance, {
+ filename,
+ url: match.groups.sourceMappingURL,
+ data: dataFromUrl(basePath, match.groups.sourceMappingURL)
+ });
+ } else {
+ // If there is no cjsModuleInstance assume we are in a
+ // "modules/esm" context.
+ esmSourceMapCache.set(filename, {
+ url: match.groups.sourceMappingURL,
+ data: dataFromUrl(basePath, match.groups.sourceMappingURL)
+ });
+ }
+ }
+}
+
+function dataFromUrl(basePath, sourceMappingURL) {
+ try {
+ const url = new URL(sourceMappingURL);
+ switch (url.protocol) {
+ case 'data:':
+ return sourceMapFromDataUrl(basePath, url.pathname);
+ default:
+ debug(`unknown protocol ${url.protocol}`);
+ return null;
+ }
+ } catch (err) {
+ debug(err.stack);
+ // If no scheme is present, we assume we are dealing with a file path.
+ const sourceMapFile = resolve(basePath, sourceMappingURL);
+ return sourceMapFromFile(sourceMapFile);
+ }
+}
+
+function sourceMapFromFile(sourceMapFile) {
+ try {
+ const content = fs.readFileSync(sourceMapFile, 'utf8');
+ const data = JSON.parse(content);
+ return sourcesToAbsolute(dirname(sourceMapFile), data);
+ } catch (err) {
+ debug(err.stack);
+ return null;
+ }
+}
+
+// data:[<mediatype>][;base64],<data> see:
+// https://tools.ietf.org/html/rfc2397#section-2
+function sourceMapFromDataUrl(basePath, url) {
+ const [format, data] = url.split(',');
+ const splitFormat = format.split(';');
+ const contentType = splitFormat[0];
+ const base64 = splitFormat[splitFormat.length - 1] === 'base64';
+ if (contentType === 'application/json') {
+ const decodedData = base64 ?
+ Buffer.from(data, 'base64').toString('utf8') : data;
+ try {
+ const parsedData = JSON.parse(decodedData);
+ return sourcesToAbsolute(basePath, parsedData);
+ } catch (err) {
+ debug(err.stack);
+ return null;
+ }
+ } else {
+ debug(`unknown content-type ${contentType}`);
+ return null;
+ }
+}
+
+// If the sources are not absolute URLs after prepending of the "sourceRoot",
+// the sources are resolved relative to the SourceMap (like resolving script
+// src in a html document).
+function sourcesToAbsolute(base, data) {
+ data.sources = data.sources.map((source) => {
+ source = (data.sourceRoot || '') + source;
+ if (!/^[\\/]/.test(source[0])) {
+ source = resolve(base, source);
+ }
+ if (!source.startsWith('file://')) source = `file://${source}`;
+ return source;
+ });
+ // The sources array is now resolved to absolute URLs, sourceRoot should
+ // be updated to noop.
+ data.sourceRoot = '';
+ return data;
+}
+
+// Move source map from garbage collected module to alternate key.
+function rekeySourceMap(cjsModuleInstance, newInstance) {
+ const sourceMap = cjsSourceMapCache.get(cjsModuleInstance);
+ if (sourceMap) {
+ cjsSourceMapCache.set(newInstance, sourceMap);
+ }
+}
+
+// Get serialized representation of source-map cache, this is used
+// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
+function sourceMapCacheToObject() {
+ const obj = Object.create(null);
+
+ for (const [k, v] of esmSourceMapCache) {
+ obj[k] = v;
+ }
+ appendCJSCache(obj);
+
+ if (Object.keys(obj).length === 0) {
+ return undefined;
+ } else {
+ return obj;
+ }
+}
+
+// Since WeakMap can't be iterated over, we use Module._cache's
+// keys to facilitate Source Map serialization.
+//
+// TODO(bcoe): this means we don't currently serialize source-maps attached
+// to error instances, only module instances.
+function appendCJSCache(obj) {
+ const { Module } = require('internal/modules/cjs/loader');
+ Object.keys(Module._cache).forEach((key) => {
+ const value = cjsSourceMapCache.get(Module._cache[key]);
+ if (value) {
+ obj[`file://${key}`] = {
+ url: value.url,
+ data: value.data
+ };
+ }
+ });
+}
+
+// Create a prettified stacktrace, inserting context from source maps
+// if possible.
+const ErrorToString = Error.prototype.toString; // Capture original toString.
+const prepareStackTrace = (globalThis, error, trace) => {
+ // API for node internals to override error stack formatting
+ // without interfering with userland code.
+ // TODO(bcoe): add support for source-maps to repl.
+ if (overrideStackTrace.has(error)) {
+ const f = overrideStackTrace.get(error);
+ overrideStackTrace.delete(error);
+ return f(error, trace);
+ }
+
+ const { SourceMap } = require('internal/source_map/source_map');
+ const errorString = ErrorToString.call(error);
+
+ if (trace.length === 0) {
+ return errorString;
+ }
+ const preparedTrace = trace.map((t, i) => {
+ let str = i !== 0 ? '\n at ' : '';
+ str = `${str}${t}`;
+ try {
+ const sourceMap = findSourceMap(t.getFileName(), error);
+ if (sourceMap && sourceMap.data) {
+ const sm = new SourceMap(sourceMap.data);
+ // Source Map V3 lines/columns use zero-based offsets whereas, in
+ // stack traces, they start at 1/1.
+ const [, , url, line, col] =
+ sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
+ if (url && line !== undefined && col !== undefined) {
+ str +=
+ `\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
+ }
+ }
+ } catch (err) {
+ debug(err.stack);
+ }
+ return str;
+ });
+ return `${errorString}\n at ${preparedTrace.join('')}`;
+};
+
+// Attempt to lookup a source map, which is either attached to a file URI, or
+// keyed on an error instance.
+function findSourceMap(uri, error) {
+ const { Module } = require('internal/modules/cjs/loader');
+ let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
+ if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
+ if (sourceMap === undefined) {
+ sourceMap = esmSourceMapCache.get(uri);
+ }
+ if (sourceMap === undefined) {
+ const candidateSourceMap = cjsSourceMapCache.get(error);
+ if (candidateSourceMap && uri === candidateSourceMap.filename) {
+ sourceMap = candidateSourceMap;
+ }
+ }
+ return sourceMap;
+}
+
+module.exports = {
+ maybeCacheSourceMap,
+ prepareStackTrace,
+ rekeySourceMap,
+ sourceMapCacheToObject,
+};