// Copyright (c) 2019, 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:async';
import 'dart:math';

import 'constants.dart' as constants;
import 'dwarf.dart';

String _stackTracePiece(CallInfo call, int depth) =>
    '#${depth.toString().padRight(6)} $call';

// The initial header line in a non-symbolic stack trace.
const _headerStartLine =
    '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***';

// A pattern matching the os/arch line of the non-symbolic stack trace header.
//
// This RegExp has been adjusted to parse the header line found in
// non-symbolic stack traces and the modified version in signal handler stack
// traces.
final _osArchLineRE = RegExp(r'os(?:=|: )(\S+?),? '
    r'arch(?:=|: )(\S+?),? comp(?:=|: )(yes|no),? sim(?:=|: )(yes|no)');

// A pattern matching a build ID in the non-symbolic stack trace header.
//
// This RegExp has been adjusted to parse the header line found in
// non-symbolic stack traces and the modified version in signal handler stack
// traces.
const _buildIdREString = r"build_id(?:=|: )'([\da-f]+)'";
final _buildIdRE = RegExp(_buildIdREString);

// A pattern matching a loading unit in the non-symbolic stack trace header.
//
// This RegExp has been adjusted to parse the header line found in
// non-symbolic stack traces and the modified version in signal handler stack
// traces.
final _loadingUnitLineRE = RegExp(r'loading_unit(?:=|: )([\d]+),? (?:' +
    _buildIdREString +
    r',? )?dso_base(?:=|: )([\da-f]+),? instructions(?:=|: )([\da-f]+)');

// A pattern matching the isolate DSO base in the non-symbolic stack trace
// header.
//
// This RegExp has been adjusted to parse the header line found in
// non-symbolic stack traces and the modified version in signal handler stack
// traces.
final _isolateDsoBaseLineRE = RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)');

// A pattern matching the last line of the non-symbolic stack trace header.
//
// This RegExp has been adjusted to parse the header line found in
// non-symbolic stack traces and the modified version in signal handler stack
// traces.
final _instructionsLineRE = RegExp(r'isolate_instructions(?:=|: )([\da-f]+),? '
    r'vm_instructions(?:=|: )([\da-f]+)');

/// Information for a loading unit.
class LoadingUnit {
  /// The id of the loading unit.
  final int id;

  /// The base address at which the loading unit was loaded.
  final int dsoBase;

  /// The address at which the loading unit instructions were loaded.
  final int start;

  /// The build ID of the loading unit, when available.
  final String? buildId;

  LoadingUnit(this.id, this.dsoBase, this.start, {this.buildId});

  void writeToStringBuffer(StringBuffer buffer) {
    buffer
      ..write('LoadingUnit(')
      ..write(id)
      ..write(', start: ')
      ..write(start.toRadixString(16))
      ..write(', dso_base: ')
      ..write(dsoBase.toRadixString(16));
    if (buildId != null) {
      buffer
        ..write(", buildId: '")
        ..write(buildId)
        ..write("'");
    }
    buffer.write(")");
  }

  @override
  String toString() {
    final b = StringBuffer();
    writeToStringBuffer(b);
    return b.toString();
  }
}

/// Header information for a non-symbolic Dart stack trace.
class StackTraceHeader {
  String? _os;
  String? _arch;
  bool? _compressed;
  bool? _simulated;
  String? _buildId;
  Map<int, LoadingUnit>? _units;
  int? _isolateStart;
  int? _vmStart;
  int? _isolateDsoBase;

  String? get os => _os;
  String? get architecture => _arch;
  String? get buildId => _buildId;
  Map<int, LoadingUnit>? get units => _units;
  bool? get compressedPointers => _compressed;
  bool? get usingSimulator => _simulated;
  int? get vmStart => _vmStart;
  int? get isolateStart => _isolateStart;
  int? get isolateDsoBase => _isolateDsoBase;

  static StackTraceHeader fromStarts(int isolateStart, int vmStart,
          {String? architecture}) =>
      StackTraceHeader()
        .._isolateStart = isolateStart
        .._vmStart = vmStart
        .._arch = architecture;

