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

xoofx / zio / 14308567185

07 Apr 2025 11:58AM UTC coverage: 90.692% (+0.2%) from 90.462%
14308567185

push

github

xoofx
Breaking change: remove support for net4.6.2

1889 of 2257 branches covered (83.7%)

Branch coverage included in aggregate %.

5652 of 6058 relevant lines covered (93.3%)

20394.13 hits per line

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

91.25
/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
            {
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
        {
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.GetDirectoryAsSpan()))
2!
200
            {
201
                throw new DirectoryNotFoundException(srcPath.GetDirectory().FullName);
×
202
            }
203

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

207
        var parentDirectory = destPath.GetDirectoryAsSpan();
2,007✔
208
        if (!DirectoryExistsImpl(parentDirectory))
2,007✔
209
        {
210
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(parentDirectory.ToString());
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 = GetParent(path.AsSpan());
4,095✔
265
        if (!parentPath.IsEmpty)
4,095✔
266
        {
267
            if (!DirectoryExistsImpl(parentPath))
2,047✔
268
            {
269
                CreateDirectoryImpl(parentPath.ToString());
7✔
270
            }
271
        }
272

273
        CreateEntry(path, isDirectory: true);
4,095✔
274
        TryGetDispatcher()?.RaiseCreated(path);
4,095!
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))
7,500!
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
        return DirectoryExistsImpl(path.FullName.AsSpan());
10,269✔
409
    }
410

411
    private bool DirectoryExistsImpl(ReadOnlySpan<char> path)
412
    {
413
        if (path is "/" or "\\" or "")
18,333✔
414
        {
415
            return true;
4,021✔
416
        }
417

418
        _entriesLock.EnterReadLock();
14,312✔
419

420
        try
421
        {
422
#if HAS_ALTERNATEEQUALITYCOMPARER
423
            return _entries.GetAlternateLookup<ReadOnlySpan<char>>().TryGetValue(path, out var entry) && entry.IsDirectory;
14,312✔
424
#else
425
            return _entries.TryGetValue(path.ToString(), out var entry) && entry.IsDirectory;
14,312✔
426
#endif
427
        }
428
        finally
429
        {
430
            _entriesLock.ExitReadLock();
14,312✔
431
        }
14,312✔
432
    }
14,312✔
433

434
    /// <inheritdoc />
435
    protected override void Dispose(bool disposing)
436
    {
437
        _archive.Dispose();
45✔
438

439
        if (_stream != null && _disposeStream)
45✔
440
        {
441
            _stream.Dispose();
30✔
442
        }
443

444
        if (disposing)
45✔
445
        {
446
            TryGetDispatcher()?.Dispose();
7!
447
        }
448
    }
38✔
449

450
    /// <inheritdoc />
451
    protected override IEnumerable<FileSystemItem> EnumerateItemsImpl(UPath path, SearchOption searchOption, SearchPredicate? searchPredicate)
452
    {
453
        return EnumeratePathsStr(path, "*", searchOption, SearchTarget.Both).Select(p => new FileSystemItem(this, p, p[p.Length - 1] == DirectorySeparator));
290✔
454
    }
455

456
    /// <inheritdoc />
