• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xoofx / zio / 10196167465

01 Aug 2024 09:54AM UTC coverage: 90.399% (-0.03%) from 90.425%
10196167465

push

github

web-flow
Merge pull request #96 from Facepunch/fix/path-without-volume

Fix rooted path without volume label regression on Windows

2096 of 2505 branches covered (83.67%)

Branch coverage included in aggregate %.

10 of 11 new or added lines in 2 files covered. (90.91%)

1 existing line in 1 file now uncovered.

5634 of 6046 relevant lines covered (93.19%)

33283.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.76
/src/Zio/FileSystems/PhysicalFileSystem.cs
1
// Copyright (c) Alexandre Mutel. All rights reserved.
2
// This file is licensed under the BSD-Clause 2 license. 
3
// See the license.txt file in the project root for more information.
4

5
using System.Diagnostics;
6
using System.IO;
7
using System.Linq;
8
using System.Runtime.InteropServices;
9

10
using static Zio.FileSystemExceptionHelper;
11
#if NETSTANDARD2_1
12
using System.IO.Enumeration;
13
#endif
14

15
namespace Zio.FileSystems;
16

17
/// <summary>
18
/// Provides a <see cref="IFileSystem"/> for the physical filesystem.
19
/// </summary>
20
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")]
21
public class PhysicalFileSystem : FileSystem
22
{
23
    private const string DrivePrefixOnWindows = "/mnt/";
24
    private static readonly UPath PathDrivePrefixOnWindows = new UPath(DrivePrefixOnWindows);
1✔
25
#if NETSTANDARD
26
    internal static readonly bool IsOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
1✔
27
#else
28
    internal static readonly bool IsOnWindows = CheckIsOnWindows();
1✔
29

30
    private static bool CheckIsOnWindows()
31
    {
32
        switch (Environment.OSVersion.Platform)
1!
33
        {
34
            case PlatformID.Xbox:
35
            case PlatformID.Win32NT:
36
            case PlatformID.Win32S:
37
            case PlatformID.Win32Windows:
38
            case PlatformID.WinCE:
39
                return true;
1✔
40
        }
41
        return false;
×
42
    }
43
#endif
44

45
    // ----------------------------------------------
46
    // Directory API
47
    // ----------------------------------------------
48

49
    /// <inheritdoc />
50
    protected override void CreateDirectoryImpl(UPath path)
51
    {
52
        if (IsWithinSpecialDirectory(path))
1,995✔
53
        {
54
            throw new UnauthorizedAccessException($"Cannot create a directory in the path `{path}`");
4✔
55
        }
56

57
        Directory.CreateDirectory(ConvertPathToInternal(path));
1,991✔
58
    }
1,990✔
59

60
    /// <inheritdoc />
61
    protected override bool DirectoryExistsImpl(UPath path)
62
    {
63
        return IsWithinSpecialDirectory(path) ? SpecialDirectoryExists(path) : Directory.Exists(ConvertPathToInternal(path));
2,521✔
64
    }
65

66
    /// <inheritdoc />
67
    protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath)
68
    {
69
        if (IsOnWindows)
211✔
70
        {
71
            if (IsWithinSpecialDirectory(srcPath))
211✔
72
            {
73
                if (!SpecialDirectoryExists(srcPath))
2✔
74
                {
75
                    throw NewDirectoryNotFoundException(srcPath);
1✔
76
                }
77

78
                throw new UnauthorizedAccessException($"Cannot move the special directory `{srcPath}`");
1✔
79
            }
80

81
            if (IsWithinSpecialDirectory(destPath))
209✔
82
            {
83
                if (!SpecialDirectoryExists(destPath))
2✔
84
                {
85
                    throw NewDirectoryNotFoundException(destPath);
1✔
86
                }
87
                throw new UnauthorizedAccessException($"Cannot move to the special directory `{destPath}`");
1✔
88
            }
89
        }
90

91
        var systemSrcPath = ConvertPathToInternal(srcPath);
207✔
92
        var systemDestPath = ConvertPathToInternal(destPath);
207✔
93

94
        // If the souce path is a file
95
        var fileInfo = new FileInfo(systemSrcPath);
207✔
96
        if (fileInfo.Exists)
207✔
97
        {
98
            throw new IOException($"The source `{srcPath}` is not a directory");
1✔
99
        }
100

101
        Directory.Move(systemSrcPath, systemDestPath);
206✔
102
    }
203✔
103

104
    /// <inheritdoc />
105
    protected override void DeleteDirectoryImpl(UPath path, bool isRecursive)
106
    {
107
        if (IsWithinSpecialDirectory(path))
217✔
108
        {
109
            if (!SpecialDirectoryExists(path))
3✔
110
            {
111
                throw NewDirectoryNotFoundException(path);
2✔
112
            }
113
            throw new UnauthorizedAccessException($"Cannot delete directory `{path}`");
1✔
114
        }
115

116
        Directory.Delete(ConvertPathToInternal(path), isRecursive);
214✔
117
    }
208✔
118

119
    // ----------------------------------------------
120
    // File API
121
    // ----------------------------------------------
122

123
    /// <inheritdoc />
124
    protected override void CopyFileImpl(UPath srcPath, UPath destPath, bool overwrite)