  static StackTraceHeader fromLines(List<String> lines, {bool lossy = false}) {
    final result = StackTraceHeader();
    for (final line in lines) {
      result.tryParseHeaderLine(line, lossy: lossy);
    }
    return result;
  }

  /// Try and parse the given line as one of the recognized lines in the
  /// header of a non-symbolic stack trace.
  ///
  /// If [lossy] is true, then the parser assumes that some header lines may
  /// have been lost (e.g., due to log truncation) and recreates missing parts
  /// of the header from other parsed parts if possible.
  ///
  /// Returns whether the line was recognized and parsed successfully.
  bool tryParseHeaderLine(String line, {bool lossy = false}) {
    if (line.contains(_headerStartLine)) {
      // This is the start of a new non-symbolic stack trace, so reset all the
      // stored information to be parsed anew.
      _os = null;
      _arch = null;
      _compressed = null;
      _simulated = null;
      _buildId = null;
      _isolateStart = null;
      _isolateDsoBase = null;
      _vmStart = null;
      _units = null;
      return true;
    }
    RegExpMatch? match;
    match = _osArchLineRE.firstMatch(line);
    if (match != null) {
      _os = match[1]!;
      _arch = match[2]!;
      _compressed = match[3]! == "yes";
      _simulated = match[4]! == "yes";
      if (lossy) {
        // Reset all stored information that is parsed after this point,
        // just in case we've missed earlier lines in this header.
        _buildId = null;
        _isolateStart = null;
        _isolateDsoBase = null;
        _vmStart = null;
        _units = null;
      }
      return true;
    }
    // Have to check for loading units first because they can include a
    // build ID, so the build ID RegExp matches them as well.
    match = _loadingUnitLineRE.firstMatch(line);
    if (match != null) {
      _units ??= <int, LoadingUnit>{};
      final id = int.parse(match[1]!);
      final buildId = match[2];
      final dsoBase = int.parse(match[3]!, radix: 16);
      final start = int.parse(match[4]!, radix: 16);
      _units![id] = LoadingUnit(id, dsoBase, start, buildId: buildId);
      if (lossy) {
        // Reset all stored information that is parsed after this point,
        // just in case we've missed earlier lines in this header.
        _isolateStart = null;
        _isolateDsoBase = null;
        _vmStart = null;
      }
      return true;
    }
    match = _buildIdRE.firstMatch(line);
    if (match != null) {
      _buildId = match[1]!;
      if (lossy) {
        // Reset all stored information that is parsed after this point,
        // just in case we've missed earlier lines in this header.
        _isolateStart = null;
        _isolateDsoBase = null;
        _vmStart = null;
        _units = null;
      }
      return true;
    }
    match = _isolateDsoBaseLineRE.firstMatch(line);
    if (match != null) {
      _isolateDsoBase = int.parse(match[1]!, radix: 16);
      if (lossy) {
        // Reset all stored information that is parsed after this point,
        // just in case we've missed earlier lines in this header.
        _isolateStart = null;
        _vmStart = null;
      }
      return true;
    }
    match = _instructionsLineRE.firstMatch(line);
    if (match != null) {
      _isolateStart = int.parse(match[1]!, radix: 16);
      _vmStart = int.parse(match[2]!, radix: 16);
      if (_units != null) {
        final rootUnit = _units![constants.rootLoadingUnitId];
        if (lossy && rootUnit == null) {
          // We missed the header entry for the root loading unit, but it can
          // be reconstructed from other header lines.
          _units![constants.rootLoadingUnitId] = LoadingUnit(
            constants.rootLoadingUnitId,
            _isolateDsoBase!,
            _isolateStart!,
            buildId: _buildId,
          );
        } else {
          assert(rootUnit != null);
          assert(_isolateStart == rootUnit!.start);
          assert(_isolateDsoBase == rootUnit!.dsoBase);
          assert(_buildId == null || _buildId == rootUnit!.buildId);
        }
      }
      return true;
    }
    return false;
  }

  // Returns the closest positive offset, unless both offsets are negative in
  // which case it returns the negative offset closest to zero.
  int _closestOffset(int offset1, int offset2) {
    if (offset1 < 0) {
      if (offset2 < 0) return max(offset1, offset2);
      return offset2;
    }
    if (offset2 < 0) return offset1;
    return min(offset1, offset2);
  }