457
    protected override IEnumerable<UPath> EnumeratePathsImpl(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
458
    {
459
        return EnumeratePathsStr(path, searchPattern, searchOption, searchTarget).Select(x => new UPath(x));
9,979✔
460
    }
461

462
    private IEnumerable<string> EnumeratePathsStr(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget)
463
    {
464
        var search = SearchPattern.Parse(ref path, ref searchPattern);
2,173✔
465

466
        _entriesLock.EnterReadLock();
2,173✔
467
        var entriesList = new List<ZipArchiveEntry>();
2,173✔
468
        try
469
        {
470
            var internEntries = path == UPath.Root
2,173✔
471
                ? _entries
2,173✔
472
                : _entries.Where(kv => kv.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) && kv.Key.FullName.Length > path.FullName.Length);
3,863!
473

474
            if (searchOption == SearchOption.TopDirectoryOnly)
2,173✔
475
            {
476
                internEntries = internEntries.Where(kv => kv.Key.IsInDirectory(path, false));
11,949✔
477
            }
478

479
            entriesList = internEntries.Select(kv => kv.Value.Entry).ToList();
11,259✔
480
        }
2,173✔
481
        finally
482
        {
483
            _entriesLock.ExitReadLock();
2,173✔
484
        }
2,173✔
485

486
        if (entriesList.Count == 0)
2,173✔
487
        {
488
            return Enumerable.Empty<string>();
17✔
489
        }
490

491
        var entries = (IEnumerable<ZipArchiveEntry>)entriesList;
2,156✔
492

493
        if (searchTarget == SearchTarget.File)
2,156✔
494
        {
495
            entries = entries.Where(e => e.FullName[e.FullName.Length - 1] != DirectorySeparator);
1,135✔
496
        }
497
        else if (searchTarget == SearchTarget.Directory)
2,091✔
498
        {
499
            entries = entries.Where(e => e.FullName[e.FullName.Length - 1] == DirectorySeparator);
524✔
500
        }
501

502
        if (!string.IsNullOrEmpty(searchPattern))
2,156✔
503
        {
504
            entries = entries.Where(e => search.Match(GetName(e)));
10,557✔
505
        }
506

507
        return entries.Select(e => '/' + e.FullName);
10,252✔
508
    }
509

510
    /// <inheritdoc />
511
    protected override bool FileExistsImpl(UPath path)
512
    {
513
        _entriesLock.EnterReadLock();
10,257✔
514

515
        try
516
        {
517
            return _entries.TryGetValue(path, out var entry) && !entry.IsDirectory;
10,257✔
518
        }
519
        finally
520
        {
521
            _entriesLock.ExitReadLock();
10,257✔
522
        }
10,257✔
523
    }
10,257✔
524

525
    /// <inheritdoc />
526
    protected override FileAttributes GetAttributesImpl(UPath path)
527
    {
528
        var entry = GetEntry(path);
44✔
529
        if (entry is null)
44!
530
        {
531
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
×
532
        }
533

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

536
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
537
        const FileAttributes validValues = (FileAttributes)0x7FFF /* Up to FileAttributes.Encrypted */ | FileAttributes.IntegrityStream | FileAttributes.NoScrubData;
538
        var externalAttributes = (FileAttributes)entry.ExternalAttributes & validValues;
44✔
539

540
        if (externalAttributes == 0 && attributes == 0)
44✔
541
        {
542
            attributes |= FileAttributes.Normal;
1✔
543
        }
544

545
        return externalAttributes | attributes;
44✔
546
#else
547
        // return standard attributes if it's not NetStandard2.1
548
        return attributes == FileAttributes.Directory ? FileAttributes.Directory : entry.LastWriteTime >= _creationTime ? FileAttributes.Archive : FileAttributes.Normal;
549
#endif
550
    }
551

552
    /// <inheritdoc />
553
    protected override long GetFileLengthImpl(UPath path)
554
    {
555
        var entry = GetEntry(path, out var isDirectory);
12✔
556

557
        if (entry == null || isDirectory)
12✔
558
        {
559
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
3✔
560
        }
561

562
        try
563
        {
564
            return entry.Length;
9✔
565
        }
566
        catch (Exception ex) // for some reason entry.Length doesn't work with MemoryStream used in tests
9✔
567
        {
568
            Debug.WriteLine(ex.Message);
569
            using var stream = OpenFileImpl(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
9✔
570
            return stream.Length;
9✔
571
        }
572
    }
9✔
573

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

582
    /// <summary>
583
    ///     Not supported by zip format. Return last write time
584
    /// </summary>
585
    protected override DateTime GetLastAccessTimeImpl(UPath path)
586
    {
587
        return GetLastWriteTimeImpl(path);
3✔
588
    }
589

590
    /// <inheritdoc />
591
    protected override DateTime GetLastWriteTimeImpl(UPath path)
592
    {
593
        var entry = GetEntry(path);
47✔
594
        if (entry == null)
47✔
595
        {
596
            return DefaultFileTime;
3✔
597
        }
598

599
        return entry.LastWriteTime.DateTime;
44✔
600
    }
601

602
    /// <inheritdoc />
603
    protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath)
