blob: cae4c2289c467f3aeae1a9f560a2230a74eed56b [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Logic to expand and deobuscate stack traces.
import 'package:stack_trace/stack_trace.dart';
import 'package:source_span/source_span.dart';
import 'package:source_maps/src/utils.dart';
import 'sourcemap_helper.dart';
import 'dart2js_mapping.dart';
import 'util.dart';
/// Provides the result of deobfuscating a stack trace.
class StackDeobfuscationResult {
/// Representation of the obfuscated stack trace.
final Trace original;
/// Representation of the deobfsucated stack trace.
final Trace deobfuscated;
/// Details about how one original frame maps to deobfuscated frames. A single
/// frame might map to many frames (in the case of inlining), or to a null
/// value (when we were unabled to deobfuscate it).
final Map<Frame, List<Frame>> frameMap;
StackDeobfuscationResult(this.original, this.deobfuscated, this.frameMap);
}
/// Parse [stackTrace] and deobfuscate it using source-map data available from
/// [provider].
StackDeobfuscationResult deobfuscateStack(
String stackTrace, FileProvider provider) {
var trace = Trace.parse(stackTrace.trim());
var deobfuscatedFrames = <Frame>[];
var frameMap = <Frame, List<Frame>>{};
for (var frame in trace.frames) {
// If there's no line information, there's no way to translate this frame.
// We could return it as-is, but these lines are usually not useful anyways.
if (frame.line == null) {
continue;
}
// If there's no column, try using the first column of the line.
var column = frame.column ?? 0;
Dart2jsMapping mapping = provider.mappingFor(frame.uri);
if (mapping == null) continue;
// Subtract 1 because stack traces use 1-indexed lines and columns and
// source maps uses 0-indexed.
SourceSpan span = mapping.sourceMap
.spanFor(frame.line - 1, column - 1, uri: frame.uri?.toString());
// If we can't find a source span, ignore the frame. It's probably something
// internal that the user doesn't care about.
if (span == null) continue;
List<Frame> mappedFrames = frameMap[frame] = [];
SourceFile jsFile = provider.fileFor(frame.uri);
int offset = jsFile.getOffset(frame.line - 1, column - 1);
String nameOf(id) =>
_normalizeName(id >= 0 ? mapping.sourceMap.names[id] : null);
Uri fileName = span.sourceUrl;
int targetLine = span.start.line + 1;
int targetColumn = span.start.column + 1;
// Expand inlining data. When present, the fileName, line and column above
// correspond to the deepest inlined function, as we expand each frame we
// consume the location information, and retrieve the location information
// of the caller frame until we reach the actual function that dart2js
// inlined all the code into.
Map<int, List<FrameEntry>> frames = mapping.frames;
List<int> index = mapping.frameIndex;
int key = binarySearch(index, (i) => i > offset) - 1;
int depth = 0;
outer:
while (key >= 0) {
for (var frame in frames[index[key]].reversed) {
if (frame.isEmpty) break outer;
if (frame.isPush) {
if (depth <= 0) {
mappedFrames.add(new Frame(fileName, targetLine, targetColumn,
_normalizeName(frame.inlinedMethodName) + "(inlined)"));
fileName = Uri.parse(frame.callUri);
targetLine = (frame.callLine ?? 0) + 1;
targetColumn = (frame.callColumn ?? 0) + 1;
} else {
depth--;
}
}
if (frame.isPop) {
depth++;
}
}
key--;
}
var functionEntry = findEnclosingFunction(provider, frame.uri, offset);
String methodName = nameOf(functionEntry?.sourceNameId ?? -1);
mappedFrames.add(new Frame(fileName, targetLine, targetColumn, methodName));
deobfuscatedFrames.addAll(mappedFrames);
}
return new StackDeobfuscationResult(
trace, new Trace(deobfuscatedFrames), frameMap);
}
/// Ensure we don't use spaces in method names. At this time, they are only
/// introduced by `<anonymous function>`.
_normalizeName(String methodName) =>
methodName?.replaceAll("<anonymous function>", "<anonymous>") ??
'<unknown>';