  /// The [PCOffset] for the given absolute program counter address.
  PCOffset? offsetOf(int address) {
    if (_isolateStart == null || _vmStart == null) return null;
    var vmOffset = address - _vmStart!;
    var unitOffset = address - _isolateStart!;
    var unitBuildId = _buildId;
    var unitId = constants.rootLoadingUnitId;
    if (units != null) {
      for (final unit in units!.values) {
        final newOffset = address - unit.start;
        if (newOffset == _closestOffset(unitOffset, newOffset)) {
          unitOffset = newOffset;
          unitBuildId = unit.buildId;
          unitId = unit.id;
        }
      }
    }
    if (unitOffset == _closestOffset(vmOffset, unitOffset)) {
      return PCOffset(unitOffset, InstructionsSection.isolate,
          os: _os,
          architecture: _arch,
          compressedPointers: _compressed,
          usingSimulator: _simulated,
          buildId: unitBuildId,
          unitId: unitId);
    }
    // The VM section is always stored in the root loading unit.
    return PCOffset(vmOffset, InstructionsSection.vm,
        os: _os,
        architecture: _arch,
        compressedPointers: _compressed,
        usingSimulator: _simulated,
        buildId: _buildId,
        unitId: constants.rootLoadingUnitId);
  }

  void writeToStringBuffer(StringBuffer buffer) {
    var printedField = false;
    void printField(String name, dynamic value) {
      buffer
        ..writeln(printedField ? ',' : '')
        ..write('  ')
        ..write(name)
        ..write(': ')
        ..write(value);
      printedField = true;
    }

    buffer.write('StackTraceHeader(');
    if (_vmStart != null) {
      printField('vmStart', _vmStart!.toRadixString(16));
    }
    if (_isolateStart != null) {
      printField('isolateStart', _isolateStart!.toRadixString(16));
    }
    if (_isolateDsoBase != null) {
      printField('isolateDsoBase', _isolateDsoBase!.toRadixString(16));
    }
    if (_arch != null) {
      final b = StringBuffer();
      if (_simulated == true) {
        b.write('SIM');
      }
      b.write(_arch!.toUpperCase());
      if (_compressed == true) {
        b.write('C');
      }
      printField('arch', b.toString());
    } else {
      if (_simulated != null) {
        printField('simulated', _simulated);
      }
      if (_compressed != null) {
        printField('compressed', _compressed);
      }
    }
    if (_buildId != null) {
      printField('buildId', "'$_buildId'");
    }
    if (_units != null) {
      final b = StringBuffer();
      b.writeln('{');
      for (final unitId in _units!.keys) {
        b.write('    $unitId => ');
        _units![unitId]!.writeToStringBuffer(b);
        b.writeln(',');
      }
      b.write('}');
      printField('units', b.toString());
    }
    buffer.write(')');
  }

  @override
  String toString() {
    final buffer = StringBuffer();
    writeToStringBuffer(buffer);
    return buffer.toString();
  }
}

(InstructionsSection, int)? _tryParseSymbolOffset(String s,
    {bool forceHexadecimal = false}) {
  final match = _symbolOffsetRE.firstMatch(s);
  if (match == null) return null;
  final symbolString = match.namedGroup('symbol')!;
  final offsetString = match.namedGroup('offset')!;
  int? offset;
  if (!forceHexadecimal && !offsetString.startsWith('0x')) {
    offset = int.tryParse(offsetString);
  }
  if (offset == null) {
    final digits = offsetString.startsWith('0x')
        ? offsetString.substring(2)
        : offsetString;
    offset = int.tryParse(digits, radix: 16);
  }
  if (offset == null) return null;
  switch (symbolString) {
    case constants.vmSymbolName:
      return (InstructionsSection.vm, offset);
    case constants.isolateSymbolName:
      return (InstructionsSection.isolate, offset);
    default:
      break;
  }
  return null;
}