604
    {
605
        if (destPath.IsInDirectory(srcPath, true))
2,006✔
606
        {
607
            throw new IOException("Cannot move directory to itself or a subdirectory.");
1✔
608
        }
609

610
        if (FileExistsImpl(srcPath))
2,005✔
611
        {
612
            throw new IOException(nameof(srcPath) + " is a file.");
1✔
613
        }
614

615
        var srcDir = srcPath.FullName;
2,004✔
616

617
        _entriesLock.EnterReadLock();
2,004✔
618
        var entries = Array.Empty<ZipArchiveEntry>();
2,004✔
619
        try
620
        {
621
            entries = _archive.Entries.Where(e => e.FullName.StartsWith(srcDir, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)).ToArray();
2,008,504!
622
        }
2,004✔
623
        finally
624
        {
625
            _entriesLock.ExitReadLock();
2,004✔
626
        }
2,004✔
627

628
        if (entries.Length == 0)
2,004✔
629
        {
630
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(srcPath);
1✔
631
        }
632

633
        CreateDirectoryImpl(destPath);
2,003✔
634
        foreach (var entry in entries)
8,018✔
635
        {
636
            if (entry.FullName.Length == srcDir.Length)
2,007!
637
            {
638
                RemoveEntry(entry);
×
639
                continue;
×
640
            }
641

642
            using (var entryStream = entry.Open())
2,007✔
643
            {
644
                var entryName = entry.FullName.Substring(srcDir.Length);
2,007✔
645
                var destEntry = CreateEntry(destPath + entryName, isDirectory: true);
2,007✔
646
                using (var destEntryStream = destEntry.Open())
2,007✔
647
                {
648
                    entryStream.CopyTo(destEntryStream);
2,007✔
649
                }
2,007✔
650
            }
651

652
            TryGetDispatcher()?.RaiseCreated(destPath);
2,007!
653
            RemoveEntry(entry);
2,007✔
654
            TryGetDispatcher()?.RaiseDeleted(srcPath);
2,007!
655
        }
656
    }
2,002✔
657

658
    /// <inheritdoc />
659
    protected override void MoveFileImpl(UPath srcPath, UPath destPath)
660
    {
661
        var srcEntry = GetEntry(srcPath) ?? throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
4,006✔
662

663
        if (!DirectoryExistsImpl(destPath.GetDirectoryAsSpan()))
4,004✔
664
        {
665
            throw FileSystemExceptionHelper.NewDirectoryNotFoundException(destPath.GetDirectory());
1✔
666
        }
667
        
668
        var destEntry = GetEntry(destPath);
4,003✔
669
        if (destEntry != null)
4,003✔
670
        {
671
            throw new IOException("Cannot overwrite existing file.");
1✔
672
        }        
673

674
        destEntry = CreateEntry(destPath.FullName);
4,002✔
675
        TryGetDispatcher()?.RaiseCreated(destPath);
4,002!
676
        using (var destStream = destEntry.Open())
4,002✔
677
        {
678
            using var srcStream = srcEntry.Open();
4,002✔
679
            srcStream.CopyTo(destStream);
4,002✔
680
        }
681

682
        RemoveEntry(srcEntry);
4,002✔
683
        TryGetDispatcher()?.RaiseDeleted(srcPath);
4,002!
684
    }
×
685

686
    /// <inheritdoc />
687
    protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess access, FileShare share)
688
    {
689
        if (_archive.Mode == ZipArchiveMode.Read && access == FileAccess.Write)
6,270!
690
        {
691
            throw new UnauthorizedAccessException("Cannot open a file for writing in a read-only archive.");
×
692
        }
693

694
        if (access == FileAccess.Read && (mode == FileMode.CreateNew || mode == FileMode.Create || mode == FileMode.Truncate || mode == FileMode.Append))
6,270✔
695
        {
696
            throw new ArgumentException("Cannot write in a read-only access.");
1✔
697
        }
698

699
        var entry = GetEntry(path, out var isDirectory);
6,269✔
700

701
        if (isDirectory)
6,269✔
702
        {
703
            throw new UnauthorizedAccessException(nameof(path) + " is a directory.");
1✔
704
        }
705

706
        if (entry == null)
6,268✔
707
        {
708
            if (mode is FileMode.Create or FileMode.CreateNew or FileMode.OpenOrCreate or FileMode.Append)
2,165✔
709
            {
710
                entry = CreateEntry(path.FullName);
2,161✔
711
#if NETSTANDARD2_1
712
                entry.ExternalAttributes = (int)FileAttributes.Archive;
713
#endif
714
                TryGetDispatcher()?.RaiseCreated(path);
2,161✔
715
            }
716
            else
717
            {
718
                if (!DirectoryExistsImpl(path.GetDirectoryAsSpan()))
4✔
719
                {
720
                    throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path.GetDirectory());
1✔
721
                }
722

723
                throw FileSystemExceptionHelper.NewFileNotFoundException(path);
3✔
724
            }
725
        }
