Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Delphi Effizientes Einlesen und Verarbeiten von Textdatei (https://www.delphipraxis.net/210935-effizientes-einlesen-und-verarbeiten-von-textdatei.html)

Dalai 30. Jun 2022 21:05

Effizientes Einlesen und Verarbeiten von Textdatei
 
Hallo *.*.

Gegeben ist eine Textdatei, konkret im PEM-Format (BASE64 codiert). Ziel ist es, alle Blöcke dieser Datei einzulesen in ein Byte-Array - letztlich muss jeder Block in eine MSDN-Library durchsuchenCERT_BLOB-Struktur. Bisher mache ich das folgendermaßen:
Delphi-Quellcode:
procedure NameNotRelevant;
var
  Lsl: TMyStringList;
  LidxH, LidxF: integer;
  Lstr: string;
begin
    Lsl:= TMyStringList.Create;
    try
      Lsl.LoadFromFile(FFileName);
      repeat
          LidxH:= Lsl.IndexOf(PEM_HEADER);
          LidxF:= Lsl.IndexOf(PEM_FOOTER);
          if LidxH < 0 then
            NameNotRelevant2(Lstr)
          else begin
            Lstr:= Trim(Lsl.StringsBetween(LidxH+1, LidxF-1));
            NameNotRelevant2(Lstr);
            Lsl.DeleteMultiple(LidxF-LidxH+1, LidxH);
          end;
      until LidxH < 0;
    finally
      Lsl.Free;
    end;
end;

procedure NameNotRelevant2(const ACertStr: string);
var
  Lbin: TBytes;
  Lblob: CERT_BLOB;
begin
    if Length(ACertStr) = 0 then Exit;
    Lblob.cbData:= (Length(ACertStr)) * SizeOf(Char);
    SetLength(Lbin, Lblob.cbData);
    Move(ACertStr[1], Lbin[0], Lblob.cbData);
    Lblob.pbData:= LPByte(Lbin);
    // Do something with Lblob

(*    GetMem(Lblob.pbData, Lblob.cbData+1);
    try
      Move(ACertStr[1], Lblob.pbData, Lblob.cbData);
    finally
      FreeMem(Lblob.pbData);
    end;*)
end;
Die Methoden StringsBetween() und DeleteMultiple() hab ich ergänzt, und die Namen sollten selbsterklärend sein, aber sicherheitshalber hier noch deren Deklaration und Inhalt:
Delphi-Quellcode:
function TMyStringList.StringsBetween(AIndexFrom, AIndexTo: integer): string;
var
  i: integer;
begin
    Result:= '';
    if (AIndexFrom < 0) OR (AIndexTo >= Self.Count) then
        Exit;
    for i:= AIndexFrom to AIndexTo do
        Result:= Result + Self.Strings[i];
end;

function TMyStringList.DeleteMultiple(ACount: integer; AIndex: integer = 0): integer;
var
  i: integer;
begin
    Result:= -1;
    if (AIndex < 0) OR (AIndex >= Self.Count) then
        Exit;
    if (ACount >= Self.Count) then begin
        Result:= Self.Count;
        Clear;
    end else begin
        for i:= 1 to ACount do begin
            Self.Delete(AIndex);
            Inc(Result);
        end;
    end;
end;
Irgendwie finde ich die Sache ineffizent. Daher folgende Fragen:
  1. Kann man die Verarbeitung der Datei anders gestalten, beispielsweise mit einem Stream, und wenn ja, wie suche ich in einem solchen nach einem bzw. mehreren bestimmten Strings?
  2. Der Zwischenschritt über TBytes (= array of Byte) ist meiner Meinung nach unnötig. Irgendwie bin ich aber zu blöd, direkt auf Lblob.pbData den Speicher zu reservieren, die Bytes des Strings mit Move dorthin zu kopieren, und anschließend den Speicher mit FreeMem wieder freizugeben, ohne dass es knallt (siehe auskommentierter Teil). Ja, ich weiß, dass Move die Adresse des Zielspeichers ändert und es deshalb knallt. Wie kann ich denn sonst die Bytes des Strings dorthin übertragen?

Grüße
Dalai

freimatz 1. Jul 2022 09:04

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Hallo,
inwiefern ineffizient? Vermutest Du oder hast Du gemessen? Was die Laufzeit betrifft denke ich, dass LoadFromFile die allermeiste Zeit benötigt.

Deinen Code verstehe ich nicht, aber das "until Lidx < 0;" kommt mir komisch vor. Lidx kommt sonst nicht vor.

Sinspin 1. Jul 2022 09:16

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von Dalai (Beitrag 1508233)
Ja, ich weiß, dass Move die Adresse des Zielspeichers ändert und es deshalb knallt.

Was? Nee, noch nie gehört.
Ich denke dann gibst Du bei Move einen Parameter falsch an.

Neutral General 1. Jul 2022 09:26

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Delphi-Quellcode:
procedure NameNotRelevant2(const ACertStr: string);
var
  Lbin: TBytes;
  Lblob: CERT_BLOB;
begin
    if Length(ACertStr) = 0 then Exit;
    Lblob.cbData:= (Length(ACertStr)) * SizeOf(Char);
    SetLength(Lbin, Lblob.cbData);
    Move(ACertStr[1], Lbin[0], Lblob.cbData);
    Lblob.pbData:= LPByte(Lbin);
Man sieht dass du den Code etwas gekürzt/vereinfacht hast um ihn hier zu posten von daher trifft meine Kritik unter Umständen nicht zu, aber
Delphi-Quellcode:
.
Lblob.pbData:= LPByte(Lbin);
Wird nur innerhalb dieser Methode funktionieren. Wird auf den CERT_BLOB außerhalb dieser Methode zugegriffen ist das TBytes Array nicht mehr gültig und lBlob.pbData zeigt auf ungültige Daten.
So geht der direkte Weg ohne TBytes:
Delphi-Quellcode:
procedure NameNotRelevant2(const ACertStr: string);
var
  Lblob: CERT_BLOB;
begin
    if Length(ACertStr) = 0 then Exit;
    Lblob.cbData:= (Length(ACertStr)) * SizeOf(Char);
    GetMem(Lblob.pbData, Lblob.cbData);
    Move(ACertStr[1], Lblob.pbData^, Lblob.cbData);
Du musst dann aber dran denken lBlob.pbData mit FreeMem freizugeben, wenn der BLOB nicht mehr gebraucht wird.

EDIT: Ich weiß zwar nicht was du da machst, aber solltest du den Base64 String nicht vorher decoden und dann erst dem Blob zuordnen?

TiGü 1. Jul 2022 10:03

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Der Zwischenschritt über TBytes (= array of Byte) ist meiner Meinung nach unnötig. Irgendwie bin ich aber zu blöd, direkt auf Lblob.pbData den Speicher zu reservieren, die Bytes des Strings mit Move dorthin zu kopieren, und anschließend den Speicher mit FreeMem wieder freizugeben, ohne dass es knallt (siehe auskommentierter Teil). Ja, ich weiß, dass Move die Adresse des Zielspeichers ändert und es deshalb knallt. Wie kann ich denn sonst die Bytes des Strings dorthin übertragen?
Falls es dir irgendwie eine Hilfe ist, aber in der HTTP-Request-Klasse der höheren Delphi-Versionen in der Unit System.Net.HttpClient.Win wird auch mit einem Stream (FClientCertificate) der Umweg über ein dynamisches TBytes-Array gegangen.

Delphi-Quellcode:
procedure TWinHTTPRequest.SetWinCertificate;
var
  LStore: HCERTSTORE;
  LCertContext: PCCERT_CONTEXT;
  LBlob: CRYPT_DATA_BLOB;
  LBytes: TBytes;
begin
  if (FClientCertPath = '') and (FClientCertificate = nil) then
    Exit;

  if FClientCertPath <> '' then
    LStore := CertOpenStore(PAnsiChar(CERT_STORE_PROV_FILENAME), X509_ASN_ENCODING or PKCS_7_ASN_ENCODING,
      0, CERT_STORE_OPEN_EXISTING_FLAG or CERT_STORE_READONLY_FLAG, PChar(FClientCertPath))
  else
  begin
    LBlob.cbData := FClientCertificate.Size; // <--- siehe von hier...
    SetLength(LBytes, LBlob.cbData);
    FClientCertificate.Position := 0;
    FClientCertificate.Read(LBytes, LBlob.cbData);
    LBlob.pbData := PByte(@LBytes[0]); // <--- ...bis hier!
    LStore := PFXImportCertStore(@LBlob, PChar(FClientCertPassword), 0);
  end;
  if LStore = nil then
    raise ENetHTTPRequestException.CreateResFmt(@SNetHttpCertFileOpenError,
      [GetLastError, SysErrorMessage(GetLastError, TWinHttpLib.Handle)]);
  try
    LCertContext := CertFindCertificateInStore(LStore, X509_ASN_ENCODING or PKCS_7_ASN_ENCODING,
      0, CERT_FIND_ANY, nil, nil);
    if LCertContext = nil then
      raise ENetHTTPRequestException.CreateResFmt(@SNetHttpCertNotFoundError,
        [GetLastError, SysErrorMessage(GetLastError, TWinHttpLib.Handle)]);
    try
      WinHttpSetOption(FWRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
        LCertContext, SizeOf(CERT_CONTEXT));
    finally
      CertFreeCertificateContext(LCertContext);
    end;
  finally
    CertCloseStore(LStore, 0);
  end;
end;
Delphi-Quellcode:
// mit der Definition aus Winapi.Windows
type
  _CRYPTOAPI_BLOB = record
    cbData: DWORD;
    pbData: PBYTE;
  end;
...
CRYPT_DATA_BLOB = _CRYPTOAPI_BLOB;

Dalai 1. Jul 2022 13:42

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von freimatz (Beitrag 1508252)
Vermutest Du oder hast Du gemessen?

Da ich aus der Vergangenheit hier im Forum weiß, dass Messen in sich ein komplexes Thema ist, habe ich nicht gemessen. Aber trotzdem dauert das Verarbeiten einer Datei mit 132 solcher Blöcke ca. 2 Sekunden (jedenfalls wenn es im externen Speicherlecksucher läuft). Wie gesagt, nicht besonders genau, weil lediglich mitgezählt.

Zitat:

Was die Laufzeit betrifft denke ich, dass LoadFromFile die allermeiste Zeit benötigt.
Das muss ich in der Tat mit dem Debugger noch genauer untersuchen, ob es wirklich am Laden liegt. Aber ich weiß, dass Strings in Bezug auf Ressourcen deutlich teurer sind als sonstige einfach Datentypen, vor allem weil ich ja nach und nach Zeilen aus der Stringliste lösche. Daher kann ich mir gut vorstellen, dass andere Wege effizienter sind.

Zitat:

Deinen Code verstehe ich nicht, aber das "until Lidx < 0;" kommt mir komisch vor. Lidx kommt sonst nicht vor.
Sorry, das kommt davon, wenn man Code für solche Fragen verkürzt. Hab's grade korrigiert.


Zitat:

Zitat von Sinspin (Beitrag 1508253)
Was? Nee, noch nie gehört.
Ich denke dann gibst Du bei Move einen Parameter falsch an.

Dest ist bei Move doch ein
Delphi-Quellcode:
var
-Parameter. Daraus schloss ich, dass dessen Adresse verändert werden kann. Was ist denn deiner Meinung nach falsch an dem auskommentierten Move-Aufruf?

Grüße
Dalai

Dalai 1. Jul 2022 14:23

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von Neutral General (Beitrag 1508255)
[...] aber
Delphi-Quellcode:
.
Lblob.pbData:= LPByte(Lbin);
Wird nur innerhalb dieser Methode funktionieren. Wird auf den CERT_BLOB außerhalb dieser Methode zugegriffen ist das TBytes Array nicht mehr gültig und lBlob.pbData zeigt auf ungültige Daten.

Ja, das ist mir klar. Die Variablen werden nur innerhalb dieser Methode verwendet.

Zitat:

So geht der direkte Weg ohne TBytes:
Delphi-Quellcode:
procedure NameNotRelevant2(const ACertStr: string);
var
  Lblob: CERT_BLOB;
begin
    if Length(ACertStr) = 0 then Exit;
    Lblob.cbData:= (Length(ACertStr)) * SizeOf(Char);
    GetMem(Lblob.pbData, Lblob.cbData);
    Move(ACertStr[1], Lblob.pbData^, Lblob.cbData);

Danke, das funktioniert tatsächlich. Warum muss Lblob.pbData dereferenziert werden? Was übersehe ich?

Zitat:

Du musst dann aber dran denken lBlob.pbData mit FreeMem freizugeben, wenn der BLOB nicht mehr gebraucht wird.
Deswegen steht ja das FreeMem bereits da ;).