125
    {
126
        if (IsWithinSpecialDirectory(srcPath))
227✔
127
        {
128
            throw new UnauthorizedAccessException($"The access to `{srcPath}` is denied");
1✔
129
        }
130
        if (IsWithinSpecialDirectory(destPath))
226✔
131
        {
132
            throw new UnauthorizedAccessException($"The access to `{destPath}` is denied");
1✔
133
        }
134

135
        File.Copy(ConvertPathToInternal(srcPath), ConvertPathToInternal(destPath), overwrite);
225✔
136
    }
213✔
137

138
    /// <inheritdoc />
139
    protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath destBackupPath, bool ignoreMetadataErrors)
140
    {
141
        if (IsWithinSpecialDirectory(srcPath))
6!
142
        {
143
            throw new UnauthorizedAccessException($"The access to `{srcPath}` is denied");
×
144
        }
145
        if (IsWithinSpecialDirectory(destPath))
6!
146
        {
147
            throw new UnauthorizedAccessException($"The access to `{destPath}` is denied");
×
148
        }
149
        if (!destBackupPath.IsNull && IsWithinSpecialDirectory(destBackupPath))
6✔
150
        {
151
            throw new UnauthorizedAccessException($"The access to `{destBackupPath}` is denied");
1✔
152
        }
153

154
        if (!destBackupPath.IsNull)
5✔
155
        {
156
            CopyFileImpl(destPath, destBackupPath, true);
4✔
157
        }
158
        CopyFileImpl(srcPath, destPath, true);
5✔
159
        DeleteFileImpl(srcPath);
4✔
160

161
        // TODO: Add atomic version using File.Replace coming with .NET Standard 2.0
162
    }
4✔
163

164
    /// <inheritdoc />
165
    protected override long GetFileLengthImpl(UPath path)
166
    {
167
        if (IsWithinSpecialDirectory(path))
23✔
168
        {
169
            throw new UnauthorizedAccessException($"The access to `{path}` is denied");
1✔
170
        }
171
        return new FileInfo(ConvertPathToInternal(path)).Length;
22✔
172
    }
173

174
    /// <inheritdoc />
175
    protected override bool FileExistsImpl(UPath path)
176
    {
177
        return !IsWithinSpecialDirectory(path) && File.Exists(ConvertPathToInternal(path));
282✔
178
    }
179

180
    /// <inheritdoc />
181
    protected override void MoveFileImpl(UPath srcPath, UPath destPath)
182
    {
183
        if (IsWithinSpecialDirectory(srcPath))
409✔
184
        {
185
            throw new UnauthorizedAccessException($"The access to `{srcPath}` is denied");
1✔
186
        }
187
        if (IsWithinSpecialDirectory(destPath))
408✔
188
        {
189
            throw new UnauthorizedAccessException($"The access to `{destPath}` is denied");
1✔
190
        }
191
        File.Move(ConvertPathToInternal(srcPath), ConvertPathToInternal(destPath));
407✔
192
    }
403✔
193

194
    /// <inheritdoc />
195
    protected override void DeleteFileImpl(UPath path)
196
    {
197
        if (IsWithinSpecialDirectory(path))
416✔
198
        {
199
            throw new UnauthorizedAccessException($"The access to `{path}` is denied");
1✔
200
        }
201
        File.Delete(ConvertPathToInternal(path));
415✔
202
    }
412✔
203

204
    /// <inheritdoc />
205
    protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess access,
206
        FileShare share = FileShare.None)
207
    {
208
        if (IsWithinSpecialDirectory(path))
3,816✔
209
        {
210
            throw new UnauthorizedAccessException($"The access to `{path}` is denied");
1✔
211
        }
212
        return File.Open(ConvertPathToInternal(path), mode, access, share);
3,815✔
213
    }
214

215
    /// <inheritdoc />
216
    protected override FileAttributes GetAttributesImpl(UPath path)
217
    {
218
        // Handle special folders to return valid FileAttributes
219
        if (IsWithinSpecialDirectory(path))
20✔
220
        {
221
            if (!SpecialDirectoryExists(path))
4✔
222
            {
223
                throw NewDirectoryNotFoundException(path);
1✔
224
            }
225

226
            // The path / and /drive are readonly
227
            if (path == PathDrivePrefixOnWindows || path == UPath.Root)
3✔
228
            {
229
                return FileAttributes.Directory | FileAttributes.System | FileAttributes.ReadOnly;
2✔
230
            }
231
            // Otherwise let the File.GetAttributes returns the proper attributes for root drive (e.g /drive/c)
232
        }
233

234
        return File.GetAttributes(ConvertPathToInternal(path));
17✔
235
    }
236

237
    // ----------------------------------------------
238
    // Metadata API
239
    // ----------------------------------------------
240

241
    /// <inheritdoc />
242
    protected override void SetAttributesImpl(UPath path, FileAttributes attributes)
243
    {
244
        // Handle special folders
245
        if (IsWithinSpecialDirectory(path))
12✔
246
        {
247
            if (!SpecialDirectoryExists(path))
2✔
248
            {
249
                throw NewDirectoryNotFoundException(path);
1✔
250
            }
251
            throw new UnauthorizedAccessException($"Cannot set attributes on system directory `{path}`");
1✔
252
        }
253

254
        File.SetAttributes(ConvertPathToInternal(path), attributes);
10✔
255
    }