726
        else if (mode == FileMode.CreateNew)
4,103✔
727
        {
728
            throw new IOException("Cannot create a file in CreateNew mode if it already exists.");
2✔
729
        }
730
        else if (mode == FileMode.Create)
4,101✔
731
        {
732
            RemoveEntry(entry);
11✔
733
            entry = CreateEntry(path.FullName);
10✔
734
        }
735

736
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
737
        if ((access == FileAccess.Write || access == FileAccess.ReadWrite) && (entry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly)
6,261✔
738
        {
739
            throw new UnauthorizedAccessException("Cannot open a file for writing in a file with readonly attribute.");
1✔
740
        }
741
#endif
742

743
        var stream = new ZipEntryStream(share, this, entry);
6,260✔
744

745
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
746
        if (access is FileAccess.Write or FileAccess.ReadWrite)
6,258✔
747
        {
748
            entry.ExternalAttributes |= (int)FileAttributes.Archive;
2,176✔
749
        }
750
#endif
751

752
        if (mode == FileMode.Append)
6,258✔
753
        {
754
            stream.Seek(0, SeekOrigin.End);
4✔
755
        }
756
        else if (mode == FileMode.Truncate)
6,254✔
757
        {
758
            stream.SetLength(0);
1✔
759
        }
760

761
        return stream;
6,258✔
762
    }
763

764
    /// <inheritdoc />
765
    protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath destBackupPath, bool ignoreMetadataErrors)
766
    {
767
        var sourceEntry = GetEntry(srcPath);
5✔
768
        if (sourceEntry is null)
5!
769
        {
770
            throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath);
×
771
        }
772

773
        var destEntry = GetEntry(destPath);
5✔
774
        if (destEntry == sourceEntry)
5✔
775
        {
776
            throw new IOException("Cannot replace the file with itself.");
1✔
777
        }
778

779
        if (destEntry != null)
4✔
780
        {
781
            // create a backup at destBackupPath if its not null
782
            if (!destBackupPath.IsEmpty)
4✔
783
            {
784
                var destBackupEntry = CreateEntry(destBackupPath.FullName);
4✔
785
                using var destBackupStream = destBackupEntry.Open();
4✔
786
                using var destStream = destEntry.Open();
4✔
787
                destStream.CopyTo(destBackupStream);
4✔
788
            }
789

790
            RemoveEntry(destEntry);
4✔
791
        }
792

793
        var newEntry = CreateEntry(destPath.FullName);
4✔
794
        using (var newStream = newEntry.Open())
4✔
795
        {
796
            using (var sourceStream = sourceEntry.Open())
4✔
797
            {
798
                sourceStream.CopyTo(newStream);
4✔
799
            }
4✔
800
        }
801

802
        RemoveEntry(sourceEntry);
4✔
803
        TryGetDispatcher()?.RaiseDeleted(srcPath);
4!
804
        TryGetDispatcher()?.RaiseCreated(destPath);
4!
805
    }
×
806

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

823
        entry.ExternalAttributes = (int)attributes;
39✔
824
        TryGetDispatcher()?.RaiseChange(path);
39!
825
#else
826
        Debug.WriteLine("SetAttributes don't work in NetStandard2.0 or older.");
827
#endif
828
    }
×
829

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

836
    }
1✔
837

838
    /// <summary>
839
    ///     Not supported by zip format. Does nothing.
840
    /// </summary>
841
    protected override void SetLastAccessTimeImpl(UPath path, DateTime time)
842
    {
843

844
    }
