Coverage Report

Created: 2025-04-11 06:56

/src/unrar/scantree.cpp
Line
Count
Source (jump to first uncovered line)
1
#include "rar.hpp"
2
3
ScanTree::ScanTree(StringList *FileMasks,RECURSE_MODE Recurse,bool GetLinks,SCAN_DIRS GetDirs)
4
0
{
5
0
  ScanTree::FileMasks=FileMasks;
6
0
  ScanTree::Recurse=Recurse;
7
0
  ScanTree::GetLinks=GetLinks;
8
0
  ScanTree::GetDirs=GetDirs;
9
10
0
  ScanEntireDisk=false;
11
0
  FolderWildcards=false;
12
13
0
  FindStack.push_back(NULL); // We need a single NULL pointer for initial Depth==0.
14
  
15
0
  SetAllMaskDepth=0;
16
0
  Depth=0;
17
0
  Errors=0;
18
0
  Cmd=NULL;
19
0
  ErrDirList=NULL;
20
0
  ErrDirSpecPathLength=NULL;
21
0
}
22
23
24
ScanTree::~ScanTree()
25
0
{
26
0
  for (int I=Depth;I>=0;I--)
27
0
    if (FindStack[I]!=NULL)
28
0
      delete FindStack[I];
29
0
}
30
31
32
SCAN_CODE ScanTree::GetNext(FindData *FD)
33
0
{
34
0
  if (Depth<0)
35
0
    return SCAN_DONE;
36
37
#ifndef SILENT
38
  uint LoopCount=0;
39
#endif
40
41
0
  SCAN_CODE FindCode;
42
0
  while (1)
43
0
  {
44
0
    if (CurMask.empty() && !GetNextMask())
45
0
      return SCAN_DONE;
46
47
#ifndef SILENT
48
    // Let's return some ticks to system or WinRAR can become irresponsible
49
    // while scanning files in command like "winrar a -r arc c:\file.ext".
50
    // Also we reset system sleep timer here.
51
    if ((++LoopCount & 0x3ff)==0)
52
      Wait();
53
#endif
54
55
0
    FindCode=FindProc(FD);
56
0
    if (FindCode==SCAN_ERROR)
57
0
    {
58
0
      Errors++;
59
0
      continue;
60
0
    }
61
0
    if (FindCode==SCAN_NEXT)
62
0
      continue;
63
0
    if (FindCode==SCAN_SUCCESS && FD->IsDir && GetDirs==SCAN_SKIPDIRS)
64
0
      continue;
65
0
    if (FindCode==SCAN_DONE && GetNextMask())
66
0
      continue;
67
0
    if (FilterList.ItemsCount()>0 && FindCode==SCAN_SUCCESS)
68
0
      if (!CommandData::CheckArgs(&FilterList,FD->IsDir,FD->Name,false,MATCH_WILDSUBPATH))
69
0
        continue;
70
0
    break;
71
0
  }
72
0
  return FindCode;
73
0
}
74
75
76
// For masks like dir1\dir2*\*.ext in non-recursive mode.
77
bool ScanTree::ExpandFolderMask()
78
0
{
79
0
  bool WildcardFound=false;
80
0
  uint SlashPos=0;
81
0
  for (uint I=0;I<CurMask.size();I++)
82
0
  {
83
0
    if (CurMask[I]=='?' || CurMask[I]=='*')
84
0
      WildcardFound=true;
85
0
    if (WildcardFound && IsPathDiv(CurMask[I]))
86
0
    {
87
      // First path separator position after folder wildcard mask.
88
      // In case of dir1\dir2*\dir3\name.ext mask it may point not to file
89
      // name, so we cannot use PointToName() here.
90
0
      SlashPos=I; 
91
0
      break;
92
0
    }
93
0
  }
94
95
0
  std::wstring Mask=CurMask.substr(0,SlashPos);
96
97
  // Prepare the list of all folders matching the wildcard mask.
98
0
  ExpandedFolderList.Reset();
99
0
  FindFile Find;
100
0
  Find.SetMask(Mask);
101
0
  FindData FD;
102
0
  while (Find.Next(&FD))
103
0
    if (FD.IsDir)
104
0
    {
105
0
      FD.Name+=CurMask.substr(SlashPos);
106
107
      // Treat dir*\*, dir*\*.* or dir*\ as dir, so empty 'dir' is also matched
108
      // by such mask. Skipping empty dir with dir*\*.* confused some users.
109
0
      std::wstring LastMask=PointToName(FD.Name);
110
0
      if (LastMask==L"*" || LastMask==L"*.*" || LastMask.empty())
111
0
        RemoveNameFromPath(FD.Name);
112
113
0
      ExpandedFolderList.AddString(FD.Name);
114
0
    }
115
0
  if (ExpandedFolderList.ItemsCount()==0)
116
0
    return false;
117
  // Return the first matching folder name now.
118
0
  ExpandedFolderList.GetString(CurMask);
119
0
  return true;
120
0
}
121
122
123
// For masks like dir1\dir2*\file.ext this function sets 'dir1' recursive mask
124
// and '*\dir2*\file.ext' filter. Masks without folder wildcards are
125
// returned as is.
126
bool ScanTree::GetFilteredMask()
127
0
{
128
  // If we have some matching folders left for non-recursive folder wildcard
129
  // mask, we return it here.
130
0
  if (ExpandedFolderList.ItemsCount()>0 && ExpandedFolderList.GetString(CurMask))
131
0
    return true;
132
133
0
  FolderWildcards=false;
134
0
  FilterList.Reset();
135
0
  if (!FileMasks->GetString(CurMask))
136
0
    return false;
137
138
  // Check if folder wildcards present.
139
0
  bool WildcardFound=false;
140
0
  uint FolderWildcardCount=0;
141
0
  uint SlashPos=0;
142
0
  uint StartPos=0;
143
#ifdef _WIN_ALL // Not treat the special NTFS \\?\d: path prefix as a wildcard.
144
  if (CurMask.rfind(L"\\\\?\\",0)==0)
145
    StartPos=4;
146
#endif
147
0
  for (uint I=StartPos;I<CurMask.size();I++)
148
0
  {
149
0
    if (CurMask[I]=='?' || CurMask[I]=='*')
150
0
      WildcardFound=true;
151
0
    if (IsPathDiv(CurMask[I]) || IsDriveDiv(CurMask[I]))
152
0
    {
153
0
      if (WildcardFound)
154
0
      {
155
        // Calculate a number of folder wildcards in current mask.
156
0
        FolderWildcardCount++;
157
0
        WildcardFound=false;
158
0
      }
159
0
      if (FolderWildcardCount==0)
160
0
        SlashPos=I; // Slash position before first folder wildcard mask.
161
0
    }
162
0
  }
163
0
  if (FolderWildcardCount==0)
164
0
    return true;
165
0
  FolderWildcards=true; // Global folder wildcards flag.
166
167
  // If we have only one folder wildcard component and -r is missing or -r-
168
  // is specified, prepare matching folders in non-recursive mode.
169
  // We assume -r for masks like dir1*\dir2*\file*, because it is complicated
170
  // to fast find them using OS file find API call.
171
0
  if ((Recurse==RECURSE_NONE || Recurse==RECURSE_DISABLE) && FolderWildcardCount==1)
172
0
    return ExpandFolderMask();
173
174
  // Convert path\dir*\ to *\dir filter to search for 'dir' in all 'path' subfolders.
175
0
  std::wstring Filter=L"*";
176
0
  AddEndSlash(Filter); // Path separator is OS dependent, so we set it here instead of variable declaration.
177
178
  // SlashPos might point or not point to path separator for masks like 'dir*', '\dir*' or 'd:dir*'
179
0
  std::wstring WildName=IsPathDiv(CurMask[SlashPos]) || IsDriveDiv(CurMask[SlashPos]) ? CurMask.substr(SlashPos+1) : CurMask.substr(SlashPos);
180
0
  Filter+=WildName;
181
182
  // Treat dir*\* or dir*\*.* as dir\, so empty 'dir' is also matched
183
  // by such mask. Skipping empty dir with dir*\*.* confused some users.
184
0
  std::wstring LastMask=PointToName(Filter);
185
0
  if (LastMask==L"*" || LastMask==L"*.*")
186
0
    GetPathWithSep(Filter,Filter);
187
188
0
  FilterList.AddString(Filter);
189
190
0
  bool RelativeDrive=IsDriveDiv(CurMask[SlashPos]);
191
0
  if (RelativeDrive)
192
0
    SlashPos++; // Use "d:" instead of "d" for d:* mask.
193
194
0
  CurMask.erase(SlashPos);
195
196
0
  if (!RelativeDrive) // Keep d: mask as is, not convert to d:\*
197
0
  {
198
    // We need to append "\*" both for -ep1 to work correctly and to
199
    // convert d:\* masks previously truncated to d: back to original form.
200
0
    AddEndSlash(CurMask);
201
0
    CurMask+=MASKALL;
202
0
  }
203
0
  return true;
204
0
}
205
206
207
bool ScanTree::GetNextMask()
208
0
{
209
0
  if (!GetFilteredMask())
210
0
    return false;
211
#ifdef _WIN_ALL
212
  UnixSlashToDos(CurMask,CurMask);
213
#endif
214
215
  // We shall set it before appending the path separator to \\server\share
216
  // UNC mask below, so "rar a -ep1 arc \\server\share" includes paths
217
  // starting from "share\".
218
0
  SpecPathLength=GetNamePos(CurMask);
219
220
  // We prefer to scan entire disk if mask like \\server\share\ or c:\
221
  // is specified even without -r, but not with -r-. Use \\server\share\*.*,
222
  // c:\*.* mask or -r- to scan only the root directory. Note that UNC names
223
  // are possible both in Win32 and Unix, just with proper path separators.
224
0
  if (Recurse!=RECURSE_DISABLE)
225
0
    if (CurMask.size()>2 && CurMask[0]==CPATHDIVIDER && CurMask[1]==CPATHDIVIDER)
226
0
    {
227
0
      auto Slash=CurMask.find(CPATHDIVIDER,2);
228
0
      if (Slash!=std::wstring::npos)
229
0
      {
230
0
        Slash=CurMask.find(CPATHDIVIDER,Slash+1);
231
        // If path separator is mssing or it is the last string character.
232
0
        ScanEntireDisk=Slash==std::wstring::npos || 
233
0
                       Slash!=std::wstring::npos && Slash+1==CurMask.size();
234
235
        // Win32 FindFirstFile fails for \\server\share names without
236
        // the trailing backslash. So we add it here.
237
0
        if (Slash==std::wstring::npos)
238
0
          CurMask+=CPATHDIVIDER;
239
0
      }
240
0
    }
241
0
    else
242
0
      ScanEntireDisk=IsDriveLetter(CurMask) && IsPathDiv(CurMask[2]) && CurMask[3]==0;
243
244
  // Calculate the name position again, because we could modify UNC path above.
245
0
  auto NamePos=GetNamePos(CurMask);
246
0
  std::wstring Name=CurMask.substr(NamePos);
247
0
  if (Name.empty())
248
0
    CurMask+=MASKALL;
249
0
  if (Name==L"." || Name==L"..")
250
0
  {
251
0
    AddEndSlash(CurMask);
252
0
    CurMask+=MASKALL;
253
0
  }
254
0
  Depth=0;
255
256
0
  OrigCurMask=CurMask;
257
258
0
  return true;
259
0
}
260
261
262
SCAN_CODE ScanTree::FindProc(FindData *FD)
263
0
{
264
0
  if (CurMask.empty())
265
0
    return SCAN_NEXT;
266
0
  bool FastFindFile=false;
267
  
268
0
  if (FindStack[Depth]==NULL) // No FindFile object for this depth yet.
269
0
  {
270
0
    bool Wildcards=IsWildcard(CurMask);
271
272
    // If we have a file name without wildcards, we can try to use
273
    // FastFind to optimize speed. For example, in Unix it results in
274
    // stat call instead of opendir/readdir/closedir.
275
0
    bool FindCode=!Wildcards && FindFile::FastFind(CurMask,FD,GetLinks);
276
277
    // Link check is important for NTFS, where links can have "Directory"
278
    // attribute, but we do not want to recurse to them in "get links" mode.
279
0
    bool IsDir=FindCode && FD->IsDir && (!GetLinks || !FD->IsLink);
280
281
    // SearchAll means that we'll use "*" mask for search, so we'll find
282
    // subdirectories and will be able to recurse into them.
283
    // We do not use "*" for directories at any level or for files
284
    // at top level in recursion mode. We always comrpess the entire directory
285
    // if folder wildcard is specified.
286
0
    bool SearchAll=!IsDir && (Depth>0 || Recurse==RECURSE_ALWAYS ||
287
0
                   FolderWildcards && Recurse!=RECURSE_DISABLE || 
288
0
                   Wildcards && Recurse==RECURSE_WILDCARDS || 
289
0
                   ScanEntireDisk && Recurse!=RECURSE_DISABLE);
290
0
    if (Depth==0)
291
0
      SearchAllInRoot=SearchAll;
292
0
    if (SearchAll || Wildcards)
293
0
    {
294
      // Create the new FindFile object for wildcard based search.
295
0
      FindStack[Depth]=new FindFile;
296
297
0
      std::wstring SearchMask=CurMask;
298
0
      if (SearchAll)
299
0
        SetName(SearchMask,MASKALL);
300
0
      FindStack[Depth]->SetMask(SearchMask);
301
0
    }
302
0
    else
303
0
    {
304
      // Either we failed to fast find or we found a file or we found
305
      // a directory in RECURSE_DISABLE mode, so we do not need to scan it.
306
      // We can return here and do not need to process further.
307
      // We need to process further only if we fast found a directory.
308
0
      if (!FindCode || !IsDir || Recurse==RECURSE_DISABLE)
309
0
      {
310
         // Return SCAN_SUCCESS if we found a file.
311
0
        SCAN_CODE RetCode=SCAN_SUCCESS;
312
313
0
        if (!FindCode)
314
0
        {
315
          // Return SCAN_ERROR if problem is more serious than just
316
          // "file not found".
317
0
          RetCode=FD->Error ? SCAN_ERROR:SCAN_NEXT;
318
319
          // If we failed to find an object, but our current mask is excluded,
320
          // we skip this object and avoid indicating an error.
321
0
          if (Cmd!=NULL && Cmd->ExclCheck(CurMask,false,true,true))
322
0
            RetCode=SCAN_NEXT;
323
0
          else
324
0
          {
325
0
            ErrHandler.OpenErrorMsg(ErrArcName,CurMask);
326
            // User asked to return RARX_NOFILES and not RARX_OPEN here.
327
0
            ErrHandler.SetErrorCode(RARX_NOFILES);
328
0
          }
329
0
        }
330
331
        // If we searched only for one file or directory in "fast find" 
332
        // (without a wildcard) mode, let's set masks to zero, 
333
        // so calling function will know that current mask is used 
334
        // and next one must be read from mask list for next call.
335
        // It is not necessary for directories, because even in "fast find"
336
        // mode, directory recursing will quit by (Depth < 0) condition,
337
        // which returns SCAN_DONE to calling function.
338
0
        CurMask.clear();
339
340
0
        return RetCode;
341
0
      }
342
343
      // We found a directory using only FindFile::FastFind function.
344
0
      FastFindFile=true;
345
0
    }
346
0
  }
347
348
0
  if (!FastFindFile && !FindStack[Depth]->Next(FD,GetLinks))
349
0
  {
350
    // We cannot find anything more in directory either because of
351
    // some error or just as result of all directory entries already read.
352
353
0
    bool Error=FD->Error;
354
0
    if (Error)
355
0
      ScanError(Error);
356
357
    // Going to at least one directory level higher.
358
0
    delete FindStack[Depth];
359
0
    FindStack[Depth--]=NULL;
360
0
    while (Depth>=0 && FindStack[Depth]==NULL)
361
0
      Depth--;
362
0
    if (Depth < 0)
363
0
    {
364
      // Directories scanned both in normal and FastFindFile mode,
365
      // finally exit from scan here, by (Depth < 0) condition.
366
367
0
      if (Error)
368
0
        Errors++;
369
0
      return SCAN_DONE;
370
0
    }
371
372
0
    auto Slash=CurMask.rfind(CPATHDIVIDER);
373
0
    if (Slash!=std::wstring::npos)
374
0
    {
375
0
      std::wstring Mask;
376
0
      Mask=CurMask.substr(Slash); // Name mask with leading slash like \*.*
377
0
      if (Depth<SetAllMaskDepth)
378
0
        Mask.replace(1, std::wstring::npos, PointToName(OrigCurMask));
379
0
      CurMask.erase(Slash);
380
381
0
      std::wstring DirName=CurMask;
382
383
0
      auto PrevSlash=CurMask.rfind(CPATHDIVIDER);
384
0
      if (PrevSlash==std::wstring::npos)
385
0
        CurMask=Mask.substr(1); // Set to name only without leading slash.
386
0
      else
387
0
      {
388
0
        CurMask.erase(PrevSlash); // Remove one of two sequential slashes.
389
0
        CurMask+=Mask;
390
0
      }
391
392
0
      if (GetDirs==SCAN_GETDIRSTWICE &&
393
0
          FindFile::FastFind(DirName,FD,GetLinks) && FD->IsDir)
394
0
      {
395
0
        FD->Flags|=FDDF_SECONDDIR;
396
0
        return Error ? SCAN_ERROR:SCAN_SUCCESS;
397
0
      }
398
0
    }
399
0
    return Error ? SCAN_ERROR:SCAN_NEXT;
400
0
  }
401
402
  // Link check is required for NTFS links, not for Unix.
403
0
  if (FD->IsDir && (!GetLinks || !FD->IsLink))
404
0
  {
405
    // If we found the directory in top (Depth==0) directory
406
    // and if we are not in "fast find" (directory name only as argument)
407
    // or in recurse (SearchAll was set when opening the top directory) mode,
408
    // we do not recurse into this directory. We either return it by itself
409
    // or skip it.
410
0
    if (!FastFindFile && Depth==0 && !SearchAllInRoot)
411
0
      return GetDirs==SCAN_GETCURDIRS ? SCAN_SUCCESS:SCAN_NEXT;
412
413
    // Let's check if directory name is excluded, so we do not waste
414
    // time searching in directory, which will be excluded anyway.
415
0
    if (Cmd!=NULL && (Cmd->ExclCheck(FD->Name,true,false,false) ||
416
0
        Cmd->ExclDirByAttr(FD->FileAttr)))
417
0
    {
418
      // If we are here in "fast find" mode, it means that entire directory
419
      // specified in command line is excluded. Then we need to return
420
      // SCAN_DONE to go to next mask and avoid the infinite loop
421
      // in GetNext() function. Such loop would be possible in case of
422
      // SCAN_NEXT code and "rar a arc dir -xdir" command.
423
424
0
      return FastFindFile ? SCAN_DONE:SCAN_NEXT;
425
0
    }
426
    
427
0
    std::wstring Mask=FastFindFile ? MASKALL:PointToName(CurMask);
428
0
    CurMask=FD->Name;
429
430
0
    if (CurMask.size()+Mask.size()+1>=MAXPATHSIZE || Depth>=MAXSCANDEPTH-1)
431
0
    {
432
0
      uiMsg(UIERROR_PATHTOOLONG,CurMask,SPATHDIVIDER,Mask);
433
0
      return SCAN_ERROR;
434
0
    }
435
436
0
    AddEndSlash(CurMask);
437
0
    CurMask+=Mask;
438
439
0
    Depth++;
440
441
0
    FindStack.resize(Depth+1);
442
443
    // We need to use OrigCurMask for depths less than SetAllMaskDepth
444
    // and "*" for depths equal or larger than SetAllMaskDepth.
445
    // It is important when "fast finding" directories at Depth > 0.
446
    // For example, if current directory is RootFolder and we compress
447
    // the following directories structure:
448
    //   RootFolder
449
    //     +--Folder1
450
    //     |  +--Folder2
451
    //     |  +--Folder3
452
    //     +--Folder4
453
    // with 'rar a -r arcname Folder2' command, rar could add not only
454
    // Folder1\Folder2 contents, but also Folder1\Folder3 if we were using
455
    // "*" mask at all levels. We need to use "*" mask inside of Folder2,
456
    // but return to "Folder2" mask when completing scanning Folder2.
457
    // We can rewrite SearchAll expression above to avoid fast finding
458
    // directories at Depth > 0, but then 'rar a -r arcname Folder2'
459
    // will add the empty Folder2 and do not add its contents.
460
461
0
    if (FastFindFile)
462
0
      SetAllMaskDepth=Depth;
463
0
  }
464
0
  if (!FastFindFile && !CmpName(CurMask,FD->Name,MATCH_NAMES))
465
0
    return SCAN_NEXT;
466
467
0
  return SCAN_SUCCESS;
468
0
}
469
470
471
void ScanTree::ScanError(bool &Error)
472
0
{
473
#ifdef _WIN_ALL
474
  if (Error)
475
  {
476
    // Get attributes of parent folder and do not display an error
477
    // if it is reparse point. We cannot scan contents of standard
478
    // Windows reparse points like "C:\Documents and Settings"
479
    // and we do not want to issue numerous useless errors for them.
480
    // We cannot just check FD->FileAttr here, it can be undefined
481
    // if we process "folder\*" mask or if we process "folder" mask,
482
    // but "folder" is inaccessible.
483
    auto Slash=GetNamePos(CurMask);
484
    if (Slash>1)
485
    {
486
      std::wstring Parent=CurMask.substr(0,Slash-1);
487
      DWORD Attr=GetFileAttr(Parent);
488
      if (Attr!=0xffffffff && (Attr & FILE_ATTRIBUTE_REPARSE_POINT)!=0)
489
        Error=false;
490
    }
491
492
    // Do not display an error if we cannot scan contents of
493
    // "System Volume Information" folder. Normally it is not accessible.
494
    if (CurMask.find(L"System Volume Information\\")!=std::wstring::npos)
495
      Error=false;
496
  }
497
#endif
498
499
0
  if (Error && Cmd!=NULL && Cmd->ExclCheck(CurMask,false,true,true))
500
0
    Error=false;
501
502
0
  if (Error)
503
0
  {
504
0
    if (ErrDirList!=NULL)
505
0
      ErrDirList->AddString(CurMask);
506
0
    if (ErrDirSpecPathLength!=NULL)
507
0
      ErrDirSpecPathLength->push_back((uint)SpecPathLength);
508
0
    std::wstring FullName;
509
    // This conversion works for wildcard masks too.
510
0
    ConvertNameToFull(CurMask,FullName);
511
0
    uiMsg(UIERROR_DIRSCAN,FullName);
512
0
    ErrHandler.SysErrMsg();
513
0
  }
514
0
}