Zitat:

EDIT: Ich weiß zwar nicht was du da machst, aber solltest du den Base64 String nicht vorher decoden und dann erst dem Blob zuordnen?
Das kommt darauf an, an welche Funktion man diesen CRYPT_BLOB übergibt. In diesem Fall überlasse ich das der Windows API, konkret der Funktion CryptQueryObject.

Grüße
Dalai

BerndS 1. Jul 2022 14:43

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Effizienter wäre es sicher, wenn das Auswerten der Stringlist ohne IndexOf und Delete gemacht würde. Das sind zusätzliche Speicheroperationen, die nicht nötig wären, wenn man es in etwas so machen würde.

Delphi-Quellcode:
var
  Lsl: TStringList;
  LidxH, LidxF: integer;
  Lstr, Line: string;
  I: Integer;
begin
    Lsl:= TStringList.Create;
    try
      Lsl.LoadFromFile(FFileName);
      LidxH := -1;
      LidxF := -1;
      Lstr := '';
      for I := 0 to lsl.Count -1  do
      begin
        Line := lsl[I].Trim;
        case IndexStr(Line, [PEM_HEADER, PEM_FOOTER]) of
          0: LidxH := I;
          1:
            if LidxH>=0 then
            begin
              NameNotRelevant2(Lstr);
              LidxH := -1;
              LidxF := -1;
              Lstr := '';
            end;
          else
           if LidxH >= 0 then
             Lstr := Lstr + Line;
        end;
      end;
    finally
      Lsl.Free;
    end;