×
845

846
    /// <inheritdoc />
847
    protected override void SetLastWriteTimeImpl(UPath path, DateTime time)
848
    {
849
        var entry = GetEntry(path);
32✔
850
        if (entry is null)
32!
851
        {
852
            throw FileSystemExceptionHelper.NewFileNotFoundException(path);
×
853
        }
854

855
        TryGetDispatcher()?.RaiseChange(path);
32!
856
        entry.LastWriteTime = time;
32✔
857
    }
32✔
858

859
    /// <inheritdoc />
860
    protected override void CreateSymbolicLinkImpl(UPath path, UPath pathToTarget)
861
    {
862
        throw new NotSupportedException("Symbolic links are not supported by ZipArchiveFileSystem");
×
863
    }
864

865
    /// <inheritdoc />
866
    protected override bool TryResolveLinkTargetImpl(UPath linkPath, out UPath resolvedPath)
867
    {
868
        resolvedPath = UPath.Empty;
×
869
        return false;
×
870
    }
871

872
    /// <inheritdoc />
873
    protected override IFileSystemWatcher WatchImpl(UPath path)
874
    {
875
        var watcher = new FileSystemWatcher(this, path);
1✔
876
        lock (_dispatcherLock)
1✔
877
        {
878
            _dispatcher ??= new FileSystemEventDispatcher<FileSystemWatcher>(this);
1✔
879
            _dispatcher.Add(watcher);
1✔
880
        }
1✔
881

882
        return watcher;
1✔
883
    }
884

885
    private void RemoveEntry(ZipArchiveEntry entry)
886
    {
887
        _entriesLock.EnterWriteLock();
10,034✔
888
        try
889
        {
890
            entry.Delete();
10,034✔
891
            _entries.Remove(new UPath(entry.FullName).ToAbsolute());
10,032✔
892
        }
10,032✔
893
        finally
894
        {
895
            _entriesLock.ExitWriteLock();
10,034✔
896
        }
10,034✔
897
    }
10,032✔
898

899
    private ZipArchiveEntry CreateEntry(UPath path, bool isDirectory = false)
900
    {
901
        _entriesLock.EnterWriteLock();
14,286✔
902
        try
903
        {
904
            var internalPath = path.FullName;
14,286✔
905

906
            if (isDirectory)
14,286✔
907
            {
908
                internalPath += DirectorySeparator;
6,102✔
909
            }
910

911
            var entry = _archive.CreateEntry(internalPath, _compressionLevel);
14,286✔
912
            _entries[path] = new InternalZipEntry(entry, isDirectory);
14,286✔
913
            return entry;
14,286✔
914
        }
915
        finally
916
        {
917
            _entriesLock.ExitWriteLock();
14,286✔
918
        }
14,286✔
919
    }
14,286✔
920

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

923
    private static ReadOnlySpan<char> GetName(ZipArchiveEntry entry)
924
    {
925
        var name = entry.FullName.TrimEnd(s_slashChars);
8,401✔
926
        var index = name.LastIndexOfAny(s_slashChars);
8,401✔
927
        return index == -1 ? name.AsSpan() : name.AsSpan(index + 1);
8,401!
928
    }
929

930
    private static ReadOnlySpan<char> GetParent(ReadOnlySpan<char> path)
931
    {
932
        path = path.TrimEnd(s_slashChars);
4,095✔
933
        var lastIndex = path.LastIndexOfAny(s_slashChars);
4,095✔
934
        return lastIndex == -1 ? ReadOnlySpan<char>.Empty : path.Slice(0, lastIndex);
4,095!
935
    }
936

937
    private FileSystemEventDispatcher<FileSystemWatcher>? TryGetDispatcher()
938
    {
939
        lock (_dispatcherLock)
26,373✔
940
        {
941
            return _dispatcher;
26,373✔
942
        }
943
    }
26,373✔
944

945
    private sealed class ZipEntryStream : Stream
