blob: 89e8425f17e112e060a2fc6272943b5c2b8de33a [file] [edit]
// 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.
import 'dart:typed_data';
import 'package:source_maps/parser.dart';
import 'debug_info.dart';
export 'debug_info.dart';
/// Builder that collects function-level byte debug info during serialization
/// and generates Source Map v3 JSON.
class SourceMapBuilder implements DebugInfoSerializer {
final DebugInfoTables? debugInfoTables;
final List<(int, Uint8List)> _functions = [];
int? _codeSectionFileOffset;
SourceMapBuilder(this.debugInfoTables);
@override
void addFunction(int functionCodeOffset, Uint8List byteDebugInfo) {
if (byteDebugInfo.isNotEmpty) {
_functions.add((functionCodeOffset, byteDebugInfo));
}
}
@override
void setCodeSectionFileOffset(int fileOffset) {
_codeSectionFileOffset = fileOffset;
}
Map<String, Object?> toJson() {
final filesInfo = debugInfoTables;
if (filesInfo == null || _functions.isEmpty) {
return <String, Object?>{
"version": 3,
"sources":
filesInfo?.files.map((uri) => uri.toString()).toList() ??
const <String>[],
"names": filesInfo?.names ?? const <String>[],
"mappings": "",
};
}
final codeSectionFileOffset = _codeSectionFileOffset!;
final StringBuffer mappingsStr = StringBuffer();
int lastTargetColumn = 0;
int lastSourceIndex = 0;
int lastSourceLine = 0;
int lastSourceColumn = 0;
int lastNameIndex = 0;
bool firstSegment = true;
for (final (functionCodeOffset, byteDebugInfo) in _functions) {
final baseOffset = codeSectionFileOffset + functionCodeOffset;
final reader = DebugInfoReader(byteDebugInfo);
while (reader.moveNext()) {
if (!firstSegment) {
mappingsStr.write(',');
}
firstSegment = false;
final targetOffset = baseOffset + reader.offset;
lastTargetColumn = _encodeVLQ(
mappingsStr,
targetOffset,
lastTargetColumn,
);
if (reader.hasSourcePosition) {
lastSourceIndex = _encodeVLQ(
mappingsStr,
reader.fileIndex,
lastSourceIndex,
);
lastSourceLine = _encodeVLQ(mappingsStr, reader.line, lastSourceLine);
lastSourceColumn = _encodeVLQ(
mappingsStr,
reader.col,
lastSourceColumn,
);
if (reader.nameIndex >= 0) {
lastNameIndex = _encodeVLQ(
mappingsStr,
reader.nameIndex,
lastNameIndex,
);
}
}
}
}
return <String, Object?>{
"version": 3,
"sources": filesInfo.files.map((uri) => uri.toString()).toList(),
"names": filesInfo.names,
"mappings": mappingsStr.toString(),
};
}
}
/// Streaming decoder that decodes Source Map v3 JSON into instruction-level
/// debug info during Wasm module deserialization.
class SourceMapDecoder implements DebugInfoDeserializer {
@override
final DebugInfoTables debugInfoTables;
final SingleMapping _mapping;
int _entryIdx = 0;
DebugInfoWriter? _writer;
int? _pendingInstructionIndex;
SourceMapDecoder(this.debugInfoTables, this._mapping);
factory SourceMapDecoder.fromJson(Map<String, dynamic> json) {
final mapping = SingleMapping.fromJson(json);
final debugInfoTables = DebugInfoTables();
for (final url in mapping.urls) {
debugInfoTables.files.add(Uri.parse(url));
}
debugInfoTables.names.addAll(mapping.names);
return SourceMapDecoder(debugInfoTables, mapping);
}
List<TargetEntry> get _entries =>
_mapping.lines.isNotEmpty ? _mapping.lines[0].entries : const [];
@override
void startFunction(int fileOffset) {
_writer = DebugInfoWriter(debugInfoTables);
_pendingInstructionIndex = null;
final entries = _entries;
while (_entryIdx < entries.length &&
entries[_entryIdx].column < fileOffset) {
_entryIdx++;
}
}
@override
void onInstruction(int instructionIndex, int fileOffset) {
if (_pendingInstructionIndex != null) {
// Process entries that belong to the previous instruction
_flushEntriesUpTo(fileOffset, _pendingInstructionIndex!);
}
_pendingInstructionIndex = instructionIndex;
}
@override
Uint8List? endFunction(int fileOffset) {
if (_pendingInstructionIndex != null) {
// Process entries for the last instruction in the function
_flushEntriesUpTo(fileOffset, _pendingInstructionIndex!);
_pendingInstructionIndex = null;
}
final writer = _writer;
_writer = null;
if (writer == null || writer.isEmpty) return null;
return writer.build();
}
void _flushEntriesUpTo(int limitFileOffset, int instructionIndex) {
final writer = _writer!;
final entries = _entries;
while (_entryIdx < entries.length &&
entries[_entryIdx].column < limitFileOffset) {
final entry = entries[_entryIdx];
if (entry.sourceUrlId != null) {
writer.setSourcePositionWithIndices(
instructionIndex,
entry.sourceUrlId!,
entry.sourceLine!,
entry.sourceColumn!,
entry.sourceNameId ?? -1,
);
} else {
writer.clearSourcePosition(instructionIndex);
}
_entryIdx++;
}
}
}
/// 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+/';