end;
Es geht sicher auch, wie du es machst.
Ob diese Änderung aber schneller ist, kann ich ohne geeignete Daten nicht prüfen.
Um die Zeitdauer von bestimmten Operationen zu messen, werwende ich gerne GetTickCount. Das ist zwar nicht genau, aber man bekommt dadurch grob raus, wo es länger dauert.

Neutral General 1. Jul 2022 16:44

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von Dalai (Beitrag 1508269)
Danke, das funktioniert tatsächlich. Warum muss Lblob.pbData dereferenziert werden? Was übersehe ich?

Delphi-Quellcode:
procedure Move(const Source; var Dest; Count: NativeInt);

Wie du siehst sind für Source und Dest keine Datentypen angegeben.

Was dort intern passiert ist, dass in der Wahrheit immer ein Pointer dessen übergeben wird was du als Parameter angibst.
Im Prinzip wird bei Source und Dest ein unsichtbares "@" vor den übergebenen Wert gesetzt.

Das hier:
Delphi-Quellcode:
Move(ACertStr[1], Lblob.pbData^, Lblob.cbData)

ist unter der Haube letztendlich
Delphi-Quellcode:
Move(@ACertStr[1], @(Lblob.pbData^), Lblob.cbData) // Was wirklich übergeben wird


D.h. wenn du bei Lblob.pbData das ^ weglässt schreibt er die Daten nach @Lblob.pbData, was den Inhalt des Pointers selbst überschreibt statt den Speicher auf den der Pointer zeigt.
Durch das dereferenzieren bekommst du dann quasi sowas @(Lblob.pbData^) was sich wieder ausgleicht und ausgewertet Lblob.pbData ergibt wodurch dann tatsächlich dahin geschrieben wird, wo der Pointer hinzeigt, statt den Pointer selbst zu überschreiben.