10✔
256

257
    /// <inheritdoc />
258
    protected override DateTime GetCreationTimeImpl(UPath path)
259
    {
260
        // Handle special folders
261

262
        if (IsWithinSpecialDirectory(path))
15✔
263
        {
264
            if (!SpecialDirectoryExists(path))
4✔
265
            {
266
                throw NewDirectoryNotFoundException(path);
1✔
267
            }
268

269
            // For /drive and /, get the oldest CreationTime of all folders (approx)
270
            if (path == PathDrivePrefixOnWindows || path == UPath.Root)
3✔
271
            {
272
                var creationTime = DateTime.MaxValue;
2✔
273

274
                foreach (var drive in DriveInfo.GetDrives())
16✔
275
                {
276
                    if (!drive.IsReady)
6✔
277
                        continue;
278

279
                    var newCreationTime = drive.RootDirectory.CreationTime;
4✔
280
                    if (newCreationTime < creationTime)
4✔
281
                    {
282
                        creationTime = newCreationTime;
2✔
283
                    }
284
                }
285
                return creationTime;
2✔
286
            }
287
        }
288

289
        return File.GetCreationTime(ConvertPathToInternal(path));
12✔
290
    }
291

292
    /// <inheritdoc />
293
    protected override void SetCreationTimeImpl(UPath path, DateTime time)
294
    {
295
        // Handle special folders
296
        if (IsWithinSpecialDirectory(path))
6✔
297
        {
298
            if (!SpecialDirectoryExists(path))
3✔
299
            {
300
                throw NewDirectoryNotFoundException(path);
1✔
301
            }
302
            throw new UnauthorizedAccessException($"Cannot set creation time on system directory `{path}`");
2✔
303
        }
304

305
        var internalPath = ConvertPathToInternal(path);
3✔
306
        var attributes = File.GetAttributes(internalPath);
3✔
307

308
        if ((attributes & FileAttributes.Directory) == FileAttributes.Directory)
3✔
309
        {
310
            Directory.SetCreationTime(internalPath, time);
1✔
311
        }
312
        else
313
        {
314
            File.SetCreationTime(internalPath, time);
2✔
315
        }
316
    }
2✔
317

318
    /// <inheritdoc />
319
    protected override DateTime GetLastAccessTimeImpl(UPath path)
320
    {
321
        // Handle special folders to return valid LastAccessTime
322
        if (IsWithinSpecialDirectory(path))
13✔
323
        {
324
            if (!SpecialDirectoryExists(path))
4✔
325
            {
326
                throw NewDirectoryNotFoundException(path);
1✔
327
            }
328

329
            // For /drive and /, get the oldest CreationTime of all folders (approx)
330
            if (path == PathDrivePrefixOnWindows || path == UPath.Root)
3✔
331
            {
332
                var lastAccessTime = DateTime.MaxValue;
2✔
333

334
                foreach (var drive in DriveInfo.GetDrives())
16✔
335
                {
336
                    if (!drive.IsReady)
6✔
337
                        continue;
338

339
                    var time = drive.RootDirectory.LastAccessTime;
4✔
340
                    if (time < lastAccessTime)
4✔
341
                    {
342
                        lastAccessTime = time;
2✔
343
                    }
344
                }
345
                return lastAccessTime;
2✔
346
            }
347

348
            // otherwise let the regular function running
349
        }
350

351
        return File.GetLastAccessTime(ConvertPathToInternal(path));
10✔
352
    }
353

354
    /// <inheritdoc />
355
    protected override void SetLastAccessTimeImpl(UPath path, DateTime time)
356
    {
357
        // Handle special folders
358
        if (IsWithinSpecialDirectory(path))
6✔
359
        {
360
            if (!SpecialDirectoryExists(path))
3✔
361
            {
362
                throw NewDirectoryNotFoundException(path);
1✔
363
            }
364
            throw new UnauthorizedAccessException($"Cannot set last access time on system directory `{path}`");
2✔
365
        }
366

367
        var internalPath = ConvertPathToInternal(path);
3✔
368
        var attributes = File.GetAttributes(internalPath);
3✔
369

370
        if ((attributes & FileAttributes.Directory) == FileAttributes.Directory)
3✔
371
        {
372
            Directory.SetLastAccessTime(internalPath, time);
1✔
373
        }
374
        else
375
        {
376
            File.SetLastAccessTime(internalPath, time);
2✔
377
        }
378
    }
2✔
379

380
    /// <inheritdoc />
381
    protected override DateTime GetLastWriteTimeImpl(UPath path)
