File: Parser.pas

package info (click to toggle)
ddrescueview 0.4.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,212 kB
  • sloc: pascal: 2,770; makefile: 8
file content (532 lines) | stat: -rw-r--r-- 18,544 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
(*
   Parser.pas - Mapfile parser unit

   Copyright (C) 2013 - 2022 Martin Bittermann (martinbittermann@gmx.de)

   This file is part of ddrescueview.

   ddrescueview 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 3 of the License, or
   (at your option) any later version.

   ddrescueview 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.

   You should have received a copy of the GNU General Public License
   along with ddrescueview.  If not, see <http://www.gnu.org/licenses/>.
*)

// {$DEFINE VerboseProfiler}

unit Parser;

interface
uses Classes, Shared;

type

  { TSimpleParser }

  TSimpleParser = class(TObservablePersistent)
  private
    FMap : TMap;
    FRescueStatus : TRescueStatus;
    FComments : TStringList; // comment lines of mapfile
    FVersion : String; // ddrescue version in first comment line / for future use?
    FFileName : String;
    procedure logMsg(msg : String);
    function isEmptyLine(strLine : String) : boolean;
    function isCommentLine(strLine : String) : boolean;
  public
    constructor Create;
    destructor Destroy; override;
    procedure OpenFile(filename : String);
    procedure CloseFile();
    function parse(forceSameDevSize : Boolean) : Boolean;
    function hasFile() : boolean;
    property rescueStatus : TRescueStatus read FRescueStatus;
    property map : TMap read FMap;
    property CommentLines : TStringList read FComments;
    property Version : String read FVersion;
    property MapFileName : String read FFileName;
  end;


  { TMapParser }

  // This parser can also embed a domain mapfile parser
  TMapParser = class(TObservablePersistent)
  private
    FMapParser : TSimpleParser;
    FDomParser : TSimpleParser;
    FMap: TMap;
    FRescueStatus : TRescueStatus;
    FContiguous : Boolean;
    FHasFile : Boolean;
    function getMap(): TMap;
    procedure postParse();
    procedure setContiguous(mode: Boolean);
    function getCommentLines(): TStringList;
    function getVersion(): String;
    function getMapFileName(): String;
    function getDomFileName(): String;
  public
    constructor Create;
    destructor Destroy; override;
    procedure OpenFile(filename : String);
    procedure OpenDomainFile(filename : String);
    procedure CloseFile();
    function parse(forceSameDevSize : Boolean) : Boolean;
    function hasFile() : boolean;
    function hasDomFile() : Boolean;
    property rescueStatus : TRescueStatus read FRescueStatus;
    property map : TMap read getMap;
    property CommentLines : TStringList read GetCommentLines;
    property Version : String read getVersion;
    property MapFileName : String read getMapFileName;
    property DomFileName : String read getDomFileName;
    property ContiguousDomain : Boolean read FContiguous write setContiguous;
  end;

implementation
uses SysUtils, Math, GUI;

{ TMapParser }

constructor TMapParser.Create;
begin
  inherited;
  FRescueStatus := emptyRescueStatus;
  FMapParser:=TSimpleParser.Create;
  FDomParser:=TSimpleParser.Create;
end;

destructor TMapParser.Destroy;
begin
  CloseFile();
  FreeAndNil(FDomParser);
  FreeAndNil(FMapParser);
  inherited Destroy;
end;

procedure TMapParser.OpenFile(filename: String);
begin
  CloseFile;
  FMapParser.OpenFile(filename);
  postParse;
end;

procedure TMapParser.OpenDomainFile(filename: String);
begin
  FDomParser.OpenFile(filename);
  if FDomParser.hasFile and FContiguous then begin
    FHasFile:=false;
    FPONotifyObservers(self, ooDeleteItem, nil);
  end;
  postParse;
end;

procedure TMapParser.CloseFile();
begin
  FDomParser.CloseFile;
  FMapParser.CloseFile;
  FRescueStatus := emptyRescueStatus;
  SetLength(FMap, 0);
  FHasFile:=false;
  FPONotifyObservers(self, ooDeleteItem, nil);
end;

procedure TMapParser.postParse();
var
  ResMap, DomMap: TMap;
  i, iRes, iDom : Longint;
  newEntry : TMapEntry;
  mapCurOffset : Int64;