Ich hoffe das war halbwegs verständlich (Erklären ist nicht immer so einfach :mrgreen:)

Uwe Raabe 1. Jul 2022 16:47

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von BerndS (Beitrag 1508270)
Um die Zeitdauer von bestimmten Operationen zu messen, werwende ich gerne GetTickCount. Das ist zwar nicht genau, aber man bekommt dadurch grob raus, wo es länger dauert.

Genauere Zeiten und mindestens ebenso bequem bekommt man es mit TStopWatch aus System.Diagnostics:
Delphi-Quellcode:
var sw := TStopWatch.StartNew;
...
Writeln(sw.ElapsedMilliseconds);

Andreas13 1. Jul 2022 17:08

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
@Uwe :thumb:
Das hat den Vorteil einer seeeehr genauen Zeitmessung, weil die intern verwendete Funktion
Delphi-Quellcode:
QueryPerformanceCounter
einen "High Resolution Counter" mit einer Genauigkeit von 0.00083 ms (!) abfragt.
Gruß, Andreas

Dalai 1. Jul 2022 19:31

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von Neutral General (Beitrag 1508272)
Ich hoffe das war halbwegs verständlich (Erklären ist nicht immer so einfach :mrgreen:)

Ja, das ist verständlich. Danke! :thumb: Nun ist klar, warum es knallen musste. Endlich kann ich auf die Zwischenvariable verzichten.