382
    {
383
        // Handle special folders to return valid LastAccessTime
384
        if (IsWithinSpecialDirectory(path))
15✔
385
        {
386
            if (!SpecialDirectoryExists(path))
4✔
387
            {
388
                throw NewDirectoryNotFoundException(path);
1✔
389
            }
390

391
            // For /drive and /, get the oldest CreationTime of all folders (approx)
392
            if (path == PathDrivePrefixOnWindows || path == UPath.Root)
3✔
393
            {
394
                var lastWriteTime = DateTime.MaxValue;
2✔
395

396
                foreach (var drive in DriveInfo.GetDrives())
16✔
397
                {
398
                    if (!drive.IsReady)
6✔
399
                        continue;
400

401
                    var time = drive.RootDirectory.LastWriteTime;
4✔
402
                    if (time < lastWriteTime)
4✔
403
                    {
404
                        lastWriteTime = time;
2✔
405
                    }
406
                }
407
                return lastWriteTime;
2✔
408
            }
409

410
            // otherwise let the regular function running
411
        }
412

413
        return File.GetLastWriteTime(ConvertPathToInternal(path));
12✔
414
    }
415

416
    /// <inheritdoc />
417
    protected override void SetLastWriteTimeImpl(UPath path, DateTime time)
418
    {
419
        // Handle special folders
420
        if (IsWithinSpecialDirectory(path))
7✔
421
        {
422
            if (!SpecialDirectoryExists(path))
3✔
423
            {
424
                throw NewDirectoryNotFoundException(path);
1✔
425
            }
426
            throw new UnauthorizedAccessException($"Cannot set last write time on system directory `{path}`");
2✔
427
        }
428

429
        var internalPath = ConvertPathToInternal(path);
4✔
430
        var attributes = File.GetAttributes(internalPath);
4✔
431

432
        if ((attributes & FileAttributes.Directory) == FileAttributes.Directory)
4✔
433
        {
434
            Directory.SetLastWriteTime(internalPath, time);
1✔
435
        }
436
        else
437
        {
438
            File.SetLastWriteTime(internalPath, time);
3✔
439
        }
440
    }
3✔
441

442
    protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget)
443
    {
444
        if (IsWithinSpecialDirectory(path))
4!
445
        {
446
            throw new UnauthorizedAccessException($"The access to `{path}` is denied");
×
447
        }
448

449
        if (IsWithinSpecialDirectory(pathToTarget))
4!
450
        {
451
            throw new UnauthorizedAccessException($"The access to `{pathToTarget}` is denied");
×
452
        }
453

454
        var systemPath = ConvertPathToInternal(path);
4✔
455

456
        if (File.Exists(systemPath))
4!
457
        {
458
            throw NewDestinationFileExistException(path);
×
459
        }
460

461
        if (Directory.Exists(systemPath))
4!
462
        {
463
            throw NewDestinationDirectoryExistException(path);
×
464
        }
465

466
        var systemPathToTarget = ConvertPathToInternal(pathToTarget);
4✔
467

468
        bool isDirectory;
469

470
        if (File.Exists(systemPathToTarget))
4✔
471
        {
472
            isDirectory = false;
1✔
473
        }
474
        else if (Directory.Exists(systemPathToTarget))
3!
475
        {
476
            isDirectory = true;
3✔
477
        }
478
        else
479
        {
480
            throw NewDirectoryNotFoundException(path);
×
481
        }
482

483
#if NET7_0_OR_GREATER
484
        if (isDirectory)
4✔
485
        {
486
            Directory.CreateSymbolicLink(systemPath, systemPathToTarget);
3✔
487
        }
488
        else
489
        {
490
            File.CreateSymbolicLink(systemPath, systemPathToTarget);
1✔
491
        }
492
#else
493
        bool success;
494

495
        if (IsOnWindows)
4!
496
        {
497
            var type = isDirectory ? Interop.Windows.SymbolicLink.Directory : Interop.Windows.SymbolicLink.File;
4✔
498

499
            success = Interop.Windows.CreateSymbolicLink(systemPath, systemPathToTarget, type);
4✔
500

501
            if (!success && Marshal.GetLastWin32Error() == 1314)
4!
502
            {
503
                throw new UnauthorizedAccessException($"Could not create symbolic link `{path}` to `{pathToTarget}` due to insufficient privileges");
×
504
            }
505
        }
506
        else
507
        {
508
            success = Interop.Unix.symlink(systemPathToTarget, systemPath) == 0;
×
509
        }
510

511
        if (!success)
4!
512
        {
513
            throw new IOException($"Could not create symbolic link `{path}` to `{pathToTarget}`");
×
514
        }
515
#endif
516
    }
4✔
517

518
    /// <inheritdoc />
519
    protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath)
520
    {
521
        if (IsWithinSpecialDirectory(linkPath))
4!
522
        {
523
            throw new UnauthorizedAccessException($"The access to `{linkPath}` is denied");
×
524
        }
525

526
        var systemPath = ConvertPathToInternal(linkPath);
4✔
527
        bool isDirectory;
528

529
        if (File.Exists(systemPath))
4✔
530
        {
531
            isDirectory = false;
1✔
532
        }
533
        else if (Directory.Exists(systemPath))
3!
534
        {
535
            isDirectory = true;
3✔
536
        }
537
        else
538
        {
539
            resolvedPath = default;
×
540
            return false;
×
541
        }
542

543
#if NET7_0_OR_GREATER
544
        var systemResult = isDirectory ? Directory.ResolveLinkTarget(systemPath, true)?.FullName : File.ResolveLinkTarget(systemPath, true)?.FullName;
4!
545
#else
546
        var systemResult = IsOnWindows ? Interop.Windows.GetFinalPathName(systemPath) : Interop.Unix.readlink(systemPath);
4!
547
#endif
548

549
        if (systemResult == null)
4!
550
        {
551
            resolvedPath = default;
×
552
            return false;
×
553
        }
554

555
        resolvedPath = ConvertPathFromInternal(systemResult);
4✔
556
        return true;
4✔
557
    }