/// Parses strings of the format `<static symbol>+<integer offset>`, where
/// `<static symbol>` is one of the static symbols used for Dart instruction
/// sections.
///
/// Unless forceHexadecimal is true, an integer offset without a "0x" prefix or
/// any hexadecimal digits will be parsed as decimal.
///
/// Assumes that the symbol should be resolved in the root loading unit.
///
/// Returns null if the string is not of the expected format.
PCOffset? tryParseSymbolOffset(String s,
    {bool forceHexadecimal = false,
    String? buildId,
    StackTraceHeader? header}) {
  final result = _tryParseSymbolOffset(s, forceHexadecimal: forceHexadecimal);
  if (result == null) return null;
  return PCOffset(result.$2, result.$1,
      os: header?.os,
      architecture: header?.architecture,
      compressedPointers: header?.compressedPointers,
      usingSimulator: header?.usingSimulator,
      buildId: header?.buildId,
      unitId: constants.rootLoadingUnitId);
}

/// A Dart DWARF stack trace contains up to four pieces of information:
///   - The zero-based frame index from the top of the stack.
///   - The absolute address of the program counter.
///   - The virtual address of the program counter, if the snapshot was
///     loaded as a dynamic library, otherwise not present.
///   - The location of the virtual address, which is one of the following:
///     - A dynamic symbol name, a plus sign, and an integer offset.
///     - The path to the snapshot, if it was loaded as a dynamic library,
///       otherwise the string `"<unknown>"`.
const _symbolOffsetREString = r'(?<symbol>' +
    constants.vmSymbolName +
    r'|' +
    constants.isolateSymbolName +
    r')\+(?<offset>(?:0x)?[\da-f]+)';
final _symbolOffsetRE = RegExp(_symbolOffsetREString);
final _traceLineRE = RegExp(r'\s*#(\d+) abs (?<absolute>[\da-f]+)'
    r'(?: unit (?<unitId>\d+))?'
    r'(?: virt (?<virtual>[\da-f]+))?'
    r' (?<rest>.*)$');

PCOffset? _retrievePCOffset(StackTraceHeader header, RegExpMatch? match) {
  if (match == null) return null;
  // Retrieve the unit ID for this stack frame, if one was provided.
  var unitId = constants.rootLoadingUnitId;
  var buildId = header.buildId;
  if (match.namedGroup('unitId') != null) {
    unitId = int.parse(match.namedGroup('unitId')!);
    final unit = header.units?[unitId];
    if (unit == null) {
      // The given non-root loading unit wasn't found in the header.
      return null;
    }
    buildId = unit.buildId;
  }
  if (unitId == constants.rootLoadingUnitId) {
    // Try checking for symbol information first, since we don't need the header
    // information to translate it for the root loading unit.
    final restString = match.namedGroup('rest')!;
    if (restString.isNotEmpty) {
      final result = _tryParseSymbolOffset(restString);
      if (result != null) {
        return PCOffset(result.$2, result.$1,
            os: header.os,
            architecture: header.architecture,
            compressedPointers: header.compressedPointers,
            usingSimulator: header.usingSimulator,
            buildId: buildId,
            unitId: unitId);
      }
    }
  }
  // If we're parsing the absolute address, we can only convert it into
  // a PCOffset if we saw the instructions line of the stack trace header.
  final addressString = match.namedGroup('absolute')!;
  final address = int.parse(addressString, radix: 16);
  final pcOffset = header.offsetOf(address);
  if (pcOffset != null) return pcOffset;
  // If all other cases failed, check for a virtual address. Until this package
  // depends on a version of Dart which only prints virtual addresses when the
  // virtual addresses in the snapshot are the same as in separately saved
  // debugging information, the other methods should be tried first.
  if (match.namedGroup('virtual') != null) {
    final address = int.parse(match.namedGroup('virtual')!, radix: 16);
    return PCOffset(address, InstructionsSection.none,
        os: header.os,
        architecture: header.architecture,
        compressedPointers: header.compressedPointers,
        usingSimulator: header.usingSimulator,
        buildId: buildId,
        unitId: unitId);
  }
  return null;
}

/// The [PCOffset]s for frames of the non-symbolic stack traces in [lines].
Iterable<PCOffset> collectPCOffsets(Iterable<String> lines,
    {bool lossy = false}) sync* {
  final header = StackTraceHeader();
  for (var line in lines) {
    if (header.tryParseHeaderLine(line, lossy: lossy)) {
      continue;
    }
    final match = _traceLineRE.firstMatch(line);
    final offset = _retrievePCOffset(header, match);
    if (offset != null) yield offset;
  }
}