-----

Zwischenzeitlich hatte ich auch einen anderen Weg getestet, bei dem keine Strings aus der Liste gelöscht werden und stattdessen eine Variable mit der aktuellen Zeile hochgezählt wird, ab der in der Schleife gesucht wird. Der Unterschied in der Laufzeit war zu vernachlässigen. Mit den ordentlichen Zeitmessungen muss ich mich noch beschäftigen.

Grüße
Dalai

Dalai 2. Jul 2022 19:02

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Nach ein paar Tests mit TStopwatch und einer absichtlich großen PEM-Datei (etwas über 1 MiB) stelle ich Folgendes fest:
  1. Das Problem ist nicht so groß wie es anfänglich schien. Externe Speicherlecksucher schauen eben genauer hin, was seine Zeit braucht. Das hat mich glauben lassen, die Implementation an sich wäre langsam. Naja, im Vergleich mit anderen ist sie das auch, siehe nächster Punkt :)
  2. Der Weg mit dem Löschen der Strings ist trotzdem langsamer ist als Hochzählen eines Index

Das Verarbeiten der Textdatei (nach LoadFromFile!) mittels Löschen von Strings - siehe OP - dauert um die 200ms, mit dem Hochzählen des Index nur zwischen ca. 20 und 40ms.

Ich habe mich nun für das Hochzählen des Index mit folgender Implementation entschieden:
Delphi-Quellcode:
procedure NameNotRelevant;
var
  Lsl: TMyStringList;
  LidxH, LidxF: integer;
  Lheaderpresent: Boolean;
  Lstr: string;
begin
    Lsl:= TMyStringList.Create;
    try
      Lsl.LoadFromFile(FFileName);
      LidxF:= 0;
      Lheaderpresent:= False;
      repeat
          LidxH:= Lsl.IndexOf(PEM_HEADER, LidxF);
          LidxF:= Lsl.IndexOf(PEM_FOOTER, LidxF+1);
          if LidxH < 0 then begin
            if NOT Lheaderpresent then
                NameNotRelevant2(Lstr);
          end else begin
            Lheaderpresent:= True;
            Lstr:= Trim(Lsl.StringsBetween(LidxH+1, LidxF-1));
            NameNotRelevant2(Lstr);
            Inc(LidxF);
          end;
      until LidxH < 0;
    finally
      Lsl.Free;
    end;
end;

procedure NameNotRelevant2(const ACertStr: string);
var
  Lblob: CERT_BLOB;