begin
  if FDomParser.hasFile then begin // post-process the maps
    FRescueStatus:=emptyRescueStatus;
    ResMap:=FMapParser.map;
    DomMap:=FDomParser.map;
    //build map and rescuestatus
    i:=0; iRes:=0; iDom:=0; // current map indices
    mapCurOffset:=0; // needed for contiguous mode, but also used for normal mode
    while iDom < Length(DomMap) do begin
      if (DomMap[iDom].status<>'+') then begin // outside domain entry
        if not FContiguous then begin
          newEntry.status:='d';
          newEntry.offset:=DomMap[iDom].offset;
          newEntry.length:=DomMap[iDom].length;
          inc(FRescueStatus.outsidedomain, DomMap[iDom].length);
          mapCurOffset:=newEntry.offset+newEntry.length;
          if InRange(FMapParser.rescueStatus.pos, newEntry.offset, mapCurOffset-1) then
             FRescueStatus.pos:=FMapParser.rescueStatus.pos;
          if Length(FMap) <= i then SetLength(FMap, i+1024);
          FMap[i]:=newEntry;
          Inc(i);
        end;
      end else begin // iterate over the ResEntries intersecting current DomEntry
        while iRes < Length(ResMap) do begin
          // increase iRes until intersecting dom block
          if ResMap[iRes].offset+ResMap[iRes].length <= DomMap[iDom].offset then begin
            Inc(iRes);
            Continue;
          end;
          // if intersecting current dom block, create new block
          newEntry:=IntersectEntries(ResMap[iRes], DomMap[iDom]);
          if newEntry.length > 0 then begin //can be 0 for rescue mapfiles with gaps
            if InRange(FMapParser.rescueStatus.pos, newEntry.offset,
                       newEntry.offset+newEntry.length-1) then
              FRescueStatus.pos:=mapCurOffset+FMapParser.rescueStatus.pos-newEntry.offset;
            newEntry.offset:=mapCurOffset;
            mapCurOffset+=newEntry.length;
            if Length(FMap) <= i then SetLength(FMap, i+1024);
            FMap[i]:=newEntry;
            Inc(i);
            case newEntry.status of
              '?' : inc(FRescueStatus.nontried, newEntry.length);
              '+' : inc(FRescueStatus.rescued, newEntry.length);
              '*' : inc(FRescueStatus.nontrimmed, newEntry.length);
              '/' : inc(FRescueStatus.nonscraped, newEntry.length);
              '-' : begin
                      inc(FRescueStatus.bad, newEntry.length);
                      inc(FRescueStatus.errors);
                    end;
            end;
          end;
          // ensure iRes is set to last intersecting entry
          if ResMap[iRes].offset+ResMap[iRes].length >=
             DomMap[iDom].offset+DomMap[iDom].length then break;
          Inc(iRes);
        end;
      end;
      Inc(iDom);
    end;
    SetLength(FMap, i);
    FRescueStatus.curOperation:=FMapParser.rescueStatus.curOperation;
    FRescueStatus.strCurOperation:=FMapParser.rescueStatus.strCurOperation;
    if i > 0 then begin  // calculate device's size
      FRescueStatus.devicesize:=FMap[i-1].offset + FMap[i-1].length;
    end;
    FRescueStatus.suggestedBlockSize:=FMapParser.rescueStatus.suggestedBlockSize;
  end else begin
    FRescueStatus:=FMapParser.rescueStatus;
  end;
  if (not FHasFile) and FMapParser.hasFile then begin  // just been opened
    FHasFile:=true;
    FPONotifyObservers(self, ooAddItem, nil);
  end else FPONotifyObservers(self, ooChange, nil)
end;

procedure TMapParser.setContiguous(mode: Boolean);
begin
  if mode <> FContiguous then begin
    FContiguous:=mode;
    if FDomParser.hasFile then begin
      FHasFile:=false;
      FPONotifyObservers(self, ooDeleteItem, nil);
      postParse;
    end;
  end;
end;

function TMapParser.parse(forceSameDevSize : Boolean) : Boolean;
begin
  result := True;
  if FMapParser.hasFile() then result := FMapParser.parse(forceSameDevSize);
  if FDomParser.hasFile() then FDomParser.parse(false);
  postParse;
end;

function TMapParser.hasFile(): boolean;
begin
  result:=FHasFile;
end;

function TMapParser.hasDomFile(): Boolean;
begin
  result:=FHasFile and FDomParser.hasFile;
end;

function TMapParser.getMap(): TMap;
begin
  if not FDomParser.hasFile then result:=FMapParser.map
  else result:=FMap;
end;

function TMapParser.getCommentLines(): TStringList;
begin
  getCommentLines:=FMapParser.FComments;
end;

function TMapParser.getVersion(): String;
begin
  getVersion:=FMapParser.FVersion;
end;

function TMapParser.getMapFileName(): String;
begin
  getMapFileName:=FMapParser.FFileName;
end;

function TMapParser.getDomFileName(): String;
begin
  getDomFileName:=FDomParser.FFileName;
end;

{ TSimpleParser }

constructor TSimpleParser.Create;
begin
  inherited;
  FRescueStatus := emptyRescueStatus;
end;

destructor TSimpleParser.Destroy;
begin
  CloseFile();
  inherited;
end;

