{ $Id$ }
{                   -------------------------------------------
                     dbgutils.pp  -  Debugger utility routines
                    -------------------------------------------

 @created(Sun Apr 28st WET 2002)
 @lastmod($Date$)
 @author(Marc Weustink <marc@@dommelstein.net>)

 This unit contains a collection of debugger support routines.

 ***************************************************************************
 *                                                                         *
 *   This source is free software; you can redistribute it and/or modify   *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This code is distributed in the hope that it will be useful, but      *
 *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
 *   General Public License for more details.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
 *                                                                         *
 ***************************************************************************
}
unit DebugUtils;

{$mode objfpc}{$H+}

interface 

uses
  Classes,
  // LazUtils
  {$ifdef FORCE_LAZLOGGER_DUMMY} LazLoggerDummy {$else} LazLoggerBase {$endif}, LazUTF8,
  // DebuggerIntf
  DbgIntfBaseTypes;

type

  TPCharWithLen = record
    Ptr: PChar;
    Len: Integer;
  end;

  TGdbUnEscapeFlags = set of (uefOctal, uefTab, uefNewLine);

function IsSehFinallyFuncName(AName: String): Boolean;
function GetLine(var ABuffer: String): String;
function ConvertToCString(const AText: String): String;
function ConvertPathDelims(const AFileName: String): String;
function DeleteEscapeChars(const AValue: String; const AEscapeChar: Char = '\'): String;
function MakePrintable(const AString: String): String; // Make a pascal like string
function UnEscapeBackslashed(const AValue: String; AFlags: TGdbUnEscapeFlags = [uefOctal]; ATabWidth: Integer = 0): String;
function UnQuote(const AValue: String): String;
function Quote(const AValue: String; AForce: Boolean=False): String;
function ConvertGdbPathAndFile(const AValue: String): String; deprecated 'use ConvertPathFromGdbToLaz'; // fix path, delim, unescape, and to utf8
function ParseGDBString(const AValue: String; KeepBackSlash: Boolean = False): String; // remove quotes(') and convert #dd chars: #9'ab'#9'x'
function GetLeadingAddr(var AValue: String; out AnAddr: TDBGPtr; ARemoveFromValue: Boolean = False): Boolean;
function UpperCaseSymbols(s: string): string;
function ConvertPascalExpression(var AExpression: String): Boolean;

procedure SmartWriteln(const s: string);

function PCLenPartToString(const AVal: TPCharWithLen; AStartOffs, ALen: Integer): String;
function PCLenToString(const AVal: TPCharWithLen; UnQuote: Boolean = False): String;
function PCLenToInt(const AVal: TPCharWithLen; Def: Integer = 0): Integer;
function PCLenToQWord(const AVal: TPCharWithLen; Def: QWord = 0): QWord;
function DbgsPCLen(const AVal: TPCharWithLen): String;

function DPtrMin(const a,b: TDBGPtr): TDBGPtr;
function DPtrMax(const a,b: TDBGPtr): TDBGPtr;

implementation

uses
  SysUtils;

{ SmartWriteln: }
var
  LastSmartWritelnStr: string;
  LastSmartWritelnCount: integer;
  LastSmartWritelnTime: double;

function IsSehFinallyFuncName(AName: String): Boolean;
var
  i: SizeInt;
begin
  i := pos('fin$', AName);
  Result := (i > 0) and (i <= 3);
end;

procedure SmartWriteln(const s: string);
var
  TimeDiff: TTimeStamp;
  i: Integer;
begin
  if (LastSmartWritelnCount>0) and (s=LastSmartWritelnStr) then begin
    TimeDiff:=DateTimeToTimeStamp(Now-LastSmartWritelnTime);
    if TimeDiff.Time<1000 then begin
      // repeating too fast
      inc(LastSmartWritelnCount);
      // write every 2nd, 4th, 8th, 16th, ... time
      i:=LastSmartWritelnCount;
      while (i>0) and ((i and 1)=0) do begin
        i:=i shr 1;
        if i=1 then begin
          DebugLn('Last message repeated %d times: "%s"',
            [LastSmartWritelnCount, LastSmartWritelnStr]);
          break;
        end;
      end;
      exit;
    end;
  end;
  LastSmartWritelnTime:=Now;
  LastSmartWritelnStr:=s;
  LastSmartWritelnCount:=1;
  DebugLn(LastSmartWritelnStr);
end;

function GetLine(var ABuffer: String): String;
var
  idx: Integer;
begin
  idx := Pos(#10, ABuffer);
  if idx = 0
  then Result := ''
  else begin
    Result := Copy(ABuffer, 1, idx);
    Delete(ABuffer, 1, idx);
  end;
end;

function ConvertToCString(const AText: String): String;
var
  srclen, dstlen, newlen: Integer;
  src, dst: PChar;
begin
  srclen := Length(AText);
  Setlength(Result, srclen);
  dstlen := srclen;
  src := @AText[1];
  dst := @Result[1];
  newlen := 0;
  while srclen > 0 do
  begin
    if newlen >= dstlen
    then begin
      Inc(dstlen, 8);
      SetLength(Result, dstlen);
      dst := @Result[newlen+1];
    end;
    case Src[0] of
      '''': begin
        if (srclen > 2) and (Src[1] = '''')
        then begin
          Inc(src);
          Dec(srclen);
          Continue;
        end;
        dst^ := '"';
      end;
      '"': begin
        if newlen+1 >= dstlen
        then begin
          Inc(dstlen, 8);
          SetLength(Result, dstlen);
          dst := @Result[newlen+1];
        end;
        dst^ := '"';
        Inc(dst);
        Inc(newlen);
        dst^ := '"';
      end;
    else
      dst^ := src^;
    end;
    Inc(src);
    Inc(dst);
    Inc(newlen);
    Dec(srclen);
  end;
  SetLength(Result, newlen);
end;

function ConvertPathDelims(const AFileName: String): String;
var
  i: Integer;
begin
  Result := AFileName;
  for i := 1 to length(Result) do
    if Result[i] in ['/','\'] then
      Result[i] := PathDelim;
end;

function MakePrintable(const AString: String): String; // Todo: Check invalid utf8
// Astring should not have quotes
var
  n, l, u: Integer;
  InString: Boolean;

  procedure ToggleInString;
  begin
    InString := not InString;
    Result := Result + '''';
  end;

begin
  Result := '';
  InString := False;
  n := 1;
  l := Length(AString);
  while n <= l do
  //for n := 1 to Length(AString) do
  begin
    case AString[n] of
      ' '..#127: begin
        if not InString then
          ToggleInString;
        Result := Result + AString[n];
        if AString[n] = '''' then Result := Result + '''';
      end;
    #192..#255: begin // Maybe utf8
        u := UTF8CodepointSize(@AString[n]);
        if (u > 0) and (n+u-1 <= l) then begin
          if not InString then
            ToggleInString;
          Result := Result + copy(AString, n, u);
          n := n + u - 1;
        end
        else begin
          if InString then
            ToggleInString;
          Result := Result + Format('#%d', [Ord(AString[n])]);
        end;
      end;
    else
      if InString then
        ToggleInString;
      Result := Result + Format('#%d', [Ord(AString[n])]);
    end;
    inc(n);
  end;
  if InString
  then Result := Result + '''';
end;

function UnEscapeBackslashed(const AValue: String; AFlags: TGdbUnEscapeFlags = [uefOctal]; ATabWidth: Integer = 0): String;
var
  c, cnt, len: Integer;
  Src, Dst: PChar;
begin
  len := Length(AValue);
  if len = 0 then Exit('');

  Src := @AValue[1];
  cnt := len;
  SetLength(Result, len); // allocate initial space

  Dst := @Result[1];
  while cnt > 0 do
  begin
    if (Src^ = '\') then begin
      case (Src+1)^ of
        '\' :
          begin
            inc(Src);
            dec(cnt);
          end;
        '0'..'7' :
          if uefOctal in AFlags then begin
            inc(Src);
            dec(cnt);
            c := 0;
            while (Src^ in ['0'..'7']) and (cnt > 0)
            do begin
              c := (c * 8) + ord(Src^) - ord('0');
              Inc(Src);
              Dec(cnt);
            end;
            //c := UnicodeToUTF8SkipErrors(c, Dst);
            //inc(Dst, c);
            Dst^ := chr(c and 255);
            if (c and 255) <> 0
            then Inc(Dst);
            if cnt = 0 then Break;
            continue;
          end;
        'n' :
          if uefNewLine in AFlags then begin
            inc(Src, 2);
            dec(cnt, 2);
            Dst^ := #10;
            Inc(Dst);
            continue;
          end;
        'r' :
          if uefNewLine in AFlags then begin
            inc(Src, 2);
            dec(cnt, 2);
            Dst^ := #13;
            Inc(Dst);
            continue;
          end;
        't' :
          if uefTab in AFlags then begin
            inc(Src, 2);
            dec(cnt, 2);
            if ATabWidth > 0 then begin;
              c := Dst - @Result[1];
              if Length(Result) < c + cnt + ATabWidth + 1 then begin
                SetLength(Result, Length(Result) + ATabWidth);
                Dst := @Result[1] + c;
              end;
              repeat
                Dst^ := ' ';
                Inc(Dst);
              until ((Dst - @Result[1]) mod ATabWidth) = 0;
            end
            else begin
              Dst^ := #9;
              Inc(Dst);
            end;
            continue;
          end;
      end;
    end;
    Dst^ := Src^;
    Inc(Dst);
    Inc(Src);
    Dec(cnt);
  end;

  SetLength(Result, Dst - @Result[1]); // adjust to actual length
end;

function UnQuote(const AValue: String): String;
var
  len: Integer;
begin
  len := Length(AValue);
  if  len < 2 then Exit(AValue);

  if (AValue[1] = '"') and (AValue[len] = '"')
  then Result := Copy(AValue, 2, len - 2)
  else Result := AValue;
end;

function Quote(const AValue: String; AForce: Boolean): String;
begin
  if (pos(' ', AValue) < 1) and (pos(#9, AValue) < 1) and (not AForce) then
    exit(AValue);
  Result := '"' + StringReplace(AValue, '"', '\"', [rfReplaceAll]) + '"';
end;

function ConvertGdbPathAndFile(const AValue: String): String;
begin
  Result := AnsiToUtf8(ConvertPathDelims(UnEscapeBackslashed(AValue, [uefOctal])));
end;

function ParseGDBString(const AValue: String; KeepBackSlash: Boolean): String;
var
  i, j, v: Integer;
  InQuote: Boolean;
begin
  if AValue = '' then exit('');

  SetLength(Result, length(AValue));
  j := 0;
  i := 0;
  InQuote := False;

  if copy(AValue,1,2) = '0x' then begin
    // skip leading address: 0x010aa00 'abc'
    i := 2;
    while (i < length(AValue)) and (AValue[i+1] in ['0'..'9', 'a'..'f', 'A'..'F']) do inc(i);
    while (i < length(AValue)) and (AValue[i+1] in [' ']) do inc(i);
  end;

  while i < length(AValue) do begin
    inc(i);
    If AValue[i] = '''' then begin
      if InQuote and (i < length(AValue)) and (AValue[i+1] = '''') then begin
        inc(i);
        inc(j);
        Result[j] := '''';
      end
      else begin
        InQuote := not InQuote;
      end;
      continue;
    end;
    if (not KeepBackSlash) and (AValue[i] = '\' ) and (i < length(AValue)) then begin // gdb escapes some chars, even it not pascalish
      inc(j);
      inc(i); // copy next char
      Result[j] := AValue[i];
      continue;
    end;
    if InQuote or not(AValue[i] = '#' ) then begin
      inc(j);
      Result[j] := AValue[i];
      continue;
    end;
    // must be #
    v := 0;
    inc(i);
    while (i <= length(AValue)) and (AValue[i] in ['0'..'9']) do begin
      v:= v * 10 + ord(AValue[i]) - ord('0');
      inc(i);
    end;
    dec(i);
    inc(j);
    Result[j] := chr(v and 255);
  end;
  SetLength(Result, j);
end;

function GetLeadingAddr(var AValue: String; out AnAddr: TDBGPtr;
  ARemoveFromValue: Boolean): Boolean;
var
  i, e: Integer;
begin
  AnAddr := 0;
  Result := (length(AValue) >= 2) and (AValue[1] = '0') and (AValue[2] = 'x');

  if not Result then exit;

  i := 2;
  while (i < length(AValue)) and (AValue[i+1] in ['0'..'9', 'a'..'f', 'A'..'F']) do inc(i);
  Result := i > 2;
  if not Result then exit;

  Val(copy(AValue,1 , i), AnAddr, e);
  Result := e = 0;
  if not Result then exit;

  if ARemoveFromValue then begin
    if (i < length(AValue)) and (AValue[i+1] in [' ']) then inc(i);
    delete(AValue, 1, i);
  end;
end;

function UpperCaseSymbols(s: string): string;
var
  i, l: Integer;
begin
  Result := s;
  i := 1;
  l := Length(Result);
  while (i <= l) do begin
    if Result[i] = '''' then begin
      inc(i);
      while (i <= l) and (Result[i] <> '''') do
        inc(i);
    end
    else
    if Result[i] = '"' then begin
      inc(i);
      while (i < l) and (Result[i] <> '"') do
        inc(i);
    end;
    (* uppercase due to https://sourceware.org/bugzilla/show_bug.cgi?id=17835
       gdb 7.7 and 7.8 fail to find members, if lowercased
       Alternative prefix with "self." if gdb returns &"Type TCLASSXXXX has no component named EXPRESSION.\n"
    *)
    if (i<=l) and (Result[i] in ['a'..'z']) then
      Result[i] := UpCase(Result[i]);
    inc(i);
  end;
end;

function ConvertPascalExpression(var AExpression: String): Boolean;
var
  QuoteChar, R: String;
  P: PChar;
  InString, WasString, IsText, ValIsChar: Boolean;
  n: Integer;
  ValMode: Char;
  Value: QWord;

  function AppendValue: Boolean;
  var
    S: String;
  begin
    if ValMode = #0 then Exit(True);
    if not (ValMode in ['h', 'd', 'o', 'b']) then Exit(False);

    if ValIsChar
    then begin
      if not IsText
      then begin
        R := R + '"';
        IsText := True;
      end;
      R := R + '\' + OctStr(Value, 3);
      ValIsChar := False;
    end
    else begin
      if IsText
      then begin
        R := R + '"';
        IsText := False;
      end;
      Str(Value, S);
      R := R + S;
    end;
    Result := True;
    ValMode := #0;
  end;

begin
  R := '';
  Instring := False;
  WasString := False;
  IsText := False;
  QuoteChar := '"';
  ValIsChar := False;
  ValMode := #0;
  Value := 0;

  P := PChar(AExpression);
  for n := 1 to Length(AExpression) do
  begin
    if InString
    then begin
      case P^ of
        '''': begin
          InString := False;
          // delay setting terminating ", more characters defined through # may follow
          WasString := True;
        end;
        #0..#31,
        '\',
        #128..#255: begin
          R := R + '\' + OctStr(Ord(P^), 3);
        end;
      else begin
          if p^ = QuoteChar then
            R := R + '\' + OctStr(Ord(P^), 3)
          else
            R := R + P^;
        end;
      end;
      Inc(P);
      Continue;
    end;

    case P^ of
      '''': begin
        if WasString
        then begin
          R := R + '\' + OctStr(Ord(''''), 3)
        end
        else begin
          if not AppendValue then Exit(False);
          if not IsText
          then begin
            QuoteChar := '"';
            // single CHAR ?
            if ( ((p+1)^ <> '''') and ((p+2)^ = '''') and not((p+3)^ in ['#', '''']) ) or
               ( ((p+1)^ = '''') and ((p+2)^ = '''') and ((p+3)^ = '''') and not((p+4)^ in ['#', '''']) )
            then
              QuoteChar := '''';
            R := R + QuoteChar;
          end
        end;
        IsText := True;
        InString := True;
      end;
      '#': begin
        if not AppendValue then Exit(False);
        Value := 0;
        ValMode := 'D';
        ValIsChar := True;
      end;
      '$', '&', '%': begin
        if not (ValMode in [#0, 'D']) then Exit(False);
        ValMode := P^;
      end;
    else
      case ValMode of
        'D', 'd': begin
          case P^ of
            '0'..'9': Value := Value * 10 + Ord(P^) - Ord('0');
          else
            Exit(False);
          end;
          ValMode := 'd';
        end;
        '$', 'h': begin
          case P^ of
            '0'..'9': Value := Value * 16 + Ord(P^) - Ord('0');
            'a'..'f': Value := Value * 16 + Ord(P^) - Ord('a');
            'A'..'F': Value := Value * 16 + Ord(P^) - Ord('A');
          else
            Exit(False);
          end;
          ValMode := 'h';
        end;
        '&', 'o': begin
          case P^ of
            '0'..'7': Value := Value * 8 + Ord(P^) - Ord('0');
          else
            Exit(False);
          end;
          ValMode := 'o';
        end;
        '%', 'b': begin
          case P^ of
            '0': Value := Value shl 1;
            '1': Value := Value shl 1 or 1;
          else
            Exit(False);
          end;
          ValMode := 'b';
        end;
      else
        if IsText
        then begin
          R := R + QuoteChar;
          IsText := False;
        end;
        R := R + P^;
      end;
    end;
    WasString := False;
    Inc(p);
  end;

  if not AppendValue then Exit(False);
  if IsText then R := R + QuoteChar;
  AExpression := R;
  Result := True;
end;

function DeleteEscapeChars(const AValue: String; const AEscapeChar: Char): String;
var
  cnt, len: Integer;
  Src, Dst: PChar;
begin
  len := Length(AValue);
  if len = 0 then Exit('');

  Src := @AValue[1];
  cnt := len;
  SetLength(Result, len); // allocate initial space

  Dst := @Result[1];
  while cnt > 0 do
  begin
    if Src^ = AEscapeChar
    then begin
      Dec(len);
      Dec(cnt);
      if cnt = 0 then Break;
      Inc(Src);
    end;
    Dst^ := Src^;
    Inc(Dst);
    Inc(Src);
    Dec(cnt);
  end;

  SetLength(Result, len); // adjust to actual length
end;

{ TPCharWithLen }

function PCLenPartToString(const AVal: TPCharWithLen; AStartOffs, ALen: Integer): String;
begin
  if AStartOffs + ALen > AVal.Len
  then ALen := AVal.Len - AStartOffs;
  if ALen <= 0
  then exit('');

  SetLength(Result, ALen);
  Move((AVal.Ptr+AStartOffs)^, Result[1], aLen)
end;

function PCLenToString(const AVal: TPCharWithLen; UnQuote: Boolean = False): String;
begin
  if UnQuote and (AVal.Len >= 2) and (AVal.Ptr[0] = '"') and (AVal.Ptr[AVal.Len-1] = '"')
  then begin
    SetLength(Result, AVal.Len - 2);
    if AVal.Len > 2
    then Move((AVal.Ptr+1)^, Result[1], AVal.Len - 2)
  end
  else begin
    SetLength(Result, AVal.Len);
    if AVal.Len > 0
    then Move(AVal.Ptr^, Result[1], AVal.Len)
  end;
end;

function PCLenToInt(const AVal: TPCharWithLen; Def: Integer = 0): Integer;
begin
  Result := StrToIntDef(PCLenToString(AVal, True), Def);
end;

function PCLenToQWord(const AVal: TPCharWithLen; Def: QWord = 0): QWord;
begin
  Result := StrToQWordDef(PCLenToString(AVal, True), Def);
end;

function DbgsPCLen(const AVal: TPCharWithLen): String;
begin
  Result := PCLenToString(AVal);
end;

function DPtrMin(const a, b: TDBGPtr): TDBGPtr;
begin
  if a < b then Result := a else Result := b;
end;

function DPtrMax(const a, b: TDBGPtr): TDBGPtr;
begin
  if a > b then Result := a else Result := b;
end;


initialization
  LastSmartWritelnCount:=0;

end.
