mirror of
				https://gitlab.com/freepascal.org/lazarus/lazarus.git
				synced 2025-10-29 07:01:37 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			592 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
			
		
		
	
	
			592 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
| {                        ----------------------------------------------
 | |
|                             GDBMIMiscClasses.pp  -  Debugger helper class
 | |
|                          ----------------------------------------------
 | |
| 
 | |
|   This unit contains a helper class for decoding GDB output.
 | |
| 
 | |
| 
 | |
|  ***************************************************************************
 | |
|  *                                                                         *
 | |
|  *   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 GDBMIMiscClasses;
 | |
| {$mode objfpc}{$H+}
 | |
| {$IFDEF linux} {$DEFINE DBG_ENABLE_TERMINAL} {$ENDIF}
 | |
| 
 | |
| interface
 | |
| 
 | |
| uses
 | |
|   SysUtils,
 | |
|   {$IFDEF DBG_ENABLE_TERMINAL}
 | |
|   IDEMiniLibC, BaseUnix, Classes,
 | |
|   {$ENDIF}
 | |
|   DebugUtils, DbgIntfDebuggerBase;
 | |
| 
 | |
| type
 | |
| 
 | |
|   TGDBMIResultFlags = set of (
 | |
|     rfNoMI,        // flag is set if the output is not MI formatted
 | |
|                    // some MI functions return normal output
 | |
|                    // some normal functions return MI output
 | |
|     rfAsyncFailed  //
 | |
|   );
 | |
| 
 | |
|   TGDBMIExecResult = record
 | |
|     State: TDBGState;
 | |
|     Values: String;
 | |
|     Flags: TGDBMIResultFlags
 | |
|   end;
 | |
| const
 | |
|   GDBMIExecResultDefault: TGDBMIExecResult = (State: dsNone; Values: ''; Flags: []);
 | |
| 
 | |
| type
 | |
|   PGDBMINameValue = ^TGDBMINameValue;
 | |
|   TGDBMINameValue = record
 | |
|     Name: TPCharWithLen;
 | |
|     Value: TPCharWithLen;
 | |
|   end;
 | |
| 
 | |
| 
 | |
|   { TGDBMINameValueList }
 | |
| 
 | |
|   TGDBMINameValueList = class(TObject)
 | |
|   private
 | |
|     FDataLen: Integer;
 | |
|     FText: String;
 | |
|     FData: PChar;
 | |
|     FCount: Integer;
 | |
|     FIndex: array of TGDBMINameValue;
 | |
|     FUseTrim: Boolean;
 | |
| 
 | |
|     function Find(const AName : string): PGDBMINameValue;
 | |
|     function GetItem(const AIndex: Integer): PGDBMINameValue;
 | |
|     function GetText: String;
 | |
|     function GetValue(const AName : string): string;
 | |
|     function GetValuePtr(const AName: string): TPCharWithLen;
 | |
|   public
 | |
|     function GetString(const AIndex: Integer): string;
 | |
|   public
 | |
|     constructor Create(const AResultValues: String); overload;
 | |
|     constructor Create(const AResultValues: TPCharWithLen); overload;
 | |
|     constructor Create(AResult: TGDBMIExecResult); overload;
 | |
|     constructor Create(const AResultValues: String; const APath: array of String); overload;
 | |
|     constructor Create(AResult: TGDBMIExecResult; const APath: array of String); overload;
 | |
|     procedure Delete(AIndex: Integer);
 | |
|     procedure Init(const AResultValues: String);
 | |
|     procedure Init(AResultValues: PChar; ALength: Integer);
 | |
|     procedure Init(const AResultValues: TPCharWithLen);
 | |
|     procedure SetPath(const APath: String); overload;
 | |
|     procedure SetPath(const APath: array of String); overload;
 | |
|     function IndexOf(const AName: string): Integer;
 | |
|     property Count: Integer read FCount;
 | |
|     property Items[const AIndex: Integer]: PGDBMINameValue read GetItem;
 | |
|     property Values[const AName: string]: string read GetValue;
 | |
|     property ValuesPtr[const AName: string]: TPCharWithLen read GetValuePtr;
 | |
|     property UseTrim: Boolean read FUseTrim write FUseTrim;
 | |
|     property Data: PChar read FData;
 | |
|     property DataLen: Integer read FDataLen;
 | |
|     property Text: String read GetText;
 | |
|   end;
 | |
| 
 | |
|   {$IFDEF DBG_ENABLE_TERMINAL}
 | |
| type
 | |
| 
 | |
|   { TPseudoTerminal }
 | |
| 
 | |
|   TPseudoTerminal = class
 | |
|   private
 | |
|     FDeviceName: string;
 | |
|     FOnCanRead: TNotifyEvent;
 | |
|     FPTy: Integer;
 | |
|     FReadBuf: String;
 | |
|     procedure CloseInp;
 | |
|   public
 | |
|     constructor Create;
 | |
|     destructor  Destroy; override;
 | |
|     procedure Open;
 | |
|     procedure Close;
 | |
|     function Write(s: string): Integer;
 | |
|     function Read: String;
 | |
|     procedure CheckCanRead;
 | |
|     property OnCanRead: TNotifyEvent read FOnCanRead write FOnCanRead;
 | |
|     property Devicename: string read FDeviceName;
 | |
|   end;
 | |
|   {$ENDIF}
 | |
| 
 | |
| 
 | |
| implementation
 | |
| 
 | |
| { TGDBMINameValueList }
 | |
| 
 | |
| constructor TGDBMINameValueList.Create(const AResultValues: String);
 | |
| begin
 | |
|   inherited Create;
 | |
|   Init(AResultValues);
 | |
| end;
 | |
| 
 | |
| constructor TGDBMINameValueList.Create(const AResultValues: TPCharWithLen);
 | |
| begin
 | |
|   inherited Create;
 | |
|   Init(AResultValues);
 | |
| end;
 | |
| 
 | |
| constructor TGDBMINameValueList.Create(const AResultValues: String; const APath: array of String);
 | |
| begin
 | |
|   inherited Create;
 | |
|   Init(AResultValues);
 | |
|   SetPath(APath);
 | |
| end;
 | |
| 
 | |
| constructor TGDBMINameValueList.Create(AResult: TGDBMIExecResult);
 | |
| begin
 | |
|   inherited Create;
 | |
|   Init(AResult.Values);
 | |
| end;
 | |
| 
 | |
| constructor TGDBMINameValueList.Create(AResult: TGDBMIExecResult; const APath: array of String);
 | |
| begin
 | |
|   inherited Create;
 | |
|   Init(AResult.Values);
 | |
|   SetPath(APath);
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.Delete(AIndex: Integer);
 | |
| begin
 | |
|   if AIndex < 0 then Exit;
 | |
|   if AIndex >= FCount then Exit;
 | |
|   Dec(FCount);
 | |
|   Move(FIndex[AIndex + 1], FIndex[AIndex], SizeOf(FIndex[0]) * (FCount - AIndex));
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.Find(const AName: string): PGDBMINameValue;
 | |
| var
 | |
|   n: Integer;
 | |
| begin
 | |
|   n := IndexOf(AName);
 | |
|   if n < 0 then Exit(nil);
 | |
|   Result := @FIndex[n];
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.GetItem(const AIndex: Integer): PGDBMINameValue;
 | |
| begin
 | |
|   if AIndex < 0 then Exit(nil);
 | |
|   if AIndex >= FCount then Exit(nil);
 | |
|   Result := @FIndex[AIndex];
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.GetText: String;
 | |
| begin
 | |
|   Result := copy(FData, 1, FDataLen);
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.GetString(const AIndex : Integer) : string;
 | |
| var
 | |
|   len: Integer;
 | |
|   item: PGDBMINameValue;
 | |
| begin
 | |
|   Result := '';
 | |
|   if (AIndex < 0) or (AIndex >= FCount) then Exit;
 | |
|   item := @FIndex[AIndex];
 | |
|   if item = nil then Exit;
 | |
| 
 | |
|   len := Item^.Name.Len;
 | |
|   if Item^.Value.Ptr <> nil then begin
 | |
|     if (Item^.Value.Ptr-1) = '"' then inc(len, 2);
 | |
|     len := len + 1 + Item^.Value.Len;
 | |
|   end;
 | |
| 
 | |
|   SetLength(Result, len);
 | |
|   if len > 0 then
 | |
|     Move(Item^.Name.Ptr^, Result[1], len);
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.GetValue(const AName: string): string;
 | |
| var
 | |
|   item: PGDBMINameValue;
 | |
| begin
 | |
|   Result := '';
 | |
|   if FCount = 0 then Exit;
 | |
|   item := Find(AName);
 | |
|   if item = nil then Exit;
 | |
| 
 | |
|   SetLength(Result, Item^.Value.Len);
 | |
|   if Item^.Value.Len > 0 then
 | |
|     Move(Item^.Value.Ptr^, Result[1], Item^.Value.Len);
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.GetValuePtr(const AName: string): TPCharWithLen;
 | |
| var
 | |
|   item: PGDBMINameValue;
 | |
| begin
 | |
|   Result.Ptr := nil;
 | |
|   Result.Len := 0;
 | |
|   if FCount = 0 then Exit;
 | |
|   item := Find(AName);
 | |
|   if item = nil then Exit;
 | |
| 
 | |
|   Result := item^.Value;
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.Init(AResultValues: PChar; ALength: Integer);
 | |
| 
 | |
|   function FindNextQuote(ACurPtr, AEndPtr: PChar): PChar;
 | |
|   begin
 | |
|     Result := ACurPtr;
 | |
|     while Result <= AEndPtr do
 | |
|     begin
 | |
|       case Result^ of
 | |
|         '\': Inc(Result, 2);
 | |
|         '"': Break;
 | |
|       else
 | |
|         Inc(Result);
 | |
|       end;
 | |
|     end;
 | |
|   end;
 | |
| 
 | |
|   function FindClosingBracket(ACurPtr, AEndPtr: PChar): PChar;
 | |
|   var
 | |
|     deep: Integer;
 | |
|   begin
 | |
|     deep := 1;
 | |
|     Result := ACurPtr;
 | |
| 
 | |
|     while Result <= AEndPtr do
 | |
|     begin
 | |
|       case Result^ of
 | |
|         '\': Inc(Result);
 | |
|         '"': Result := FindNextQuote(Result + 1, AEndPtr);
 | |
|         '[', '{': Inc(deep);
 | |
|         ']', '}': begin
 | |
|           Dec(deep);
 | |
|           if deep = 0 then break;
 | |
|         end;
 | |
|       end;
 | |
|       Inc(Result);
 | |
|     end;
 | |
|   end;
 | |
| 
 | |
|   procedure Add(AStartPtr, AEquPtr, AEndPtr: PChar);
 | |
|   var
 | |
|     Item: PGDBMINameValue;
 | |
|   begin
 | |
|     if AEndPtr <= AStartPtr then Exit;
 | |
| 
 | |
|     // check space
 | |
|     if Length(FIndex) <= FCount
 | |
|     then SetLength(FIndex, FCount + 16);
 | |
| 
 | |
|     Item := @FIndex[FCount];
 | |
|     if AEquPtr < AStartPtr
 | |
|     then begin
 | |
|       // trim spaces
 | |
|       if UseTrim then
 | |
|       begin
 | |
|         while (AStartPtr < AEndPtr) and (AStartPtr^ = #32) do
 | |
|           inc(AStartPtr);
 | |
|         while (AEndPtr > AStartPtr) and (AEndPtr^ = #32) do
 | |
|           dec(AEndPtr);
 | |
|       end;
 | |
| 
 | |
|       // only name, no value
 | |
|       Item^.Name.Ptr := AStartPtr;
 | |
|       Item^.Name.Len := {%H-}PtrUInt(AEndPtr) - {%H-}PtrUInt(AStartPtr) + 1;
 | |
|       Item^.Value.Ptr := nil;
 | |
|       Item^.Value.Len := 0;
 | |
|     end
 | |
|     else begin
 | |
|       // trim surrounding spaces
 | |
|       if UseTrim then
 | |
|       begin
 | |
|         while (AStartPtr < AEquPtr) and (AStartPtr^ = #32) do
 | |
|           inc(AStartPtr);
 | |
|         while (AEndPtr > AEquPtr) and (AEndPtr^ = #32) do
 | |
|           dec(AEndPtr);
 | |
|       end;
 | |
| 
 | |
|       Item^.Name.Ptr := AStartPtr;
 | |
|       Item^.Name.Len := {%H-}PtrUInt(AEquPtr) - {%H-}PtrUInt(AStartPtr);
 | |
| 
 | |
|       // trim name spaces
 | |
|       if UseTrim then
 | |
|         while (Item^.Name.Len > 0) and (Item^.Name.Ptr[Item^.Name.Len - 1] = #32) do
 | |
|           dec(Item^.Name.Len);
 | |
| 
 | |
|       if (AEquPtr < AEndPtr - 1) and (AEquPtr[1] = '"') and (AEndPtr^ = '"')
 | |
|       then begin
 | |
|         // strip surrounding "
 | |
|         Item^.Value.Ptr := AEquPtr + 2;
 | |
|         Item^.Value.Len := {%H-}PtrUInt(AEndPtr) - {%H-}PtrUInt(AEquPtr) - 2;
 | |
|       end
 | |
|       else begin
 | |
|         Item^.Value.Ptr := AEquPtr + 1;
 | |
|         Item^.Value.Len := {%H-}PtrUInt(AEndPtr) - {%H-}PtrUInt(AEquPtr)
 | |
|       end;
 | |
|       // trim value spaces
 | |
|       if UseTrim then
 | |
|         while (Item^.Value.Len > 0) and (Item^.Value.Ptr[0] = #32) do
 | |
|         begin
 | |
|           inc(Item^.Value.Ptr);
 | |
|           dec(Item^.Value.Len);
 | |
|         end;
 | |
|     end;
 | |
| 
 | |
|     Inc(FCount);
 | |
|   end;
 | |
| 
 | |
| var
 | |
|   CurPtr, StartPtr, EquPtr, EndPtr: PChar;
 | |
| begin
 | |
|   // clear
 | |
|   FCount := 0;
 | |
|   FData := AResultValues;
 | |
|   FDataLen := ALength;
 | |
| 
 | |
|   if AResultValues = nil then Exit;
 | |
|   if ALength <= 0 then Exit;
 | |
|   EndPtr := AResultValues + ALength - 1;
 | |
| 
 | |
|   // strip surrounding '[]' OR '{}' first
 | |
|   case AResultValues^ of
 | |
|     '[': begin
 | |
|       if EndPtr^ = ']'
 | |
|       then begin
 | |
|         Inc(AResultValues);
 | |
|         Dec(EndPtr);
 | |
|       end;
 | |
|     end;
 | |
|     '{': begin
 | |
|       if EndPtr^ = '}'
 | |
|       then begin
 | |
|         Inc(AResultValues);
 | |
|         Dec(EndPtr);
 | |
|       end;
 | |
|     end;
 | |
|   end;
 | |
| 
 | |
|   StartPtr := AResultValues;
 | |
|   CurPtr := AResultValues;
 | |
|   EquPtr := nil;
 | |
|   while CurPtr <= EndPtr do
 | |
|   begin
 | |
|     case CurPtr^ of
 | |
|       '\': Inc(CurPtr); // skip escaped char
 | |
|       '"': CurPtr := FindNextQuote(CurPtr + 1, EndPtr);
 | |
|       '[',
 | |
|       '{': CurPtr := FindClosingBracket(CurPtr + 1, EndPtr);
 | |
|       '=': EquPtr := CurPtr;
 | |
|       ',': begin
 | |
|         Add(StartPtr, EquPtr, CurPtr - 1);
 | |
|         Inc(CurPtr);
 | |
|         StartPtr := CurPtr;
 | |
|         Continue;
 | |
|       end;
 | |
|     end;
 | |
|     Inc(CurPtr);
 | |
|   end;
 | |
|   if StartPtr <= EndPtr
 | |
|   then Add(StartPtr, EquPtr, EndPtr);
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.Init(const AResultValues: TPCharWithLen);
 | |
| begin
 | |
|   Init(AResultValues.Ptr, AResultValues.Len)
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.Init(const AResultValues: String);
 | |
| begin
 | |
|   FText := AResultValues;
 | |
|   Init(PChar(FText), Length(FText));
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.SetPath(const APath: String);
 | |
| begin
 | |
|   SetPath([APath]);
 | |
| end;
 | |
| 
 | |
| procedure TGDBMINameValueList.SetPath(const APath: array of String);
 | |
| var
 | |
|   i: integer;
 | |
|   Item: PGDBMINameValue;
 | |
| begin
 | |
|   for i := low(APath) to High(APath) do
 | |
|   begin
 | |
|     item := Find(APath[i]);
 | |
|     if item = nil
 | |
|     then begin
 | |
|       FCount := 0;
 | |
|       Exit;
 | |
|     end;
 | |
|     Init(Item^.Value);
 | |
|   end;
 | |
| end;
 | |
| 
 | |
| function TGDBMINameValueList.IndexOf(const AName: string): Integer;
 | |
| var
 | |
|   len: Integer;
 | |
| begin
 | |
|   len := Length(AName);
 | |
|   Result := 0;
 | |
|   while Result < FCount do begin
 | |
|     if (FIndex[Result].Name.Len = len)
 | |
|     and (strlcomp(FIndex[Result].Name.Ptr, PChar(AName), len) = 0)
 | |
|     then exit;
 | |
|     inc(Result);
 | |
|   end;
 | |
|   Result := -1;
 | |
| end;
 | |
| 
 | |
| {$IFDEF DBG_ENABLE_TERMINAL}
 | |
| 
 | |
| { TPseudoTerminal }
 | |
| 
 | |
| procedure TPseudoTerminal.CloseInp;
 | |
| var
 | |
|   ios: termios;
 | |
| begin
 | |
|   // Based on MSEGui
 | |
|   if FPTy = InvalHandle then exit;
 | |
|   tcgetattr(FPty, @ios);
 | |
|   ios.c_lflag:= (ios.c_lflag and not (icanon)) or echo;
 | |
|   ios.c_cc[vmin]:= 0;
 | |
|   ios.c_cc[vtime]:= 0;
 | |
|   tcsetattr(FPty, tcsanow, @ios);
 | |
|     //foutput.writeln('');
 | |
| end;
 | |
| 
 | |
| constructor TPseudoTerminal.Create;
 | |
| begin
 | |
|   FPTy := InvalHandle;
 | |
| end;
 | |
| 
 | |
| destructor TPseudoTerminal.Destroy;
 | |
| begin
 | |
|   Close;
 | |
|   inherited Destroy;
 | |
| end;
 | |
| 
 | |
| procedure TPseudoTerminal.Close;
 | |
| begin
 | |
|   CloseInp;
 | |
|   if FPTy <> InvalHandle
 | |
|   then __Close(FPTy);
 | |
|   FPTy := InvalHandle;
 | |
| end;
 | |
| 
 | |
| procedure TPseudoTerminal.Open;
 | |
| const
 | |
|   BufLen = 100;
 | |
| var
 | |
|   ios: termios;
 | |
|   int1: integer;
 | |
| 
 | |
|   procedure Error;
 | |
|   begin
 | |
|     if FPTy <> InvalHandle
 | |
|     then __Close(FPTy);
 | |
|     FPTy := InvalHandle;
 | |
|     FDeviceName := '';
 | |
|   end;
 | |
| 
 | |
| begin
 | |
|   Close;
 | |
|   FPTy := getpt;
 | |
|   if FPTy < 0 then Error;
 | |
|   if (grantpt(FPTy) < 0) or (unlockpt(FPTy) < 0) then begin
 | |
|     Error;
 | |
|     exit;
 | |
|   end;
 | |
|   setlength(FDeviceName, BufLen);
 | |
|   if ptsname_r(FPTy, @FDeviceName[1], BufLen) < 0 then begin
 | |
|     Error;
 | |
|     exit;
 | |
|   end;
 | |
|   setlength(FDeviceName,length(pchar(FDeviceName)));
 | |
|   if tcgetattr(FPTy, @ios) <> 0 then begin
 | |
|     Error;
 | |
|     exit;
 | |
|   end;
 | |
|   ios.c_lflag:= ios.c_lflag and not (icanon); // or echo);
 | |
|   ios.c_cc[vmin]:= 1;
 | |
|   ios.c_cc[vtime]:= 0;
 | |
|   if tcsetattr(FPTy, tcsanow, @ios) <> 0 then begin
 | |
|     Error;
 | |
|     exit;
 | |
|   end;
 | |
| 
 | |
|   int1 := fcntl(FPTy, f_getfl, 0);
 | |
|   if int1 = InvalHandle then begin
 | |
|     Error;
 | |
|     exit;
 | |
|   end;
 | |
|   if fcntl(FPTy, f_setfl, int1 or o_nonblock) = InvalHandle then Error;
 | |
| end;
 | |
| 
 | |
| function TPseudoTerminal.Write(s: string): Integer;
 | |
| var
 | |
|   int1, nbytes: Integer;
 | |
|   p: PChar;
 | |
| begin
 | |
|   nbytes := length(s);
 | |
|   if (FPTy = InvalHandle) or (nbytes = 0) then exit(0);
 | |
|   Result:= nbytes;
 | |
|   p := @s[1];
 | |
|   repeat
 | |
|     int1 := __write(FPTy, p^, nbytes);
 | |
|     if int1 = -1 then begin
 | |
|       if errno <> eintr then begin
 | |
|         Result:= int1;
 | |
|         break;
 | |
|       end;
 | |
|       continue;
 | |
|     end;
 | |
|     inc(p, int1);
 | |
|     dec(nbytes, int1);
 | |
|   until integer(nbytes) <= 0;
 | |
| end;
 | |
| 
 | |
| function TPseudoTerminal.Read: String;
 | |
| const
 | |
|   BufLen = 1024;
 | |
| var
 | |
|   buf: String;
 | |
|   i: Integer;
 | |
| begin
 | |
|   if (FPTy = InvalHandle) then exit('');
 | |
| 
 | |
|   SetLength(buf, BufLen + 1);
 | |
|   Result := FReadBuf;
 | |
|   FReadBuf := '';
 | |
|   repeat
 | |
|     i := __read(FPTy, buf[1], BufLen);
 | |
|     if i > 0 then Result := Result + copy(buf, 1, i);
 | |
|   until i <= 0;
 | |
| end;
 | |
| 
 | |
| procedure TPseudoTerminal.CheckCanRead;
 | |
| begin
 | |
|   FReadBuf := Read;
 | |
|   if (FReadBuf <> '') and assigned(FOnCanRead)
 | |
|   then FOnCanRead(self);
 | |
| end;
 | |
| 
 | |
| {$ENDIF}
 | |
| 
 | |
| end.
 | |
| 
 | 
