992a07a552
Reuse dart2js's source map extension format to map minified class names
shown in error messages and runtime type strings ("minified:Class123")
to original class names.
The extension field is only generated when minifying, and with multiple
modules, only in the main module's source map.
- Without minification runtime type strings and error messages already
include the full class name, so mapping is not necessary.
- With multiple modules, the existing tools use the main module's source
map to deobfuscate errors.
Because wasm-opt is not aware of this custom section and it also changes
the names section, when optimizing, we read the original names before
calling wasm-opt, and write the section back to the source map file for
the optimized Wasm.
When not optimizing we generate the source map with the custom section
directly.
Example deobfuscation using the new source maps:
$ dart pkg/dart2js_tools/bin/lookup_name.dart test.wasm.map Class123
Class123 => AsyncError (a global name)
The custom section is a bit verbose for what dart2wasm needs: we could
map numbers to name indices instead of strings to name indices, as
dart2wasm minifies class names to numbers. However to avoid updating a
bunch of tools in g3, SDK, maybe also in Flutter and devtools, we reuse
dart2js's format, at least for now.
Issue: https://github.com/dart-lang/sdk/issues/60711
Change-Id: I2cda331723c6f0c7e7ef5f4772feaf420dc8c6ba
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/474660
Commit-Queue: Ömer Ağacan <omersa@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
207 lines
5.8 KiB
Dart
207 lines
5.8 KiB
Dart
// Copyright (c) 2024, 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.
|
|
|
|
/// Represents a mapping from a range of generated instructions to some source
|
|
/// code.
|
|
class SourceMapping {
|
|
/// Start offset of mapped instructions.
|
|
final int instructionOffset;
|
|
|
|
/// Source info for the mapped instructions starting at [instructionOffset].
|
|
///
|
|
/// When `null`, the mapping effectively makes the code unmapped. This is
|
|
/// useful for compiler-generated code that doesn't correstpond to any lines
|
|
/// in the source.
|
|
final SourceInfo? sourceInfo;
|
|
|
|
SourceMapping._(this.instructionOffset, this.sourceInfo);
|
|
|
|
SourceMapping(
|
|
this.instructionOffset, Uri fileUri, int line, int col, String? name)
|
|
: sourceInfo = SourceInfo(fileUri, line, col, name);
|
|
|
|
SourceMapping.unmapped(this.instructionOffset) : sourceInfo = null;
|
|
|
|
SourceMapping shiftBy(int shift) {
|
|
if (shift == 0) return this;
|
|
return SourceMapping._(shift + instructionOffset, sourceInfo);
|
|
}
|
|
|
|
@override
|
|
String toString() => '$instructionOffset -> $sourceInfo';
|
|
}
|
|
|
|
class SourceInfo {
|
|
/// URI of the compiled code's file.
|
|
final Uri fileUri;
|
|
|
|
/// 0-based line number of the compiled code.
|
|
final int line;
|
|
|
|
/// 0-based column number of the compiled code.
|
|
final int col;
|
|
|
|
/// Name of the mapped code. This is usually the name of the function that
|
|
/// contains the code.
|
|
final String? name;
|
|
|
|
SourceInfo(this.fileUri, this.line, this.col, this.name);
|
|
|
|
@override
|
|
String toString() => '$fileUri:$line:$col ($name)';
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) {
|
|
return true;
|
|
}
|
|
|
|
if (other is! SourceInfo) {
|
|
return false;
|
|
}
|
|
|
|
return fileUri == other.fileUri &&
|
|
line == other.line &&
|
|
col == other.col &&
|
|
name == other.name;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(fileUri, line, col, name);
|
|
}
|
|
|
|
class SourceMapSerializer {
|
|
final List<SourceMapping> mappings = [];
|
|
|
|
void addMapping(int instructionOffset, SourceInfo? sourceInfo) {
|
|
final mapping = SourceMapping._(instructionOffset, sourceInfo);
|
|
mappings.add(mapping);
|
|
}
|
|
|
|
void copyMappings(SourceMapSerializer other, int offset) {
|
|
for (final mapping in other.mappings) {
|
|
mappings.add(SourceMapping._(
|
|
mapping.instructionOffset + offset,
|
|
mapping.sourceInfo,
|
|
));
|
|
}
|
|
}
|
|
|
|
Map<String, Object?> serializeAsJson() => _sourceMapToJson(mappings);
|
|
}
|
|
|
|
Map<String, Object?> _sourceMapToJson(List<SourceMapping> mappings) {
|
|
final Set<Uri> sourcesSet = {};
|
|
for (final mapping in mappings) {
|
|
if (mapping.sourceInfo?.fileUri != null) {
|
|
sourcesSet.add(mapping.sourceInfo!.fileUri);
|
|
}
|
|
}
|
|
|
|
final List<Uri> sourcesList = sourcesSet.toList();
|
|
|
|
// Maps sources to their indices in the 'sources' list.
|
|
final Map<Uri, int> sourceIndices = {};
|
|
for (Uri source in sourcesList) {
|
|
sourceIndices[source] = sourceIndices.length;
|
|
}
|
|
|
|
final Set<String> namesSet = {};
|
|
for (final mapping in mappings) {
|
|
if (mapping.sourceInfo?.name != null) {
|
|
namesSet.add(mapping.sourceInfo!.name!);
|
|
}
|
|
}
|
|
|
|
final List<String> namesList = namesSet.toList();
|
|
|
|
// Maps names to their index in the 'names' list.
|
|
final Map<String, int> nameIndices = {};
|
|
for (String name in namesList) {
|
|
nameIndices[name] = nameIndices.length;
|
|
}
|
|
|
|
// Generate the 'mappings' field.
|
|
final StringBuffer mappingsStr = StringBuffer();
|
|
|
|
int lastTargetColumn = 0;
|
|
int lastSourceIndex = 0;
|
|
int lastSourceLine = 0;
|
|
int lastSourceColumn = 0;
|
|
int lastNameIndex = 0;
|
|
|
|
bool first = true;
|
|
|
|
for (int i = 0; i < mappings.length; ++i) {
|
|
final mapping = mappings[i];
|
|
final sourceInfo = mapping.sourceInfo;
|
|
|
|
if (sourceInfo == null && first) {
|
|
// Initial parts of the code will be unmapped my default, we don't need to
|
|
// explicitly unmap them. More importantly, current version of binaryen
|
|
// cannot handle single-segment mappings at the beginning of the mappings.
|
|
// We can remove this block of code after switching to a version with
|
|
// https://github.com/WebAssembly/binaryen/pull/6794.
|
|
continue;
|
|
}
|
|
|
|
first = false;
|
|
|
|
lastTargetColumn =
|
|
_encodeVLQ(mappingsStr, mapping.instructionOffset, lastTargetColumn);
|
|
|
|
if (sourceInfo != null) {
|
|
final sourceIndex = sourceIndices[sourceInfo.fileUri]!;
|
|
|
|
lastSourceIndex = _encodeVLQ(mappingsStr, sourceIndex, lastSourceIndex);
|
|
lastSourceLine = _encodeVLQ(mappingsStr, sourceInfo.line, lastSourceLine);
|
|
lastSourceColumn =
|
|
_encodeVLQ(mappingsStr, sourceInfo.col, lastSourceColumn);
|
|
|
|
if (sourceInfo.name != null) {
|
|
final nameIndex = nameIndices[sourceInfo.name!]!;
|
|
lastNameIndex = _encodeVLQ(mappingsStr, nameIndex, lastNameIndex);
|
|
}
|
|
}
|
|
|
|
if (i != mappings.length - 1) {
|
|
mappingsStr.write(',');
|
|
}
|
|
}
|
|
|
|
return <String, Object?>{
|
|
"version": 3,
|
|
"sources": sourcesList.map((uri) => uri.toString()).toList(),
|
|
"names": namesList,
|
|
"mappings": mappingsStr.toString(),
|
|
};
|
|
}
|
|
|
|
/// Writes the VLQ of delta between [value] and [offset] into [output] and
|
|
/// return [value].
|
|
int _encodeVLQ(StringSink output, int value, int offset) {
|
|
int delta = value - offset;
|
|
int signBit = 0;
|
|
if (delta < 0) {
|
|
signBit = 1;
|
|
delta = -delta;
|
|
}
|
|
delta = (delta << 1) | signBit;
|
|
do {
|
|
int digit = delta & _vlqBaseMask;
|
|
delta >>= _vlqBaseShift;
|
|
if (delta > 0) {
|
|
digit |= _vlqContinuationBit;
|
|
}
|
|
output.write(_base64Digits[digit]);
|
|
} while (delta > 0);
|
|
return value;
|
|
}
|
|
|
|
const int _vlqBaseShift = 5;
|
|
const int _vlqBaseMask = (1 << 5) - 1;
|
|
const int _vlqContinuationBit = 1 << 5;
|
|
const String _base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn'
|
|
'opqrstuvwxyz0123456789+/';
|