|
| 1 | +import 'dart:typed_data'; |
| 2 | +import 'package:meta/meta.dart'; |
| 3 | +import 'package:uuid/uuid.dart'; |
| 4 | + |
| 5 | +import '../sentry.dart'; |
| 6 | + |
| 7 | +// Regular expressions for parsing header lines |
| 8 | +const String _headerStartLine = |
| 9 | + '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'; |
| 10 | +final RegExp _buildIdRegex = RegExp(r"build_id(?:=|: )'([\da-f]+)'"); |
| 11 | +final RegExp _isolateDsoBaseLineRegex = |
| 12 | + RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)'); |
| 13 | + |
| 14 | +/// Extracts debug information from stack trace header. |
| 15 | +/// Needed for symbolication of Dart stack traces without native debug images. |
| 16 | +@internal |
| 17 | +class DebugImageExtractor { |
| 18 | + DebugImageExtractor(this._options); |
| 19 | + |
| 20 | + final SentryOptions _options; |
| 21 | + |
| 22 | + // We don't need to always parse the debug image, so we cache it here. |
| 23 | + DebugImage? _debugImage; |
| 24 | + |
| 25 | + @visibleForTesting |
| 26 | + DebugImage? get debugImageForTesting => _debugImage; |
| 27 | + |
| 28 | + DebugImage? extractFrom(String stackTraceString) { |
| 29 | + if (_debugImage != null) { |
| 30 | + return _debugImage; |
| 31 | + } |
| 32 | + _debugImage = _extractDebugInfoFrom(stackTraceString).toDebugImage(); |
| 33 | + return _debugImage; |
| 34 | + } |
| 35 | + |
| 36 | + _DebugInfo _extractDebugInfoFrom(String stackTraceString) { |
| 37 | + String? buildId; |
| 38 | + String? isolateDsoBase; |
| 39 | + |
| 40 | + final lines = stackTraceString.split('\n'); |
| 41 | + |
| 42 | + for (final line in lines) { |
| 43 | + if (_isHeaderStartLine(line)) { |
| 44 | + continue; |
| 45 | + } |
| 46 | + // Stop parsing as soon as we get to the stack frames |
| 47 | + // This should never happen but is a safeguard to avoid looping |
| 48 | + // through every line of the stack trace |
| 49 | + if (line.contains("#00 abs")) { |
| 50 | + break; |
| 51 | + } |
| 52 | + |
| 53 | + buildId ??= _extractBuildId(line); |
| 54 | + isolateDsoBase ??= _extractIsolateDsoBase(line); |
| 55 | + |
| 56 | + // Early return if all needed information is found |
| 57 | + if (buildId != null && isolateDsoBase != null) { |
| 58 | + return _DebugInfo(buildId, isolateDsoBase, _options); |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + return _DebugInfo(buildId, isolateDsoBase, _options); |
| 63 | + } |
| 64 | + |
| 65 | + bool _isHeaderStartLine(String line) { |
| 66 | + return line.contains(_headerStartLine); |
| 67 | + } |
| 68 | + |
| 69 | + String? _extractBuildId(String line) { |
| 70 | + final buildIdMatch = _buildIdRegex.firstMatch(line); |
| 71 | + return buildIdMatch?.group(1); |
| 72 | + } |
| 73 | + |
| 74 | + String? _extractIsolateDsoBase(String line) { |
| 75 | + final isolateMatch = _isolateDsoBaseLineRegex.firstMatch(line); |
| 76 | + return isolateMatch?.group(1); |
| 77 | + } |
| 78 | +} |
| 79 | + |
| 80 | +class _DebugInfo { |
| 81 | + final String? buildId; |
| 82 | + final String? isolateDsoBase; |
| 83 | + final SentryOptions _options; |
| 84 | + |
| 85 | + _DebugInfo(this.buildId, this.isolateDsoBase, this._options); |
| 86 | + |
| 87 | + DebugImage? toDebugImage() { |
| 88 | + if (buildId == null || isolateDsoBase == null) { |
| 89 | + _options.logger(SentryLevel.warning, |
| 90 | + 'Cannot create DebugImage without buildId and isolateDsoBase.'); |
| 91 | + return null; |
| 92 | + } |
| 93 | + |
| 94 | + String type; |
| 95 | + String? imageAddr; |
| 96 | + String? debugId; |
| 97 | + String? codeId; |
| 98 | + |
| 99 | + final platform = _options.platformChecker.platform; |
| 100 | + |
| 101 | + // Default values for all platforms |
| 102 | + imageAddr = '0x$isolateDsoBase'; |
| 103 | + |
| 104 | + if (platform.isAndroid) { |
| 105 | + type = 'elf'; |
| 106 | + debugId = _convertCodeIdToDebugId(buildId!); |
| 107 | + codeId = buildId; |
| 108 | + } else if (platform.isIOS || platform.isMacOS) { |
| 109 | + type = 'macho'; |
| 110 | + debugId = _formatHexToUuid(buildId!); |
| 111 | + // `codeId` is not needed for iOS/MacOS. |
| 112 | + } else { |
| 113 | + _options.logger( |
| 114 | + SentryLevel.warning, |
| 115 | + 'Unsupported platform for creating Dart debug images.', |
| 116 | + ); |
| 117 | + return null; |
| 118 | + } |
| 119 | + |
| 120 | + return DebugImage( |
| 121 | + type: type, |
| 122 | + imageAddr: imageAddr, |
| 123 | + debugId: debugId, |
| 124 | + codeId: codeId, |
| 125 | + ); |
| 126 | + } |
| 127 | + |
| 128 | + // Debug identifier is the little-endian UUID representation of the first 16-bytes of |
| 129 | + // the build ID on ELF images. |
| 130 | + String? _convertCodeIdToDebugId(String codeId) { |
| 131 | + codeId = codeId.replaceAll(' ', ''); |
| 132 | + if (codeId.length < 32) { |
| 133 | + _options.logger(SentryLevel.warning, |
| 134 | + 'Code ID must be at least 32 hexadecimal characters long'); |
| 135 | + return null; |
| 136 | + } |
| 137 | + |
| 138 | + final first16Bytes = codeId.substring(0, 32); |
| 139 | + final byteData = _parseHexToBytes(first16Bytes); |
| 140 | + |
| 141 | + if (byteData == null || byteData.isEmpty) { |
| 142 | + _options.logger( |
| 143 | + SentryLevel.warning, 'Failed to convert code ID to debug ID'); |
| 144 | + return null; |
| 145 | + } |
| 146 | + |
| 147 | + return bigToLittleEndianUuid(UuidValue.fromByteList(byteData).uuid); |
| 148 | + } |
| 149 | + |
| 150 | + Uint8List? _parseHexToBytes(String hex) { |
| 151 | + if (hex.length % 2 != 0) { |
| 152 | + _options.logger( |
| 153 | + SentryLevel.warning, 'Invalid hex string during debug image parsing'); |
| 154 | + return null; |
| 155 | + } |
| 156 | + if (hex.startsWith('0x')) { |
| 157 | + hex = hex.substring(2); |
| 158 | + } |
| 159 | + |
| 160 | + var bytes = Uint8List(hex.length ~/ 2); |
| 161 | + for (var i = 0; i < hex.length; i += 2) { |
| 162 | + bytes[i ~/ 2] = int.parse(hex.substring(i, i + 2), radix: 16); |
| 163 | + } |
| 164 | + return bytes; |
| 165 | + } |
| 166 | + |
| 167 | + String bigToLittleEndianUuid(String bigEndianUuid) { |
| 168 | + final byteArray = |
| 169 | + Uuid.parse(bigEndianUuid, validationMode: ValidationMode.nonStrict); |
| 170 | + |
| 171 | + final reversedByteArray = Uint8List.fromList([ |
| 172 | + ...byteArray.sublist(0, 4).reversed, |
| 173 | + ...byteArray.sublist(4, 6).reversed, |
| 174 | + ...byteArray.sublist(6, 8).reversed, |
| 175 | + ...byteArray.sublist(8, 10), |
| 176 | + ...byteArray.sublist(10), |
| 177 | + ]); |
| 178 | + |
| 179 | + return Uuid.unparse(reversedByteArray); |
| 180 | + } |
| 181 | + |
| 182 | + String? _formatHexToUuid(String hex) { |
| 183 | + if (hex.length != 32) { |
| 184 | + _options.logger(SentryLevel.warning, |
| 185 | + 'Hex input must be a 32-character hexadecimal string'); |
| 186 | + return null; |
| 187 | + } |
| 188 | + |
| 189 | + return '${hex.substring(0, 8)}-' |
| 190 | + '${hex.substring(8, 12)}-' |
| 191 | + '${hex.substring(12, 16)}-' |
| 192 | + '${hex.substring(16, 20)}-' |
| 193 | + '${hex.substring(20)}'; |
| 194 | + } |
| 195 | +} |
0 commit comments