| // 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+/'; |