/// A [StreamTransformer] that scans lines for non-symbolic stack traces.
///
/// A [NativeStackTraceDecoder] scans a stream of lines for non-symbolic
/// stack traces containing only program counter address information. Such
/// stack traces are generated by the VM when executing a snapshot compiled
/// with `--dwarf-stack-traces`.
///
/// The transformer assumes that there may be text preceding the stack frames
/// on individual lines, like in log files, but that there is no trailing text.
/// For each stack frame found, the transformer attempts to locate a function
/// name, file name and line number using the provided DWARF information.
///
/// If no information is found, or the line is not a stack frame, then the line
/// will be unchanged in the output stream.
///
/// If the located information corresponds to Dart internals and
/// [includeInternalFrames] is false, then the output stream contains no
/// entries for the line.
///
/// Otherwise, the output stream contains one or more lines with symbolic stack
/// frames for the given non-symbolic stack frame line. Multiple symbolic stack
/// frame lines are generated when the PC address corresponds to inlined code.
/// In the output stream, each symbolic stack frame is prefixed by the non-stack
/// frame portion of the original line.
class DwarfStackTraceDecoder extends StreamTransformerBase<String, String> {
  final Dwarf _dwarf;
  final Map<int, Dwarf>? _dwarfByUnitId;
  final Iterable<Dwarf>? _unitDwarfs;
  final bool _includeInternalFrames;

  DwarfStackTraceDecoder(
    this._dwarf, {
    Map<int, Dwarf>? dwarfByUnitId,
    Iterable<Dwarf>? unitDwarfs,
    bool includeInternalFrames = false,
  })  : _dwarfByUnitId = dwarfByUnitId,
        _unitDwarfs = unitDwarfs,
        _includeInternalFrames = includeInternalFrames;

  @override
  Stream<String> bind(Stream<String> stream) async* {
    var depth = 0;
    final header = StackTraceHeader();
    await for (final line in stream) {
      // If we successfully parse a header line, then we reset the depth to 0.
      if (header.tryParseHeaderLine(line, lossy: true)) {
        depth = 0;
        yield line;
        continue;
      }
      // If at any point we can't get appropriate information for the current
      // line as a stack trace line, then just pass the line through unchanged.
      final lineMatch = _traceLineRE.firstMatch(line);
      final offset = _retrievePCOffset(header, lineMatch);
      if (offset == null) {
        yield line;
        continue;
      }
      Dwarf dwarf = _dwarf;
      final unitId = offset.unitId;
      if (unitId != null && unitId != constants.rootLoadingUnitId) {
        Dwarf? unitDwarf;
        // Prefer the map that specifies loading unit IDs over the iterable.
        if (_dwarfByUnitId != null) {
          unitDwarf = _dwarfByUnitId[unitId];
        }
        if (unitDwarf == null &&
            _unitDwarfs != null &&
            offset.buildId != null) {
          for (final d in _unitDwarfs) {
            if (d.buildId(offset.architecture) == offset.buildId) {
              unitDwarf = d;
            }
          }
        }
        // Don't attempt to translate if we couldn't find the correct debugging
        // information for this loading unit.
        if (unitDwarf == null) {
          yield line;
          continue;
        }
        dwarf = unitDwarf;
      }
      final callInfo = offset.callInfoFrom(dwarf,
          includeInternalFrames: _includeInternalFrames);
      if (callInfo == null) {
        yield line;
        continue;
      }
      // No lines to output (as this corresponds to Dart internals).
      if (callInfo.isEmpty) continue;
      // Output the lines for the symbolic frame with the prefix found on the
      // original non-symbolic frame line, modulo all whitespace between the
      // prefix and stack trace information converted to a single space.
      //
      // If there was no prefix, just swallow any initial whitespace, since
      // symbolic Dart stacktrace lines have no initial whitespace.
      String prefix = line.substring(0, lineMatch!.start);
      if (prefix.isNotEmpty) {
        prefix += ' ';
      }
      for (final call in callInfo) {
        yield prefix + _stackTracePiece(call, depth++);
      }
    }
  }
}