946
    {
947
        private readonly ZipArchiveEntry _entry;
948
        private readonly ZipArchiveFileSystem _fileSystem;
949
        private readonly Stream _streamImplementation;
950
        private bool _isDisposed;
951

952
        public ZipEntryStream(FileShare share, ZipArchiveFileSystem system, ZipArchiveEntry entry)
6,260✔
953
        {
954
            _entry = entry;
6,260✔
955
            _fileSystem = system;
6,260✔
956

957
            lock (_fileSystem._openStreamsLock)
6,260✔
958
            {
959
                var fileShare = _fileSystem._openStreams.TryGetValue(entry, out var fileData) ? fileData.Share : FileShare.ReadWrite;
6,260✔
960
                if (fileData != null)
6,260✔
961
                {
962
                    // we only check for read share, because ZipArchive doesn't support write share
963
                    if (share is not FileShare.Read and not FileShare.ReadWrite)
335!
964
                    {
965
                        throw new IOException("File is already opened for reading");
×
966
                    }
967

968
                    if (fileShare is not FileShare.Read and not FileShare.ReadWrite)
335✔
969
                    {
970
                        throw new IOException("File is already opened for reading by another stream with non compatible share");
1✔
971
                    }
972

973
                    fileData.Count++;
334✔
974
                }
975
                else
976
                {
977
                    _fileSystem._openStreams.Add(_entry, new EntryState(share));
6,257✔
978
                }
979
                _streamImplementation = entry.Open();
6,259✔
980
            }
6,258✔
981

982
            Share = share;
983
        }
6,258✔
984

985
        private FileShare Share { get; }
986

987
        public override bool CanRead => _streamImplementation.CanRead;
66✔
988

989
        public override bool CanSeek => _streamImplementation.CanSeek;
175✔
990

991
        public override bool CanWrite => _streamImplementation.CanWrite;
172✔
992

993
        public override long Length => _streamImplementation.Length;
45✔
994

995
        public override long Position
996
        {
997
            get => _streamImplementation.Position;
176✔
998
            set => _streamImplementation.Position = value;
2✔
999
        }
1000

1001
        public override void Flush()
1002
        {
1003
            _streamImplementation.Flush();
278✔
1004
        }
277✔
1005

1006
        public override int Read(byte[] buffer, int offset, int count)
1007
        {
1008
            return _streamImplementation.Read(buffer, offset, count);
141✔
1009
        }
1010

1011
        public override long Seek(long offset, SeekOrigin origin)
1012
        {
1013
            return _streamImplementation.Seek(offset, origin);
10✔
1014
        }
1015

1016
        public override void SetLength(long value)
1017
        {
1018
            _streamImplementation.SetLength(value);
2✔
1019
        }
1✔
1020

1021
        public override void Write(byte[] buffer, int offset, int count)
1022
        {
1023
            _streamImplementation.Write(buffer, offset, count);
177✔
1024
        }
176✔
1025

1026
        public override void Close()
1027
        {
1028
            if (_isDisposed)
6,259✔
1029
            {
1030
                return;
1✔
1031
            }
1032

1033
            _streamImplementation.Close();
6,258✔
1034
            _isDisposed = true;
6,258✔
1035
            lock (_fileSystem._openStreamsLock)
6,258✔
1036
            {
1037
                if (!_fileSystem._openStreams.TryGetValue(_entry, out var fileData))
6,258!
1038
                {
1039
                    return;
×
1040
                }
1041
                fileData.Count--;
6,258✔
1042
                if (fileData.Count == 0)
6,258✔
1043
                {
1044
                    _fileSystem._openStreams.Remove(_entry);
6,256✔
1045
                }
1046
            }
6,258✔
1047
        }
6,258✔
1048
    }
1049

1050
    private sealed class EntryState
1051
    {
1052
        public EntryState(FileShare share)
1053
        {
1054
            Share = share;
1055
            Count = 1;
6,257✔
1056
        }
6,257✔
1057

1058
        public FileShare Share { get; }
1059

1060
        public int Count;
1061

1062
    }
1063

1064
    private readonly struct InternalZipEntry
1065
    {
1066
        public InternalZipEntry(ZipArchiveEntry entry, bool isDirectory)
1067
        {
1068
            Entry = entry;
14,303✔
1069
            IsDirectory = isDirectory;
14,303✔
1070
        }
14,303✔
1071

1072
        public readonly ZipArchiveEntry Entry;
1073
        public readonly bool IsDirectory;
1074
    }
1075
}
1076
#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