procedure TSimpleParser.OpenFile(filename : String);
var mapStream : TFileStream;
begin
  try
    if hasFile() then CloseFile(); // close already open file
    // check mapfile access once, and close the file again.
    mapStream := TFileStream.Create(filename, fmOpenRead or fmShareDenyNone);
    FreeAndNil(mapStream);
    // file checked, it can be considered 'open' now.
    FComments := TStringList.Create;
    FFileName := filename;
    parse(false);  // start parsing after opening
  except
    on E: Exception do begin
        CloseFile;
        logMsg('Parser error: '+E.Message);
    end;
  end;
end;

procedure TSimpleParser.CloseFile;
begin
  FFileName := '';
  FreeAndNil(FComments);
  FRescueStatus := emptyRescueStatus;
  SetLength(FMap, 0);
  FPONotifyObservers(self, ooDeleteItem, nil);
end;

(* returns true if a line is empty, assuming the line is already trimmed *)
function TSimpleParser.isEmptyLine(strLine : String) : boolean;
begin
  result := (Length(strLine) = 0); // empty line
end;

(* returns true if a line is a comment line, assuming the line is already trimmed *)
function TSimpleParser.isCommentLine(strLine : String) : boolean;
begin
  result := (Length(strLine) > 0) and (strLine[1] = '#'); // comment line
end;

(* add a log line to the application log *)
procedure TSimpleParser.logMsg(msg : String);
begin
  MainForm.DbgLog.Lines.Add(msg);
end;

(* Parse the mapfile.
   After parsing, the results are available in
   these properties: map, rescueStatus and Comments *)
function TSimpleParser.parse(forceSameDevSize : Boolean) : Boolean; //success
var
  line : string;
  token : array[0..2] of string;
  i, mapEntry, lineIdx, idx : Integer;
  mapStream : TFileStream = nil;
  mapStrings : TStringList = nil;
  prevHadFile, statuslineFound : boolean;
  newMap : TMap;
  newRescueStatus : TRescueStatus;
  newComments : TStringList;
  newVersion : String;
  {$IFDEF VerboseProfiler}
  lTimeStart: TDateTime;
  {$ENDIF}