558

559
    // ----------------------------------------------
560
    // Search API
561
    // ----------------------------------------------
562

563
    /// <inheritdoc />
564
    protected override IEnumerable<UPath> EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
565
    {
566
        // Special case for Windows as we need to provide list for:
567
        // - the root folder / (which should just return the /drive folder)
568
        // - the drive folders /drive/c, drive/e...etc.
569
        var search = SearchPattern.Parse(ref path, ref searchPattern);
4,861✔
570
        if (IsOnWindows)
4,861✔
571
        {
572
            if (IsWithinSpecialDirectory(path))
4,861✔
573
            {
574
                if (!SpecialDirectoryExists(path))
6!
575
                {
576
                    throw NewDirectoryNotFoundException(path);
×
577
                }
578

579
                var searchForDirectory = searchTarget == SearchTarget.Both || searchTarget == SearchTarget.Directory;
6✔
580

581
                // Only sub folder "/drive/" on root folder /
582
                if (path == UPath.Root)
6✔
583
                {
584
                    if (searchForDirectory)
3✔
585
                    {
586
                        yield return PathDrivePrefixOnWindows;
2✔
587

588
                        if (searchOption == SearchOption.AllDirectories)
2!
589
                        {
590
                            foreach (var subPath in EnumeratePathsImpl(PathDrivePrefixOnWindows, searchPattern, searchOption, searchTarget))
×
591
                            {
592
                                yield return subPath;
×
593
                            }
594
                        }
595
                    }
596

597
                    yield break;
3✔
598
                }
599

600
                // When listing for /drive, return the list of drives available
601
                if (path == PathDrivePrefixOnWindows)
3✔
602
                {
603
                    var pathDrives = new List<UPath>();
3✔
604
                    foreach (var drive in DriveInfo.GetDrives())
24✔
605
                    {
606
                        if (drive.Name.Length < 2 || drive.Name[1] != ':')
9✔
607
                        {
608
                            continue;
609
                        }
610

611
                        var pathDrive = PathDrivePrefixOnWindows / char.ToLowerInvariant(drive.Name[0]).ToString();
9✔
612

613
                        if (search.Match(pathDrive))
9✔
614
                        {
615
                            pathDrives.Add(pathDrive);
9✔
616

617
                            if (searchForDirectory)
9✔
618
                            {
619
                                yield return pathDrive;
6✔
620
                            }
621
                        }
622
                    }
623

624
                    if (searchOption == SearchOption.AllDirectories)
3!
625
                    {
626
                        foreach (var pathDrive in pathDrives)
×
627
                        {
628
                            foreach (var subPath in EnumeratePathsImpl(pathDrive, searchPattern, searchOption, searchTarget))
×
629
                            {
630
                                yield return subPath;
×
631
                            }
632
                        }
633
                    }
634

635
                    yield break;
3!
636
                }
637
            }
638
        }
639

640
        IEnumerable<string> results;
641
        switch (searchTarget)
642
        {
643
            case SearchTarget.File:
644
                results = Directory.EnumerateFiles(ConvertPathToInternal(path), searchPattern, searchOption);
2,215✔
645
                break;
2,215✔
646

647
            case SearchTarget.Directory:
648
                results = Directory.EnumerateDirectories(ConvertPathToInternal(path), searchPattern, searchOption);
1,330✔
649
                break;
1,330✔
650

651
            case SearchTarget.Both:
652
                results = Directory.EnumerateFileSystemEntries(ConvertPathToInternal(path), searchPattern, searchOption);
1,310✔
653
                break;
1,310✔
654
            
655
            default:
656
                yield break;
×
657
        }
658

659
        foreach (var subPath in results)
91,598✔
660
        {
661
            // Windows will truncate the search pattern's extension to three characters if the filesystem
662
            // has 8.3 paths enabled. This means searching for *.docx will list *.doc as well which is
663
            // not what we want. Check against the search pattern again to filter out those false results.
664
            if (!IsOnWindows || search.Match(Path.GetFileName(subPath)))
40,944✔
665
            {
666
                yield return ConvertPathFromInternal(subPath);
39,634✔
667
            }
668
        }
669
    }
4,855✔
670

671
    /// <inheritdoc />
672
    protected override IEnumerable<FileSystemItem> EnumerateItemsImpl(UPath path, SearchOption searchOption, SearchPredicate? searchPredicate)
673
    {
674
        if (IsOnWindows)
884✔
675
        {
676
            if (IsWithinSpecialDirectory(path))
884!
677
            {
678
                if (!SpecialDirectoryExists(path))
×
679
                {
680
                    throw NewDirectoryNotFoundException(path);
×
681
                }
682

683
                // Only sub folder "/drive/" on root folder /
684
                if (path == UPath.Root)
×
685
                {
686
                    var item = new FileSystemItem(this, PathDrivePrefixOnWindows, true);
×
687
                    if (searchPredicate == null || searchPredicate(ref item))
×
688
                    {
689
                        yield return item;
×
690
                    }
691

692
                    if (searchOption == SearchOption.AllDirectories)
×
693
                    {
694
                        foreach (var subItem in EnumerateItemsImpl(PathDrivePrefixOnWindows, searchOption, searchPredicate))
×
695
                        {
696
                            yield return subItem;
×
697
                        }
698
                    }
699

700
                    yield break;
×
701
                }
702

703
                // When listing for /drive, return the list of drives available
704
                if (path == PathDrivePrefixOnWindows)
×
705
                {
706
                    var pathDrives = new List<UPath>();
×
707
                    foreach (var drive in DriveInfo.GetDrives())
×
708
                    {
709
                        if (drive.Name.Length < 2 || drive.Name[1] != ':')
×
710
                        {
711
                            continue;
712
                        }
713

714
                        var pathDrive = PathDrivePrefixOnWindows / char.ToLowerInvariant(drive.Name[0]).ToString();
×
715

716
                        pathDrives.Add(pathDrive);
×
717

718
                        var item = new FileSystemItem(this, pathDrive, true);
×
719
                        if (searchPredicate == null || searchPredicate(ref item))
×
720
                        {
721
                            yield return item;
×
722
                        }
723
                    }
724

725
                    if (searchOption == SearchOption.AllDirectories)
×
726
                    {
727
                        foreach (var pathDrive in pathDrives)
×
728
                        {
729
                            foreach (var subItem in EnumerateItemsImpl(pathDrive, searchOption, searchPredicate))
×
730
                            {
731
                                yield return subItem;
×
732
                            }
733
                        }
734
                    }
735

736
                    yield break;
×
737
                }
738
            }
739
        }
740
        var pathOnDisk = ConvertPathToInternal(path);
884✔
741
        if (!Directory.Exists(pathOnDisk)) yield break;
884!
742

743
#if NETSTANDARD2_1
744
        var enumerable = new FileSystemEnumerable<FileSystemItem>(pathOnDisk, TransformToFileSystemItem, searchOption == SearchOption.AllDirectories ? CompatibleRecursive : Compatible);
745

746
        foreach (var item in enumerable)
747
        {
748
            var localItem = item;
749
            if (searchPredicate == null || searchPredicate(ref localItem))
750
            {
751
                yield return localItem;
752
            }
753
        }
754
#else
755
        var results = Directory.EnumerateFileSystemEntries(pathOnDisk, "*", searchOption);
884✔
756
        foreach (var subPath in results)
25,636✔
757
        {
758
            var fileInfo = new FileInfo(subPath);
11,934✔
759
            var fullPath = ConvertPathFromInternal(subPath);
11,934✔
760
            var item = new FileSystemItem
11,934✔
761
            {
11,934✔
762
                FileSystem = this,
11,934✔
763
                AbsolutePath = fullPath,
11,934✔
764
                Path = fullPath,
11,934✔
765
                Attributes = fileInfo.Attributes,
11,934✔
766
                CreationTime = fileInfo.CreationTimeUtc.ToLocalTime(),
11,934✔
767
                LastAccessTime = fileInfo.LastAccessTimeUtc.ToLocalTime(),
11,934✔
768
                LastWriteTime = fileInfo.LastWriteTimeUtc.ToLocalTime(),
11,934✔
769
                Length = (fileInfo.Attributes & FileAttributes.Directory) > 0 ? 0 : fileInfo.Length
11,934✔
770
            };
11,934✔
771
            if (searchPredicate == null || searchPredicate(ref item))
11,934!
772
            {
773
                yield return item;
11,934✔
774
            }
775
        }
776
#endif
777
    }
884✔
778

779
#if NETSTANDARD2_1
780

781
    internal static EnumerationOptions Compatible { get; } = new EnumerationOptions()
782
    {
783
        MatchType = MatchType.Win32,
784
        AttributesToSkip = (FileAttributes)0,
785
        IgnoreInaccessible = false
786
    };
787

788
    private static EnumerationOptions CompatibleRecursive { get; } = new EnumerationOptions()
789
    {
790
        RecurseSubdirectories = true,
791
        MatchType = MatchType.Win32,
792
        AttributesToSkip = (FileAttributes)0,
793
        IgnoreInaccessible = false
794
    };
795

796
    private FileSystemItem TransformToFileSystemItem(ref System.IO.Enumeration.FileSystemEntry entry)
797
    {
798
        var fullPath = ConvertPathFromInternal(entry.ToFullPath());
799
        return new FileSystemItem
800
        {
801
            FileSystem = this,
802
            AbsolutePath = fullPath,
803
            Path = fullPath,
804
            Attributes = entry.Attributes,
805
            CreationTime = entry.CreationTimeUtc.ToLocalTime(),
806
            LastAccessTime = entry.LastAccessTimeUtc.ToLocalTime(),
807
            LastWriteTime = entry.LastWriteTimeUtc.ToLocalTime(),
808
            Length = entry.Length
809
        };
810
    }
811
#endif
812

813
    // ----------------------------------------------
814
    // Watch API
815
    // ----------------------------------------------
816

817
    /// <inheritdoc />
818
    protected override bool CanWatchImpl(UPath path)
819
    {
820
        if (IsWithinSpecialDirectory(path))
20!
821
        {
822
            return SpecialDirectoryExists(path);
×
823
        }
824

825
        return Directory.Exists(ConvertPathToInternal(path));
20✔
826
    }
827

828
    /// <inheritdoc />
829
    protected override IFileSystemWatcher WatchImpl(UPath path)
830
    {
831
        if (IsWithinSpecialDirectory(path))
7!
832
        {
833
            throw new UnauthorizedAccessException($"The access to `{path}` is denied");
×
834
        }
835

836
        return new Watcher(this, path);
7✔
837
    }
838

839
    private sealed class Watcher : IFileSystemWatcher
840
    {
841
        private readonly PhysicalFileSystem _fileSystem;
842
        private readonly System.IO.FileSystemWatcher _watcher;
843

844
        public event EventHandler<FileChangedEventArgs>? Changed;
845
        public event EventHandler<FileChangedEventArgs>? Created;
846
        public event EventHandler<FileChangedEventArgs>? Deleted;
847
        public event EventHandler<FileSystemErrorEventArgs>? Error;
848
        public event EventHandler<FileRenamedEventArgs>? Renamed;
849

850
        public IFileSystem FileSystem => _fileSystem;
36✔
851
        public UPath Path { get; }
852

853
        public int InternalBufferSize
854
        {
855
            get => _watcher.InternalBufferSize;
×
856
            set => _watcher.InternalBufferSize = value;
×
857
        }
858

859
        public NotifyFilters NotifyFilter
860
        {
861
            get => (NotifyFilters)_watcher.NotifyFilter;
×
862
            set => _watcher.NotifyFilter = (System.IO.NotifyFilters)value;
×
863
        }
864

865
        public bool EnableRaisingEvents
866
        {
867
            get => _watcher.EnableRaisingEvents;
67✔
868
            set => _watcher.EnableRaisingEvents = value;
7✔
869
        }
870

871
        public string Filter
872
        {
873
            get => _watcher.Filter;
×
874
            set => _watcher.Filter = value;
×
875
        }
876

877
        public bool IncludeSubdirectories
878
        {
879
            get => _watcher.IncludeSubdirectories;
54✔
880
            set => _watcher.IncludeSubdirectories = value;
7✔
881
        }
882

883
        public Watcher(PhysicalFileSystem fileSystem, UPath path)
884
        {
885
            _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
7!
886
            _watcher = new System.IO.FileSystemWatcher(_fileSystem.ConvertPathToInternal(path))
7✔
887
            {
7✔
888
                Filter = "*"
7✔
889
            };
7✔
890
            Path = path;
891

892
            _watcher.Changed += (sender, args) => Changed?.Invoke(this, Remap(args));
16!
893
            _watcher.Created += (sender, args) => Created?.Invoke(this, Remap(args));
14!
894
            _watcher.Deleted += (sender, args) => Deleted?.Invoke(this, Remap(args));
27!
895
            _watcher.Error += (sender, args) => Error?.Invoke(this, Remap(args));
14!
896
            _watcher.Renamed += (sender, args) => Renamed?.Invoke(this, Remap(args));
7!
897
        }
7✔
898

899
        ~Watcher()
900
        {
901
            Dispose(false);
7✔
902
        }
14✔
903

904
        public void Dispose()
905
        {
906
            Dispose(true);
×
907
            GC.SuppressFinalize(this);
×
908
        }
×
909

910
        private void Dispose(bool disposing)
911
        {
912
            if (disposing)
7!
913
            {
914
                _watcher.Dispose();
×
915
            }
916
        }
7✔
917

918
        private FileChangedEventArgs Remap(FileSystemEventArgs args)
919
        {
920
            var newChangeType = (WatcherChangeTypes)args.ChangeType;
36✔
921
            var newPath = _fileSystem.ConvertPathFromInternal(args.FullPath);
36✔
922
            return new FileChangedEventArgs(FileSystem, newChangeType, newPath);
36✔
923
        }
924

925
        private FileSystemErrorEventArgs Remap(ErrorEventArgs args)
926
        {
927
            return new FileSystemErrorEventArgs(args.GetException());
7✔
928
        }
929

930
        private FileRenamedEventArgs Remap(RenamedEventArgs args)
931
        {
932
            var newChangeType = (WatcherChangeTypes)args.ChangeType;
×
933
            var newPath = _fileSystem.ConvertPathFromInternal(args.FullPath);
×
934
            var newOldPath = _fileSystem.ConvertPathFromInternal(args.OldFullPath);
×
935
            return new FileRenamedEventArgs(FileSystem, newChangeType, newPath, newOldPath);
×
936
        }
937
    }
938

939
    // ----------------------------------------------
940
    // Path API
941
    // ----------------------------------------------
942

943
    /// <inheritdoc />
944
    protected override string ConvertPathToInternalImpl(UPath path)
945
    {
946
        var absolutePath = path.FullName;
16,829✔
947

948
        if (IsOnWindows)
16,829!
949
        {
950
            if (!absolutePath.StartsWith(DrivePrefixOnWindows, StringComparison.Ordinal) ||
16,829✔
951
                absolutePath.Length == DrivePrefixOnWindows.Length ||
16,829✔
952
                !IsDriveLetter(absolutePath[DrivePrefixOnWindows.Length]))
16,829✔
953
                throw new ArgumentException($"A path on Windows must start by `{DrivePrefixOnWindows}` followed by the drive letter");
2✔
954

955
            var driveLetter = char.ToUpper(absolutePath[DrivePrefixOnWindows.Length]);
16,827✔
956
            if (absolutePath.Length != DrivePrefixOnWindows.Length + 1 &&
16,827✔
957
                absolutePath[DrivePrefixOnWindows.Length + 1] !=
16,827✔
958
                UPath.DirectorySeparator)
16,827✔
959
                throw new ArgumentException($"The driver letter `/{DrivePrefixOnWindows}{absolutePath[DrivePrefixOnWindows.Length]}` must be followed by a `/` or nothing in the path -> `{absolutePath}`");
1✔
960

961
            var builder = UPath.GetSharedStringBuilder();
16,826✔
962
            builder.Append(driveLetter).Append(":\\");
16,826✔
963
            if (absolutePath.Length > DrivePrefixOnWindows.Length + 1)
16,826✔
964
                builder.Append(absolutePath.Replace(UPath.DirectorySeparator, '\\').Substring(DrivePrefixOnWindows.Length + 2));
16,822✔
965

966
            return builder.ToString();
16,826✔
967
        }
968
        return absolutePath;
×
969
    }
970

971
    /// <inheritdoc />
972
    protected override UPath ConvertPathFromInternalImpl(string innerPath)
973
    {
974
        if (IsOnWindows)
51,872!
975
        {
976
            // We currently don't support special Windows files (\\.\ \??\  DosDevices...etc.)
977
            if (innerPath.StartsWith(@"\\", StringComparison.Ordinal) || innerPath.StartsWith(@"\?", StringComparison.Ordinal))
51,872✔
978
                throw new NotSupportedException($"Path starting with `\\\\` or `\\?` are not supported -> `{innerPath}` ");
1✔
979

980
            // We want to avoid using Path.GetFullPath unless absolutely necessary,
981
            // because it can change the case of already rooted paths that contain a ~
982
            var absolutePath = HasWindowsVolumeLabel(innerPath) ? innerPath : Path.GetFullPath(innerPath);
51,871✔
983

984
            // Assert that Path.GetFullPath returned the format we expect
985
            if (!HasWindowsVolumeLabel(absolutePath))
51,871!
UNCOV
986
                throw new ArgumentException($"Expecting a drive for the path `{absolutePath}`");
×
987

988
            var builder = UPath.GetSharedStringBuilder();
51,871✔
989
            builder.Append(DrivePrefixOnWindows).Append(char.ToLowerInvariant(absolutePath[0])).Append('/');
51,871✔
990
            if (absolutePath.Length > 2)
51,871✔
991
                builder.Append(absolutePath.Substring(2));
51,871✔
992

993
            return new UPath(builder.ToString());
51,871✔
994
        }
995
        return innerPath;
×
996
    }
997

998
    private static bool IsWithinSpecialDirectory(UPath path)
999
    {
1000
        if (!IsOnWindows)
16,855!
1001
        {
1002
            return false;
×
1003
        }
1004

1005
        var parentDirectory = path.GetDirectory();
16,855✔
1006
        return path == PathDrivePrefixOnWindows ||
16,855✔
1007
               path == UPath.Root ||
16,855✔
1008
               parentDirectory == PathDrivePrefixOnWindows ||
16,855✔
1009
               parentDirectory == UPath.Root;
16,855✔
1010
    }
1011

1012
    private static bool SpecialDirectoryExists(UPath path)
1013
    {
1014
        // /drive or / can be read
1015
        if (path == PathDrivePrefixOnWindows || path == UPath.Root)
46✔
1016
        {
1017
            return true;
27✔
1018
        }
1019

1020
        // If /xxx, invalid (parent folder is /)
1021
        var parentDirectory = path.GetDirectory();
19✔
1022
        if (parentDirectory == UPath.Root)
19✔
1023
        {
1024
            return false;
11✔
1025
        }
1026

1027
        var dirName = path.GetName();
8✔
1028
        // Else check that we have a valid drive path (e.g /drive/c)
1029
        return parentDirectory == PathDrivePrefixOnWindows && 
8✔
1030
               dirName.Length == 1 && 
8✔
1031
               DriveInfo.GetDrives().Any(p => char.ToLowerInvariant(p.Name[0]) == dirName[0]);
23✔
1032
    }
1033

1034
    private static bool IsDriveLetter(char c)
1035
    {
1036
        return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
16,827!
1037
    }
1038

1039
    private static bool HasWindowsVolumeLabel( string path )
1040
    {
1041
        if ( !IsOnWindows )
103,742!
NEW
1042
            throw new NotSupportedException( $"{nameof( HasWindowsVolumeLabel )} is only supported on Windows platforms." );
×
1043

1044
        return path.Length >= 3 && path[1] == ':' && path[2] is '\\' or '/';
103,742!
1045
    }
1046
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc