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

xoofx / zio / 9333214075

01 Jun 2024 09:13PM UTC coverage: 90.218% (+0.2%) from 90.058%
9333214075

push

github

web-flow
Merge pull request #88 from GerardSmit/fix/zip-archive

Fixed ZipArchiveFileSystem and project warnings

2075 of 2483 branches covered (83.57%)

Branch coverage included in aggregate %.

162 of 165 new or added lines in 5 files covered. (98.18%)

3 existing lines in 1 file now uncovered.

5525 of 5941 relevant lines covered (93.0%)

32200.24 hits per line

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

91.03
/src/Zio/FileSystems/ZipArchiveFileSystem.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.IO.Compression;
8
using System.Linq;
9
using System.Threading;
10

11
#if HAS_ZIPARCHIVE
12
namespace Zio.FileSystems;
13

14
/// <summary>
15
///     Provides a <see cref="IFileSystem" /> for the ZipArchive filesystem.
16
/// </summary>
17
public class ZipArchiveFileSystem : FileSystem
18
{
19
    private readonly bool _isCaseSensitive;
20
    
21
    private ZipArchive _archive;
22
    private Dictionary<UPath, InternalZipEntry> _entries;
23

24
    private readonly string? _path;
25
    private readonly Stream? _stream;
26
    private readonly bool _disposeStream;
27

28
    private readonly CompressionLevel _compressionLevel;
29

30
    private readonly ReaderWriterLockSlim _entriesLock = new();
46✔
31
    
32
    private FileSystemEventDispatcher<FileSystemWatcher>? _dispatcher;
33
    private readonly object _dispatcherLock = new();
46✔
34

35
    private readonly DateTime _creationTime;
36

37
    private readonly Dictionary<ZipArchiveEntry, EntryState> _openStreams;
38
    private readonly object _openStreamsLock = new();
46✔
39

40
    private const char DirectorySeparator = '/';
41

42
    /// <summary>
43
    ///     Initializes a new instance of the <see cref="ZipArchiveFileSystem" /> class.
44
    /// </summary>
45
    /// <param name="archive">An instance of <see cref="ZipArchive" /></param>
46
    /// <param name="isCaseSensitive">Specifies if entry names should be case sensitive</param>
47
    /// <exception cref="ArgumentNullException"></exception>
48
    public ZipArchiveFileSystem(ZipArchive archive, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression)
46✔
49
    {
50
        _archive = archive;
46✔
51
        _isCaseSensitive = isCaseSensitive;
46✔
52
        _creationTime = DateTime.Now;
46✔
53
        _compressionLevel = compressionLevel;
46✔
54
        if (archive == null)
46!
55
        {
56
            throw new ArgumentNullException(nameof(archive));
×
57
        }
58

59
        _openStreams = new Dictionary<ZipArchiveEntry, EntryState>();
46✔
60
        _entries = null!; // Loaded below
46✔
61
        LoadEntries();
46✔
62
    }
46✔
63

64
    /// <summary>
65
    ///     Initializes a new instance of the <see cref="ZipArchiveFileSystem" /> class.
66
    /// </summary>
67
    /// <param name="stream">Instance of stream to create <see cref="ZipArchive" /> from</param>
68
    /// <param name="mode">Mode of <see cref="ZipArchive" /></param>
69
    /// <param name="leaveOpen">True to leave the stream open when <see cref="ZipArchive" /> is disposed</param>
70
    /// <param name="isCaseSensitive"></param>
71
    public ZipArchiveFileSystem(Stream stream, ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression)
72
        : this(new ZipArchive(stream, mode, leaveOpen: true), isCaseSensitive, compressionLevel)
35✔
73
    {
74
        _disposeStream = !leaveOpen;
35✔
75
        _stream = stream;
35✔
76
    }
35✔
77

78
    /// <summary>
79
    ///     Initializes a new instance of the <see cref="ZipArchiveFileSystem" /> class from file.
80
    /// </summary>
81
    /// <param name="path">Path to zip file</param>
82
    /// <param name="mode">Mode of <see cref="ZipArchive" /></param>
83
    /// <param name="leaveOpen">True to leave the stream open when <see cref="ZipArchive" /> is disposed</param>
84
    /// <param name="isCaseSensitive">Specifies if entry names should be case sensitive</param>
85
    public ZipArchiveFileSystem(string path, ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression)
86
        : this(new ZipArchive(File.Open(path, FileMode.OpenOrCreate), mode, leaveOpen), isCaseSensitive, compressionLevel)
1✔
87
    {
88
        _path = path;
1✔
89
    }
1✔
90

91
    /// <summary>
92
    ///     Initializes a new instance of the <see cref="ZipArchiveFileSystem" /> class with a <see cref="MemoryStream" />
93
    /// </summary>
94
    /// <param name="mode">Mode of <see cref="ZipArchive" /></param>
95
    /// <param name="leaveOpen">True to leave the stream open when <see cref="ZipArchive" /> is disposed</param>
96
    /// <param name="isCaseSensitive">Specifies if entry names should be case sensitive</param>
97
    public ZipArchiveFileSystem(ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression)
98
        : this(new MemoryStream(), mode, leaveOpen, isCaseSensitive, compressionLevel)
25✔
99
    {
100
    }
25✔
101

102
    /// <summary>
103
    /// Saves the archive to the original path or stream.
104
    /// </summary>
105
    /// <exception cref="InvalidOperationException">Cannot save archive without a path or stream</exception>
106
    public void Save()
107
    {
108
        var mode = _archive.Mode;
4✔
109

110
        if (_path != null)
4✔
111
        {
112
            _archive.Dispose();
2✔
113
            _archive = new ZipArchive(File.Open(_path, FileMode.OpenOrCreate), mode);
2✔
114
        }
115
        else if (_stream != null)
2!
116
        {
117
            if (!_stream.CanSeek)
2!
118
            {
NEW
119
                throw new InvalidOperationException("Cannot save archive to a stream that doesn't support seeking");
×
120
            }
121

122
            _archive.Dispose();
2✔
123
            _stream.Seek(0, SeekOrigin.Begin);
2✔
124
            _archive = new ZipArchive(_stream, mode, leaveOpen: true);
2✔
125
        }
126
        else
127
        {
NEW
128
            throw new InvalidOperationException("Cannot save archive without a path or stream");
×
129
        }
130

131
        LoadEntries();
4✔
132
    }
4✔
133

134
    private void LoadEntries()
135
    {
136
        var comparer = _isCaseSensitive ? UPathComparer.Ordinal : UPathComparer.OrdinalIgnoreCase;
50✔
137

138
        _entries = _archive.Entries.ToDictionary(
50✔
139
            e => new UPath(e.FullName).ToAbsolute(),
17✔
140
            static e =>
50✔
141
            {
50✔
142
                var lastChar = e.FullName[e.FullName.Length - 1];
17✔
143
                return new InternalZipEntry(e, lastChar is '/' or '\\');
17✔
144
            },
50✔
145
            comparer);
50✔
146
    }
50✔
147

148
    private ZipArchiveEntry? GetEntry(UPath path, out bool isDirectory)
149
    {
150
        _entriesLock.EnterReadLock();
22,484✔
151
        try
152
        {
153
            if (_entries.TryGetValue(path, out var foundEntry))
22,484✔
154
            {
155
                isDirectory = foundEntry.IsDirectory;
14,304✔
156
                return foundEntry.Entry;
14,304✔
157
            }
158
        }
8,180✔
159
        finally
160
        {
161
            _entriesLock.ExitReadLock();
22,484✔
162
        }
22,484✔
163

164
        isDirectory = false;
8,180✔
165
        return null;
8,180✔
166
    }
14,304✔
167

168
    private ZipArchiveEntry? GetEntry(UPath path) => GetEntry(path, out _);
14,193✔
169

170
    /// <inheritdoc />
171
    protected override UPath ConvertPathFromInternalImpl(string innerPath)
172
    {
173
        return new UPath(innerPath);
3✔
174
    }
175

176
    /// <inheritdoc />
177
    protected override string ConvertPathToInternalImpl(UPath path)
178
    {
179
        return path.FullName;
3✔
180
    }
181

182
    /// <inheritdoc />
183
    protected override void CopyFileImpl(UPath srcPath, UPath destPath, bool overwrite)
184
    {
185
        if (srcPath == destPath)
2,014✔
186
        {
187
            throw new IOException("Source and destination path must be different.");
4✔
188
        }
189

190
        var srcEntry = GetEntry(srcPath, out var isDirectory);
2,010✔
191

192
        if (isDirectory)
2,010✔
193
        {
194
            throw new UnauthorizedAccessException(nameof(srcPath) + " is a directory.");
1✔
195
        }
196

197
        if (srcEntry == null)
2,009✔
198
        {
199
            if (!DirectoryExistsImpl(srcPath.GetDirectory()))
2!
200
            {
201
                throw new DirectoryNotFoundException(srcPath.GetDirectory().FullName);
×
202
            }
203

204
            throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
2✔
205
        }
206

207
        var parentDirectory = destPath.GetDirectory();
2,007✔
208
        if (!DirectoryExistsImpl(parentDirectory))
2,007✔
209
        {
210
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(parentDirectory);
1✔
211
        }
212

213
        if (DirectoryExistsImpl(destPath))
2,006✔
214
        {
215
            if (!FileExistsImpl(destPath))
1✔
216
            {
217
                throw new IOException("Destination path is a directory");
1✔
218
            }
219
        }
220

221
        var destEntry = GetEntry(destPath);
2,005✔
222
        if (destEntry != null)
2,005✔
223
        {
224
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
225
            if ((destEntry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly)
3✔
226
            {
227
                throw new UnauthorizedAccessException("Destination file is read only");
1✔
228
            }
229
#endif
230
            if (!overwrite)
2✔
231
            {
232
                throw FileSystemExceptionHelper.NewDestinationFileExistException(srcPath);
1✔
233
            }
234

235
            RemoveEntry(destEntry);
1✔
236
            TryGetDispatcher()?.RaiseDeleted(destPath);
1!
237
        }
238

239
        destEntry = CreateEntry(destPath.FullName);
2,003✔
240
        using (var destStream = destEntry.Open())
2,003✔
241
        {
242
            using var srcStream = srcEntry.Open();
2,003✔
243
            srcStream.CopyTo(destStream);
2,003✔
244
        }
245
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
246
        destEntry.ExternalAttributes = srcEntry.ExternalAttributes | (int)FileAttributes.Archive;
2,003✔
247
#endif
248
        TryGetDispatcher()?.RaiseCreated(destPath);
2,003!
249
    }
×
250

251
    /// <inheritdoc />
252
    protected override void CreateDirectoryImpl(UPath path)
253
    {
254
        if (FileExistsImpl(path))
4,097✔
255
        {
256
            throw FileSystemExceptionHelper.NewDestinationFileExistException(path);
1✔
257
        }
258

259
        if (DirectoryExistsImpl(path))
4,096✔
260
        {
261
            throw FileSystemExceptionHelper.NewDestinationDirectoryExistException(path);
1✔
262
        }
263

264
        var parentPath = new UPath(GetParent(path.FullName));
4,095✔
265
        if (parentPath != "")
4,095✔
266
        {
267
            if (!DirectoryExistsImpl(parentPath))
2,047✔
268
            {
269
                CreateDirectoryImpl(parentPath);
7✔
270
            }
271
        }
272

273
        CreateEntry(path, isDirectory: true);
4,095✔
274
        TryGetDispatcher()?.RaiseCreated(path);
4,095!
UNCOV
275
    }
×
276

277
    /// <inheritdoc />
278
    protected override void DeleteDirectoryImpl(UPath path, bool isRecursive)
279
    {
280
        if (FileExistsImpl(path))
2,011✔
281
        {
282
            throw new IOException(nameof(path) + " is a file.");
1✔
283
        }
284

285
        var entries = new List<ZipArchiveEntry>();
2,010✔
286
        if (!isRecursive)
2,010✔
287
        {
288
            // folder name ends with slash so StartWith check is enough
289
            _entriesLock.EnterReadLock();
2✔
290
            try
291
            {
292
                entries = _entries
2✔
293
                    .Where(x => x.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
6!
294
                    .Take(2)
2✔
295
                    .Select(x => x.Value.Entry)
3✔
296
                    .ToList();
2✔
297
            }
2✔
298
            finally
299
            {
300
                _entriesLock.ExitReadLock();
2✔
301
            }
2✔
302

303
            if (entries.Count == 0)
2!
304
            {
305
                throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path);
×
306
            }
307

308
            if (entries.Count == 1)
2✔
309
            {
310
                RemoveEntry(entries[0]);
1✔
311
            }
312

313
            if (entries.Count == 2)
2✔
314
            {
315
                throw new IOException("Directory is not empty");
1✔
316
            }
317

318
            TryGetDispatcher()?.RaiseDeleted(path);
1!
319
            return;
×
320
        }
321

322
        _entriesLock.EnterReadLock();
2,008✔
323
        try
324
        {
325
            entries = _entries
2,008✔
326
                .Where(x => x.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
8,062!
327
                .Select(x => x.Value.Entry)
2,014✔
328
                .ToList();
2,008✔
329

330
            if (entries.Count == 0)
2,008✔
331
            {
332
                throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path);
1✔
333
            }
334

335
            // check if there are no open file in directory
336
            foreach (var entry in entries)
8,041✔
337
            {
338
                lock (_openStreamsLock)
2,014✔
339
                {
340
                    if (_openStreams.ContainsKey(entry))
2,014✔
341
                    {
342
                        throw new IOException($"There is an open file {entry.FullName} in directory");
1✔
343
                    }
344
                }
2,013✔
345
            }
346
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
347
            // check if there are none readonly entries
348
            foreach (var entry in entries)
8,034✔
349
            {
350
                if ((entry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly)
2,012✔
351
                {
352
                    throw entry.FullName.Length == path.FullName.Length + 1
2✔
353
                        ? new IOException("Directory is read only")
2✔
354
                        : new UnauthorizedAccessException($"Cannot delete directory that contains readonly entry {entry.FullName}");
2✔
355
                }
356
            }
357
#endif
358
        }
359
        finally
360
        {
361
            _entriesLock.ExitReadLock();
2,008✔
362
        }
2,008✔
363

364
        _entriesLock.EnterWriteLock();
2,004✔
365
        try
366
        {
367
            foreach (var entry in entries)
8,026✔
368
            {
369
                _entries.Remove(new UPath(entry.FullName).ToAbsolute());
2,009✔
370
                entry.Delete();
2,009✔
371
            }
372
        }
373
        finally
374
        {
375
            _entriesLock.ExitWriteLock();
2,004✔
376
        }
2,004✔
377

378
        TryGetDispatcher()?.RaiseDeleted(path);
2,004!
379
    }
×
380

381
    /// <inheritdoc />
382
    protected override void DeleteFileImpl(UPath path)
383
    {
384
        if (DirectoryExistsImpl(path))
4,008✔
385
        {
386
            throw new IOException("Cannot delete a directory");
1✔
387
        }
388

389
        var entry = GetEntry(path);
4,007✔
390
        if (entry == null)
4,007✔
391
        {
392
            return;
2✔
393
        }
394
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
395
        if ((entry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly)
4,005✔
396
        {
397
            throw new UnauthorizedAccessException("Cannot delete file with readonly attribute");
1✔
398
        }
399
#endif
400

401
        TryGetDispatcher()?.RaiseDeleted(path);
4,004!
402
        RemoveEntry(entry);
4,004✔
403
    }
4,003✔
404

405
    /// <inheritdoc />
406
    protected override bool DirectoryExistsImpl(UPath path)
407
    {
408
        if (path.FullName is "/" or "\\" or "")
18,333✔
409
        {
410
            return true;
4,021✔
411
        }
412

413
        _entriesLock.EnterReadLock();
14,312✔
414

415
        try
416
        {
417
            return _entries.TryGetValue(path, out var entry) && entry.IsDirectory;
14,312✔
418
        }
419
        finally
420
        {
421
            _entriesLock.ExitReadLock();
14,312✔
422
        }
14,312✔
423
    }
14,312✔
424

425
    /// <inheritdoc />
426
    protected override void Dispose(bool disposing)
427
    {
428
        _archive.Dispose();
45✔
429

430
        if (_stream != null && _disposeStream)
45✔
431
        {
432
            _stream.Dispose();
30✔
433
        }
434

435
        if (disposing)
45✔
436
        {
437
            TryGetDispatcher()?.Dispose();
7!
438
        }
439
    }
38✔
440

441
    /// <inheritdoc />
442
    protected override IEnumerable<FileSystemItem> EnumerateItemsImpl(UPath path, SearchOption searchOption, SearchPredicate? searchPredicate)
443
    {
444
        return EnumeratePathsStr(path, "*", searchOption, SearchTarget.Both).Select(p => new FileSystemItem(this, p, p[p.Length - 1] == DirectorySeparator));
290✔
445
    }
446

447
    /// <inheritdoc />
448
    protected override IEnumerable<UPath> EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
449
    {
450
        return EnumeratePathsStr(path, searchPattern, searchOption, searchTarget).Select(x => new UPath(x));
9,811✔
451
    }
452

453
    private IEnumerable<string> EnumeratePathsStr(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
454
    {
455
        var search = SearchPattern.Parse(ref path, ref searchPattern);
2,173✔
456

457
        _entriesLock.EnterReadLock();
2,173✔
458
        var entriesList = new List<ZipArchiveEntry>();
2,173✔
459
        try
460
        {
461
            var internEntries = path == UPath.Root
2,173✔
462
                ? _entries
2,173✔
463
                : _entries.Where(kv => kv.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) && kv.Key.FullName.Length > path.FullName.Length);
3,863!
464

465
            if (searchOption == SearchOption.TopDirectoryOnly)
2,173✔
466
            {
467
                internEntries = internEntries.Where(kv => kv.Key.IsInDirectory(path, false));
11,648✔
468
            }
469

470
            entriesList = internEntries.Select(kv => kv.Value.Entry).ToList();
11,091✔
471
        }
2,173✔
472
        finally
473
        {
474
            _entriesLock.ExitReadLock();
2,173✔
475
        }
2,173✔
476

477
        if (entriesList.Count == 0)
2,173✔
478
        {
479
            return Enumerable.Empty<string>();
17✔
480
        }
481

482
        var entries = (IEnumerable<ZipArchiveEntry>)entriesList;
2,156✔
483

484
        if (searchTarget == SearchTarget.File)
2,156✔
485
        {
486
            entries = entries.Where(e => e.FullName[e.FullName.Length - 1] != DirectorySeparator);
1,135✔
487
        }
488
        else if (searchTarget == SearchTarget.Directory)
2,091✔
489
        {
490
            entries = entries.Where(e => e.FullName[e.FullName.Length - 1] == DirectorySeparator);
524✔
491
        }
492

493
        if (!string.IsNullOrEmpty(searchPattern))
2,156✔
494
        {
495
            entries = entries.Where(e => search.Match(GetName(e)));
10,389✔
496
        }
497

498
        return entries.Select(e => '/' + e.FullName);
10,084✔
499
    }
500

501
    /// <inheritdoc />
502
    protected override bool FileExistsImpl(UPath path)
503
    {
504
        _entriesLock.EnterReadLock();
10,257✔
505

506
        try
507
        {
508
            return _entries.TryGetValue(path, out var entry) && !entry.IsDirectory;
10,257✔
509
        }
510
        finally
511
        {
512
            _entriesLock.ExitReadLock();
10,257✔
513
        }
10,257✔
514
    }
10,257✔
515

516
    /// <inheritdoc />
517
    protected override FileAttributes GetAttributesImpl(UPath path)
518
    {
519
        var entry = GetEntry(path);
44✔
520
        if (entry is null)
44!
521
        {
522
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
×
523
        }
524

525
        var attributes = entry.FullName[entry.FullName.Length - 1] == DirectorySeparator ? FileAttributes.Directory : 0;
44✔
526

527
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
528
        const FileAttributes validValues = (FileAttributes)0x7FFF /* Up to FileAttributes.Encrypted */ | FileAttributes.IntegrityStream | FileAttributes.NoScrubData;
529
        var externalAttributes = (FileAttributes)entry.ExternalAttributes & validValues;
44✔
530

531
        if (externalAttributes == 0 && attributes == 0)
44✔
532
        {
533
            attributes |= FileAttributes.Normal;
1✔
534
        }
535

536
        return externalAttributes | attributes;
44✔
537
#else
538
        // return standard attributes if it's not NetStandard2.1
539
        return attributes == FileAttributes.Directory ? FileAttributes.Directory : entry.LastWriteTime >= _creationTime ? FileAttributes.Archive : FileAttributes.Normal;
42!
540
#endif
541
    }
542

543
    /// <inheritdoc />
544
    protected override long GetFileLengthImpl(UPath path)
545
    {
546
        var entry = GetEntry(path, out var isDirectory);
12✔
547

548
        if (entry == null || isDirectory)
12✔
549
        {
550
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
3✔
551
        }
552

553
        try
554
        {
555
            return entry.Length;
9✔
556
        }
557
        catch (Exception ex) // for some reason entry.Length doesn't work with MemoryStream used in tests
9✔
558
        {
559
            Debug.WriteLine(ex.Message);
560
            using var stream = OpenFileImpl(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
9✔
561
            return stream.Length;
9✔
562
        }
563
    }
9✔
564

565
    /// <summary>
566
    ///     Not supported by zip format. Return last write time.
567
    /// </summary>
568
    protected override DateTime GetCreationTimeImpl(UPath path)
569
    {
570
        return GetLastWriteTimeImpl(path);
5✔
571
    }
572

573
    /// <summary>
574
    ///     Not supported by zip format. Return last write time
575
    /// </summary>
576
    protected override DateTime GetLastAccessTimeImpl(UPath path)
577
    {
578
        return GetLastWriteTimeImpl(path);
3✔
579
    }
580

581
    /// <inheritdoc />
582
    protected override DateTime GetLastWriteTimeImpl(UPath path)
583
    {
584
        var entry = GetEntry(path);
47✔
585
        if (entry == null)
47✔
586
        {
587
            return DefaultFileTime;
3✔
588
        }
589

590
        return entry.LastWriteTime.DateTime;
44✔
591
    }
592

593
    /// <inheritdoc />
594
    protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath)
595
    {
596
        if (destPath.IsInDirectory(srcPath, true))
2,006✔
597
        {
598
            throw new IOException("Cannot move directory to itself or a subdirectory.");
1✔
599
        }
600

601
        if (FileExistsImpl(srcPath))
2,005✔
602
        {
603
            throw new IOException(nameof(srcPath) + " is a file.");
1✔
604
        }
605

606
        var srcDir = srcPath.FullName;
2,004✔
607

608
        _entriesLock.EnterReadLock();
2,004✔
609
        var entries = Array.Empty<ZipArchiveEntry>();
2,004✔
610
        try
611
        {
612
            entries = _archive.Entries.Where(e => e.FullName.StartsWith(srcDir, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)).ToArray();
2,009,072!
613
        }
2,004✔
614
        finally
615
        {
616
            _entriesLock.ExitReadLock();
2,004✔
617
        }
2,004✔
618

619
        if (entries.Length == 0)
2,004✔
620
        {
621
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(srcPath);
1✔
622
        }
623

624
        CreateDirectoryImpl(destPath);
2,003✔
625
        foreach (var entry in entries)
8,018✔
626
        {
627
            if (entry.FullName.Length == srcDir.Length)
2,007!
628
            {
UNCOV
629
                RemoveEntry(entry);
×
UNCOV
630
                continue;
×
631
            }
632

633
            using (var entryStream = entry.Open())
2,007✔
634
            {
635
                var entryName = entry.FullName.Substring(srcDir.Length);
2,007✔
636
                var destEntry = CreateEntry(destPath + entryName, isDirectory: true);
2,007✔
637
                using (var destEntryStream = destEntry.Open())
2,007✔
638
                {
639
                    entryStream.CopyTo(destEntryStream);
2,007✔
640
                }
2,007✔
641
            }
642

643
            TryGetDispatcher()?.RaiseCreated(destPath);
2,007!
644
            RemoveEntry(entry);
2,007✔
645
            TryGetDispatcher()?.RaiseDeleted(srcPath);
2,007!
646
        }
647
    }
2,002✔
648

649
    /// <inheritdoc />
650
    protected override void MoveFileImpl(UPath srcPath, UPath destPath)
651
    {
652
        var srcEntry = GetEntry(srcPath) ?? throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
4,006✔
653

654
        if (!DirectoryExistsImpl(destPath.GetDirectory()))
4,004✔
655
        {
656
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(destPath.GetDirectory());
1✔
657
        }
658
        
659
        var destEntry = GetEntry(destPath);
4,003✔
660
        if (destEntry != null)
4,003✔
661
        {
662
            throw new IOException("Cannot overwrite existing file.");
1✔
663
        }        
664

665
        destEntry = CreateEntry(destPath.FullName);
4,002✔
666
        TryGetDispatcher()?.RaiseCreated(destPath);
4,002!
667
        using (var destStream = destEntry.Open())
4,002✔
668
        {
669
            using var srcStream = srcEntry.Open();
4,002✔
670
            srcStream.CopyTo(destStream);
4,002✔
671
        }
672

673
        RemoveEntry(srcEntry);
4,002✔
674
        TryGetDispatcher()?.RaiseDeleted(srcPath);
4,002!
675
    }
×
676

677
    /// <inheritdoc />
678
    protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess access, FileShare share)
679
    {
680
        if (_archive.Mode == ZipArchiveMode.Read && access == FileAccess.Write)
6,270!
681
        {
682
            throw new UnauthorizedAccessException("Cannot open a file for writing in a read-only archive.");
×
683
        }
684

685
        if (access == FileAccess.Read && (mode == FileMode.CreateNew || mode == FileMode.Create || mode == FileMode.Truncate || mode == FileMode.Append))
6,270✔
686
        {
687
            throw new ArgumentException("Cannot write in a read-only access.");
1✔
688
        }
689

690
        var entry = GetEntry(path, out var isDirectory);
6,269✔
691

692
        if (isDirectory)
6,269✔
693
        {
694
            throw new UnauthorizedAccessException(nameof(path) + " is a directory.");
1✔
695
        }
696

697
        if (entry == null)
6,268✔
698
        {
699
            if (mode is FileMode.Create or FileMode.CreateNew or FileMode.OpenOrCreate or FileMode.Append)
2,165✔
700
            {
701
                entry = CreateEntry(path.FullName);
2,161✔
702
#if NETSTANDARD2_1
703
                entry.ExternalAttributes = (int)FileAttributes.Archive;
704
#endif
705
                TryGetDispatcher()?.RaiseCreated(path);
2,161✔
706
            }
707
            else
708
            {
709
                if (!DirectoryExistsImpl(path.GetDirectory()))
4✔
710
                {
711
                    throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path.GetDirectory());
1✔
712
                }
713

714
                throw FileSystemExceptionHelper.NewFileNotFoundException(path);
3✔
715
            }
716
        }
717
        else if (mode == FileMode.CreateNew)
4,103✔
718
        {
719
            throw new IOException("Cannot create a file in CreateNew mode if it already exists.");
2✔
720
        }
721
        else if (mode == FileMode.Create)
4,101✔
722
        {
723
            RemoveEntry(entry);
11✔
724
            entry = CreateEntry(path.FullName);
10✔
725
        }
726

727
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
728
        if ((access == FileAccess.Write || access == FileAccess.ReadWrite) && (entry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly)
6,261✔
729
        {
730
            throw new UnauthorizedAccessException("Cannot open a file for writing in a file with readonly attribute.");
1✔
731
        }
732
#endif
733

734
        var stream = new ZipEntryStream(share, this, entry);
6,260✔
735

736
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
737
        if (access is FileAccess.Write or FileAccess.ReadWrite)
6,258✔
738
        {
739
            entry.ExternalAttributes |= (int)FileAttributes.Archive;
2,176✔
740
        }
741
#endif
742

743
        if (mode == FileMode.Append)
6,258✔
744
        {
745
            stream.Seek(0, SeekOrigin.End);
4✔
746
        }
747
        else if (mode == FileMode.Truncate)
6,254✔
748
        {
749
            stream.SetLength(0);
1✔
750
        }
751

752
        return stream;
6,258✔
753
    }
754

755
    /// <inheritdoc />
756
    protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath destBackupPath, bool ignoreMetadataErrors)
757
    {
758
        var sourceEntry = GetEntry(srcPath);
5✔
759
        if (sourceEntry is null)
5!
760
        {
761
            throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
×
762
        }
763

764
        var destEntry = GetEntry(destPath);
5✔
765
        if (destEntry == sourceEntry)
5✔
766
        {
767
            throw new IOException("Cannot replace the file with itself.");
1✔
768
        }
769

770
        if (destEntry != null)
4✔
771
        {
772
            // create a backup at destBackupPath if its not null
773
            if (!destBackupPath.IsEmpty)
4✔
774
            {
775
                var destBackupEntry = CreateEntry(destBackupPath.FullName);
4✔
776
                using var destBackupStream = destBackupEntry.Open();
4✔
777
                using var destStream = destEntry.Open();
4✔
778
                destStream.CopyTo(destBackupStream);
4✔
779
            }
780

781
            RemoveEntry(destEntry);
4✔
782
        }
783

784
        var newEntry = CreateEntry(destPath.FullName);
4✔
785
        using (var newStream = newEntry.Open())
4✔
786
        {
787
            using (var sourceStream = sourceEntry.Open())
4✔
788
            {
789
                sourceStream.CopyTo(newStream);
4✔
790
            }
4✔
791
        }
792

793
        RemoveEntry(sourceEntry);
4✔
794
        TryGetDispatcher()?.RaiseDeleted(srcPath);
4!
795
        TryGetDispatcher()?.RaiseCreated(destPath);
4!
796
    }
×
797

798
    /// <summary>
799
    ///     Implementation for <see cref="SetAttributes" />, <paramref name="path" /> is guaranteed to be absolute and
800
    ///     validated through <see cref="ValidatePath" />. Works only in Net Standard 2.1
801
    ///     Sets the specified <see cref="FileAttributes" /> of the file or directory on the specified path.
802
    /// </summary>
803
    /// <param name="path">The path to the file or directory.</param>
804
    /// <param name="attributes">A bitwise combination of the enumeration values.</param>
805
    protected override void SetAttributesImpl(UPath path, FileAttributes attributes)
806
    {
807
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
808
        var entry = GetEntry(path);
39✔
809
        if (entry == null)
39!
810
        {
811
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
×
812
        }
813

814
        entry.ExternalAttributes = (int)attributes;
39✔
815
        TryGetDispatcher()?.RaiseChange(path);
39!
816
#else
817
        Debug.WriteLine("SetAttributes don't work in NetStandard2.0 or older.");
818
#endif
819
    }
32✔
820

821
    /// <summary>
822
    ///     Not supported by zip format. Does nothing.
823
    /// </summary>
824
    protected override void SetCreationTimeImpl(UPath path, DateTime time)
825
    {
826

827
    }
1✔
828

829
    /// <summary>
830
    ///     Not supported by zip format. Does nothing.
831
    /// </summary>
832
    protected override void SetLastAccessTimeImpl(UPath path, DateTime time)
833
    {
834

835
    }
×
836

837
    /// <inheritdoc />
838
    protected override void SetLastWriteTimeImpl(UPath path, DateTime time)
839
    {
840
        var entry = GetEntry(path);
32✔
841
        if (entry is null)
32!
842
        {
843
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
×
844
        }
845

846
        TryGetDispatcher()?.RaiseChange(path);
32!
847
        entry.LastWriteTime = time;
32✔
848
    }
32✔
849

850
    /// <inheritdoc />
851
    protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget)
852
    {
853
        throw new NotSupportedException("Symbolic links are not supported by ZipArchiveFileSystem");
×
854
    }
855

856
    /// <inheritdoc />
857
    protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath)
858
    {
859
        resolvedPath = UPath.Empty;
×
860
        return false;
×
861
    }
862

863
    /// <inheritdoc />
864
    protected override IFileSystemWatcher WatchImpl(UPath path)
865
    {
866
        var watcher = new FileSystemWatcher(this, path);
1✔
867
        lock (_dispatcherLock)
1✔
868
        {
869
            _dispatcher ??= new FileSystemEventDispatcher<FileSystemWatcher>(this);
1✔
870
            _dispatcher.Add(watcher);
1✔
871
        }
1✔
872

873
        return watcher;
1✔
874
    }
875

876
    private void RemoveEntry(ZipArchiveEntry entry)
877
    {
878
        _entriesLock.EnterWriteLock();
10,034✔
879
        try
880
        {
881
            entry.Delete();
10,034✔
882
            _entries.Remove(new UPath(entry.FullName).ToAbsolute());
10,032✔
883
        }
10,032✔
884
        finally
885
        {
886
            _entriesLock.ExitWriteLock();
10,034✔
887
        }
10,034✔
888
    }
10,032✔
889

890
    private ZipArchiveEntry CreateEntry(UPath path, bool isDirectory = false)
891
    {
892
        _entriesLock.EnterWriteLock();
14,286✔
893
        try
894
        {
895
            var internalPath = path.FullName;
14,286✔
896

897
            if (isDirectory)
14,286✔
898
            {
899
                internalPath += DirectorySeparator;
6,102✔
900
            }
901

902
            var entry = _archive.CreateEntry(internalPath, _compressionLevel);
14,286✔
903
            _entries[path] = new InternalZipEntry(entry, isDirectory);
14,286✔
904
            return entry;
14,286✔
905
        }
906
        finally
907
        {
908
            _entriesLock.ExitWriteLock();
14,286✔
909
        }
14,286✔
910
    }
14,286✔
911

912
    private static readonly char[] s_slashChars = { '/', '\\' };
1✔
913

914
    private static string GetName(ZipArchiveEntry entry)
915
    {
916
        var name = entry.FullName.TrimEnd(s_slashChars);
8,233✔
917
        var index = name.LastIndexOfAny(s_slashChars);
8,233✔
918
        return name.Substring(index + 1);
8,233✔
919
    }
920

921
    private static string GetParent(string path)
922
    {
923
        path = path.TrimEnd(s_slashChars);
4,095✔
924
        var lastIndex = path.LastIndexOfAny(s_slashChars);
4,095✔
925
        return lastIndex == -1 ? "" : path.Substring(0, lastIndex);
4,095!
926
    }
927

928
    private FileSystemEventDispatcher<FileSystemWatcher>? TryGetDispatcher()
929
    {
930
        lock (_dispatcherLock)
26,373✔
931
        {
932
            return _dispatcher;
26,373✔
933
        }
934
    }
26,373✔
935

936
    private sealed class ZipEntryStream : Stream
937
    {
938
        private readonly ZipArchiveEntry _entry;
939
        private readonly ZipArchiveFileSystem _fileSystem;
940
        private readonly Stream _streamImplementation;
941
        private bool _isDisposed;
942

943
        public ZipEntryStream(FileShare share, ZipArchiveFileSystem system, ZipArchiveEntry entry)
6,260✔
944
        {
945
            _entry = entry;
6,260✔
946
            _fileSystem = system;
6,260✔
947

948
            lock (_fileSystem._openStreamsLock)
6,260✔
949
            {
950
                var fileShare = _fileSystem._openStreams.TryGetValue(entry, out var fileData) ? fileData.Share : FileShare.ReadWrite;
6,260✔
951
                if (fileData != null)
6,260✔
952
                {
953
                    // we only check for read share, because ZipArchive doesn't support write share
954
                    if (share is not FileShare.Read and not FileShare.ReadWrite)
1,156!
955
                    {
956
                        throw new IOException("File is already opened for reading");
×
957
                    }
958

959
                    if (fileShare is not FileShare.Read and not FileShare.ReadWrite)
1,156✔
960
                    {
961
                        throw new IOException("File is already opened for reading by another stream with non compatible share");
1✔
962
                    }
963

964
                    fileData.Count++;
1,155✔
965
                }
966
                else
967
                {
968
                    _fileSystem._openStreams.Add(_entry, new EntryState(share));
6,108✔
969
                }
970
                _streamImplementation = entry.Open();
6,259✔
971
            }
6,258✔
972

973
            Share = share;
974
        }
6,258✔
975

976
        private FileShare Share { get; }
977

978
        public override bool CanRead => _streamImplementation.CanRead;
133✔
979

980
        public override bool CanSeek => _streamImplementation.CanSeek;
175✔
981

982
        public override bool CanWrite => _streamImplementation.CanWrite;
172✔
983

984
        public override long Length => _streamImplementation.Length;
45✔
985

986
        public override long Position
987
        {
988
            get => _streamImplementation.Position;
176✔
989
            set => _streamImplementation.Position = value;
2✔
990
        }
991

992
        public override void Flush()
993
        {
994
            _streamImplementation.Flush();
278✔
995
        }
277✔
996

997
        public override int Read(byte[] buffer, int offset, int count)
998
        {
999
            return _streamImplementation.Read(buffer, offset, count);
141✔
1000
        }
1001

1002
        public override long Seek(long offset, SeekOrigin origin)
1003
        {
1004
            return _streamImplementation.Seek(offset, origin);
10✔
1005
        }
1006

1007
        public override void SetLength(long value)
1008
        {
1009
            _streamImplementation.SetLength(value);
2✔
1010
        }
1✔
1011

1012
        public override void Write(byte[] buffer, int offset, int count)
1013
        {
1014
            _streamImplementation.Write(buffer, offset, count);
177✔
1015
        }
176✔
1016

1017
        public override void Close()
1018
        {
1019
            if (_isDisposed)
6,259✔
1020
            {
1021
                return;
1✔
1022
            }
1023

1024
            _streamImplementation.Close();
6,258✔
1025
            _isDisposed = true;
6,258✔
1026
            lock (_fileSystem._openStreamsLock)
6,258✔
1027
            {
1028
                if (!_fileSystem._openStreams.TryGetValue(_entry, out var fileData))
6,258!
1029
                {
NEW
1030
                    return;
×
1031
                }
1032
                fileData.Count--;
6,258✔
1033
                if (fileData.Count == 0)
6,258✔
1034
                {
1035
                    _fileSystem._openStreams.Remove(_entry);
6,107✔
1036
                }
1037
            }
6,258✔
1038
        }
6,258✔
1039
    }
1040

1041
    private sealed class EntryState
1042
    {
1043
        public EntryState(FileShare share)
1044
        {
1045
            Share = share;
1046
            Count = 1;
6,108✔
1047
        }
6,108✔
1048

1049
        public FileShare Share { get; }
1050

1051
        public int Count;
1052

1053
    }
1054

1055
    private readonly struct InternalZipEntry
1056
    {
1057
        public InternalZipEntry(ZipArchiveEntry entry, bool isDirectory)
1058
        {
1059
            Entry = entry;
14,303✔
1060
            IsDirectory = isDirectory;
14,303✔
1061
        }
14,303✔
1062

1063
        public readonly ZipArchiveEntry Entry;
1064
        public readonly bool IsDirectory;
1065
    }
1066
}
1067
#endif
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