begin
  result:=False;
  // make sure the parser has a file
  if not hasFile() then begin
    logMsg('Parser: No mapfile opened.');
    FPONotifyObservers(self, ooChange, nil);
    exit;
  end;
  {$IFDEF VerboseProfiler}
  lTimeStart := Now();
  {$ENDIF}
  // open the file and read its contents into string list
  try
    try
      mapStream := TFileStream.Create(FFilename, fmOpenRead or fmShareDenyNone);
      if mapStream.Seek(0, soFromBeginning) <> 0 then
        raise Exception.Create('Seek error!');
      mapStrings := TStringList.Create;
      mapStrings.LoadFromStream(mapStream);
      if mapStrings.Count = 0 then
        raise Exception.Create('File is empty!');
      logMsg('Reading mapfile: ' +IntToStr(mapStrings.Count) + ' lines.');
    except
      on E : Exception do begin
        logMsg('Error: Cannot read mapfile: '+E.Message+'('+E.ClassName+')');
        FreeAndNil(mapStrings);
        FPONotifyObservers(self, ooChange, nil);
        exit;
      end;
    end;
  finally
    FreeAndNil(mapStream); // close the map file immediately after reading
  end;

  {$IFDEF VerboseProfiler}
  logMsg(Format('[TSimpleParser.parse] File load Duration: %d ms', [DateTimeToMilliseconds(Now() - lTimeStart)]));
  lTimeStart := Now();
  {$ENDIF}

  // reserve space in the map. Less entries will be actually needed, due
  // to comment lines and the status line being counted.
  SetLength(newMap, mapStrings.Count);
  // initialize many things to their initial values
  newRescueStatus := emptyRescueStatus;
  newComments := TStringList.Create;
  newVersion:='';
  lineIdx := 0;
  mapEntry := 0;
  prevHadFile := Length(FMap) > 0;
  statuslineFound := false;

  // parse the mapfile lines into map array, rescueStatus and Comments.
  try
    while lineIdx < mapStrings.Count do begin
      line:=Trim(mapStrings[lineIdx]);
      if isCommentLine(line) then begin
        if not statuslineFound then begin //process comments only until the status line is found
          // copy comment line into FComments (but not the #currentpos one...)
          if pos('# current_pos', line) = 0 then newComments.Add(line);
          if pos('Command line:', line) > 0 then begin // try to find ddrescue's block size argument
             repeat // not a loop, but goto replacement
               idx := pos(' -b', line);
               if idx <> 0 then idx := idx+3 // point to start of number
               else begin
                 idx := pos(' --block-size=', line);
                 if idx <> 0 then idx := idx+14 // point to start of number
                 else break; // argument not found, jump over the rest
               end;
               token[0] :=TrimLeft(Copy(line, idx, Length(line)));
               idx := pos(' ', token[0]); //space after argument
               if idx <> 0 then token[0] :=Copy(token[0], 1, idx-1);
               newRescueStatus.suggestedBlockSize:=StrToIntDef(token[0], DEF_BSIZE);
             until true;
          end;
          idx:=pos('ddrescue version ', line);  // get ddrescue version
          if idx > 0 then newVersion := Copy(line, idx+17, 1337);
        end;
      end else if Length(line) > 0 then begin
        // split line into maximum of 3 tokens
        for i:= 0 to 2 do begin
          idx:= Pos(' ', line); //get pos of next delimiter
          if idx = 0 then begin //if no more delimiter...
            token[i] := line; //...the whole remainder of line is ours!
            line := '';
          end else begin   // otherwise our token = line until the pos of delimiter
            token[i] := Copy(line, 1, idx-1);
            line := TrimLeft(Copy(line, idx, Length(line)-idx+1));
          end;
        end;
        if (not statuslineFound) and (token[0] <> '') and (token[1] <> '') then begin
          // found the status line
          newRescueStatus.pos:=StrToInt64Def(token[0], 0);
          newRescueStatus.curOperation:=token[1][1];
          if token[2] <> '' then newRescueStatus.current_pass:=StrToIntDef(token[2], 0);
          newRescueStatus.strCurOperation:=OperationToText(newRescueStatus.curOperation, newRescueStatus.current_pass);
          statuslineFound := true;
        end else if (token[0] <> '') and (token[1] <> '') and (token[2] <> '') then begin
          // standard block line
          newMap[mapEntry].offset:=StrToInt64(token[0]);
          newMap[mapEntry].length:=StrToInt64(token[1]);
          newMap[mapEntry].status:=token[2][1];
          case newMap[mapEntry].status of
            '?' : inc(newRescueStatus.nontried, newMap[mapEntry].length);
            '+' : inc(newRescueStatus.rescued, newMap[mapEntry].length);
            '*' : inc(newRescueStatus.nontrimmed, newMap[mapEntry].length);
            '/' : inc(newRescueStatus.nonscraped, newMap[mapEntry].length);
            '-' : begin
                    inc(newRescueStatus.bad, newMap[mapEntry].length);
                    inc(newRescueStatus.errors);
                  end;
          end;
          inc(mapEntry);
        end else begin // unknown type or formatting of line
          logMsg('Parser: Cannot parse line '+inttostr(lineIdx));
        end;
      end;
      lineIdx+=1;
    end;
    SetLength(newMap, mapEntry); // trim array to actually needed size
  except
    on E : Exception do begin
      logMsg('Error parsing mapfile on line '+IntToStr(lineIdx+1)+': '+
             E.Message+'('+E.ClassName+')');
      if not forceSameDevSize then begin
        // Set some fail-safe values only if Same Device Size parsing is not forced
        newRescueStatus := emptyRescueStatus;
        newRescueStatus.strCurOperation:='PARSER ERROR';
        SetLength(newMap, 1);
        newMap[0].offset:=0;
        newMap[0].length:=DEF_BSIZE*(1 shl 18); // one block of 2^18 bad sectors
        newMap[0].status:='-';
        newRescueStatus.bad:=newMap[0].length;
        newRescueStatus.devicesize:=newMap[0].length;
      end;
    end;
  end;
  FreeAndNil(mapStrings);

  {$IFDEF VerboseProfiler}
  logMsg(Format('[TSimpleParser.parse] Parse Duration: %d ms', [DateTimeToMilliseconds(Now() - lTimeStart)]));
  {$ENDIF}

  // calculate device's size from last block's offset and length
  if Length(newMap) > 0 then begin
    newRescueStatus.devicesize:=newMap[Length(newMap)-1].offset + newMap[Length(newMap)-1].length;
  end else begin
    logMsg('Parser: No blocks in mapfile!');
    newRescueStatus.devicesize := 0;
  end;


  if (newRescueStatus.devicesize <> FRescueStatus.devicesize) and forceSameDevSize then begin
    // Do NOT swap the new data set in, since it has an invalid device size
    // and it's specified that we don't want that. Just deallocate memory we used.
    newComments.Free;
  end else begin
    // swap new data set in
    FRescueStatus := newRescueStatus;
    FMap := newMap;
    FComments.Free;
    FComments := newComments;
    FVersion := newVersion;
    result := true;
  end;

  if prevHadFile then FPONotifyObservers(self, ooChange, nil) // notify observers of the changes
     else FPONotifyObservers(self, ooAddItem, nil); // notify of new file
end;

function TSimpleParser.hasFile(): boolean;
begin
  hasFile:=Length(FFileName) > 0;
end;

end.