begin
    if Length(ACertStr) = 0 then Exit;
    Lblob.cbData:= Length(ACertStr) * SizeOf(Char);
    GetMem(Lblob.pbData);
    try
      Move(ACertStr[1], Lblob.pbData^, Lblob.cbData);
      // Do something with Lblob
    finally
      FreeMem(Lblob.pbData);
    end;
end;
IndexOf hab ich frecherweise aus dem Quellcode von TStrings.IndexOf kopiert und lediglich einen Parameter ergänzt:
Delphi-Quellcode:
function TMyStringList.IndexOf(const S: string; StartIndex: integer): integer;
begin
  for Result := StartIndex to GetCount - 1 do
    if AnsiCompareText(Get(Result), S) = 0 then Exit;
  Result := -1;
end;
Wieso gibt's davon eigentlich standardmäßig keine solche Überladung?

-----

Wenn jemand eine schnellere Variante hat, kann die gern gepostet werden.

Auf alle Fälle danke ich allen Postern für die rege Beteiligung! :dp:

Grüße
Dalai

Delphi.Narium 2. Jul 2022 21:03

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Musst Du auf Groß-/Kleinschreibung achten bzw. nicht beachten?
Sind die zu suchenden Zeichenfolgen (PEM_HEADER bzw. PEM_FOOTER immer gleich geschrieben?

Im zweiten Fall könntest Du das AnsiCompareText weglassen. Das Dauer durchaus auch seine Zeit.

Uwe Raabe 2. Jul 2022 22:18

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Ich würde einen Ansatz per TStreamReader vorschlagen. Dabei wird nicht erst die ganze Datei in den Speicher geladen und es sind weder IndexOf noch StringsBetween nötig:
Delphi-Quellcode:
uses
  System.SysUtils,
  System.Classes;

...

procedure NameNotRelevant;
var
  reader: TStreamReader;
  line: string;
  data: string;
begin
  reader := TStreamReader.Create(FFileName);
  try
    data := '';
    while not reader.EndOfStream do begin
      line := reader.ReadLine;
      if line = PEM_HEADER then begin
        data := '';
      end
      else if line = PEM_FOOTER then begin
        NameNotRelevant2(data);
        data := '';
      end
      else begin
        data := data + TrimRight(line);
      end;
    end;
  finally
    reader.Free;
  end;
end;

Dalai 4. Jul 2022 17:03

AW: Effizientes Einlesen und Verarbeiten von Textdatei
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1508284)
Ich würde einen Ansatz per TStreamReader vorschlagen. Dabei wird nicht erst die ganze Datei in den Speicher geladen und es sind weder IndexOf noch StringsBetween nötig:

Interessanter Ansatz, danke. Leider ist diese Variante in meinen Tests langsamer. Der Gesamtvorgang braucht zwischen 60 und 100 ms, mit der Stringliste gesamt zwischen 40 und 60 ms. Wie gesagt, das ist die Gesamtzeit, also Einlesen mit LoadFromFile und anschließende Verabeitung. Daher sind diese Zeitangaben nicht mit den o.g. vergleichbar.

Die Variante hat noch einen weiteren Nachteil: Dateien ohne Header werden nicht berücksichtigt. Ja, das hatte ich nicht erwähnt, und deshalb konnte das keiner sonst wissen.

Zitat:

Zitat von Delphi.Narium (Beitrag 1508283)
Musst Du auf Groß-/Kleinschreibung achten bzw. nicht beachten?

Die Frage hab ich mir auch gestellt. Aktuell weiß ich es nicht. Da muss ich noch im zugehörigen RFC nachgrasen, wie die Header auszusehen haben.

Zitat:

Im zweiten Fall könntest Du das AnsiCompareText weglassen. Das Dauer durchaus auch seine Zeit.
Nach meinen Messungen macht es kaum etwas aus, einzelne Millisekunden mehr bzw. weniger. Relevanter als die CPU-Zeit wäre in der Tat die Schreibweise.

Grüße
Dalai


Alle Zeitangaben in WEZ +1. Es ist jetzt 13:13 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz