diff --git a/README.md b/README.md
index 1b06bbe..c719657 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ building upon `Microsoft.Extensions.FileProviders`.
* [Ramstack.FileProviders](#ramstackfileproviders-2)
* [PrefixedFileProvider](#prefixedfileprovider)
* [SubFileProvider](#subfileprovider)
+ * [ZipFileProvider](#zipfileprovider)
* [Ramstack.FileProviders.Globbing](#ramstackfileprovidersglobbing-1)
* [Ramstack.FileProviders.Extensions](#ramstackfileprovidersextensions-1)
* [Ramstack.FileProviders.Composition](#ramstackfileproviderscomposition-1)
@@ -44,7 +45,7 @@ dotnet add package Ramstack.FileProviders.Extensions
```
### Ramstack.FileProviders
-Provides additional implementations of `IFileProvider` including `PrefixedFileProvider` and `SubFileProvider`.
+Provides additional implementations of `IFileProvider` including `PrefixedFileProvider`, `SubFileProvider`, and `ZipFileProvider`.
To install the `Ramstack.FileProviders` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders) in your project,
run the following command:
@@ -79,6 +80,7 @@ This library offers additional implementations of the [IFileProvider](https://le
- `SubFileProvider`
- `PrefixedFileProvider`
+- `ZipFileProvider`
#### PrefixedFileProvider
@@ -148,6 +150,17 @@ IFileInfo file = provider.GetFileInfo("/README");
Console.WriteLine(file.Exists);
```
+#### ZipFileProvider
+
+`ZipFileProvider` enables access to files within ZIP archives as if they were part of the file system.
+
+Example:
+```csharp
+IFileProvider provider = new ZipFileProvider("/path/to/archive.zip");
+foreach (IFileInfo file in provider.GetDirectoryContents("/"))
+ Console.WriteLine(file.Name);
+```
+
### Ramstack.FileProviders.Globbing
`GlobbingFileProvider` class filters files using include and/or exclude glob patterns. Include patterns make only matching files visible,
@@ -277,15 +290,15 @@ var changeToken = compositeFileProvider.Watch("**/*.json").Flatten();
## NuGet Packages
- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes.
-- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`.
+- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control.
- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`.
## Supported versions
-| | Version |
-|------|----------------|
-| .NET | 6, 7, 8, 9, 10 |
+| | Version |
+|------|------------|
+| .NET | 6, 7, 8, 9 |
## Contributions
diff --git a/src/Ramstack.FileProviders.Composition/README.md b/src/Ramstack.FileProviders.Composition/README.md
index 542469b..dab4ca0 100644
--- a/src/Ramstack.FileProviders.Composition/README.md
+++ b/src/Ramstack.FileProviders.Composition/README.md
@@ -1,6 +1,4 @@
# Ramstack.FileProviders.Composition
-[](https://nuget.org/packages/Ramstack.FileProviders.Composition)
-[](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE)
Represents a .NET library that provides a helper class for flattening and composing `IFileProvider` instances.
@@ -65,14 +63,14 @@ var changeToken = compositeFileProvider.Watch("**/*.json").Flatten();
## Related Packages
- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes.
-- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`.
+- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control.
## Supported versions
-| | Version |
-|------|----------------|
-| .NET | 6, 7, 8, 9, 10 |
+| | Version |
+|------|------------|
+| .NET | 6, 7, 8, 9 |
## Contributions
diff --git a/src/Ramstack.FileProviders.Extensions/README.md b/src/Ramstack.FileProviders.Extensions/README.md
index 37a2f27..5e87a92 100644
--- a/src/Ramstack.FileProviders.Extensions/README.md
+++ b/src/Ramstack.FileProviders.Extensions/README.md
@@ -1,6 +1,4 @@
# Ramstack.FileProviders.Extensions
-[](https://nuget.org/packages/Ramstack.FileProviders.Extensions)
-[](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE)
Represents a lightweight .NET library of useful and convenient extensions for `Microsoft.Extensions.FileProviders`
that enhances file handling capabilities in .NET applications.
@@ -68,16 +66,16 @@ foreach (FileNode file in provider.EnumerateFiles("/project", pattern: "**/*.md"
```
## Related Packages
-- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`.
+- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control.
- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`.
## Supported versions
-| | Version |
-|------|----------------|
-| .NET | 6, 7, 8, 9, 10 |
+| | Version |
+|------|------------|
+| .NET | 6, 7, 8, 9 |
## Contributions
diff --git a/src/Ramstack.FileProviders.Globbing/README.md b/src/Ramstack.FileProviders.Globbing/README.md
index 4e4896b..635cfbe 100644
--- a/src/Ramstack.FileProviders.Globbing/README.md
+++ b/src/Ramstack.FileProviders.Globbing/README.md
@@ -1,6 +1,4 @@
# Ramstack.FileProviders.Globbing
-[](https://nuget.org/packages/Ramstack.FileProviders.Globbing)
-[](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE)
Represents a .NET library implementing an `IFileProvider` that filters files using include and/or exclude glob patterns
for flexible file visibility control.
@@ -28,15 +26,15 @@ foreach (IFileInfo file in provider.GetDirectoryContents("/"))
## Related Packages
- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes.
-- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`.
+- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `ZipFileProvider`, `PrefixedFileProvider`, and `SubFileProvider`.
- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`.
## Supported versions
-| | Version |
-|------|----------------|
-| .NET | 6, 7, 8, 9, 10 |
+| | Version |
+|------|---------|
+| .NET | 6, 7, 8 |
## Contributions
diff --git a/src/Ramstack.FileProviders/README.md b/src/Ramstack.FileProviders/README.md
index 5118256..00e86db 100644
--- a/src/Ramstack.FileProviders/README.md
+++ b/src/Ramstack.FileProviders/README.md
@@ -1,10 +1,9 @@
# Ramstack.FileProviders
-[](https://nuget.org/packages/Ramstack.FileProviders)
-[](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE)
Represents a .NET library that provides additional implementations for `Microsoft.Extensions.FileProviders` including:
- `PrefixedFileProvider`
- `SubFileProvider`
+- `ZipFileProvider`
## Getting Started
@@ -82,6 +81,16 @@ IFileInfo file = provider.GetFileInfo("/README");
Console.WriteLine(file.Exists);
```
+## ZipFileProvider
+`ZipFileProvider` enables access to files within ZIP archives as if they were part of the file system.
+
+Example:
+```csharp
+IFileProvider provider = new ZipFileProvider("/path/to/archive.zip");
+foreach (IFileInfo file in provider.GetDirectoryContents("/"))
+ Console.WriteLine(file.Name);
+```
+
## Related Packages
- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes.
- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control.
@@ -89,9 +98,9 @@ Console.WriteLine(file.Exists);
## Supported versions
-| | Version |
-|------|----------------|
-| .NET | 6, 7, 8, 9, 10 |
+| | Version |
+|------|------------|
+| .NET | 6, 7, 8, 9 |
## Contributions
diff --git a/src/Ramstack.FileProviders/ZipFileProvider.cs b/src/Ramstack.FileProviders/ZipFileProvider.cs
new file mode 100644
index 0000000..c21425f
--- /dev/null
+++ b/src/Ramstack.FileProviders/ZipFileProvider.cs
@@ -0,0 +1,272 @@
+using System.IO.Compression;
+using System.Runtime.CompilerServices;
+
+namespace Ramstack.FileProviders;
+
+///
+/// Provides access to files within a ZIP archive.
+///
+///
+/// **WARNING:**
+///
+/// This class is not thread-safe and is designed to handle only one file read operation at a time.
+/// It does not support parallel or simultaneous opening of multiple files.
+///
+///
+[Obsolete("Deprecated due to thread safety limitations and parallel file access capabilities.")]
+public sealed class ZipFileProvider : IFileProvider, IDisposable
+{
+ private readonly ZipArchive _archive;
+ private readonly Dictionary _cache =
+ new() { ["/"] = new ZipDirectoryInfo("/") };
+
+ ///
+ /// Initializes a new instance of the class
+ /// using a ZIP archive located at the specified file path.
+ ///
+ /// The path to the ZIP archive file.
+ public ZipFileProvider(string path)
+ : this(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// using a stream containing a ZIP archive.
+ ///
+ /// The stream containing the ZIP archive.
+ /// to leave the stream open
+ /// after the object is disposed; otherwise, .
+ public ZipFileProvider(Stream stream, bool leaveOpen = false)
+ {
+ if (!stream.CanSeek)
+ throw new ArgumentException("Stream does not support seeking.", nameof(stream));
+
+ _archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen);
+ Initialize(_archive, _cache);
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// using an existing .
+ ///
+ /// The instance
+ /// to use for providing access to ZIP archive content.
+ public ZipFileProvider(ZipArchive archive)
+ {
+ if (archive.Mode != ZipArchiveMode.Read)
+ throw new ArgumentException(
+ "Archive must be opened in read mode (ZipArchiveMode.Read).",
+ nameof(archive));
+
+ _archive = archive;
+ Initialize(archive, _cache);
+ }
+
+ ///
+ public IFileInfo GetFileInfo(string subpath) =>
+ Find(subpath) ?? new NotFoundFileInfo(FilePath.GetFileName(subpath));
+
+ ///
+ public IDirectoryContents GetDirectoryContents(string subpath) =>
+ Find(subpath) as IDirectoryContents ?? NotFoundDirectoryContents.Singleton;
+
+ ///
+ public IChangeToken Watch(string filter) =>
+ NullChangeToken.Singleton;
+
+ ///
+ public void Dispose() =>
+ _archive.Dispose();
+
+ private IFileInfo? Find(string path) =>
+ _cache.GetValueOrDefault(FilePath.Normalize(path));
+
+ ///
+ /// Initializes the current provider by populating it with entries from the underlying ZIP archive.
+ ///
+ private static void Initialize(ZipArchive archive, Dictionary cache)
+ {
+ foreach (var entry in archive.Entries)
+ {
+ //
+ // Strip common path prefixes from zip entries to handle archives
+ // saved with absolute paths.
+ //
+ var path = FilePath.Normalize(
+ entry.FullName[GetPrefixLength(entry.FullName)..]);
+
+ if (FilePath.HasTrailingSlash(entry.FullName))
+ {
+ GetDirectory(path);
+ continue;
+ }
+
+ var directory = GetDirectory(FilePath.GetDirectoryName(path));
+ var file = new ZipFileInfo(FilePath.GetFileName(path), entry);
+
+ //
+ // Archives legitimately may contain entries with identical names,
+ // so skip if a file with this name has already been added,
+ // avoiding duplicates in the directory file list.
+ //
+ if (cache.TryAdd(path, file))
+ directory.RegisterFile(file);
+ }
+
+ ZipDirectoryInfo GetDirectory(string path)
+ {
+ if (cache.TryGetValue(path, out var di))
+ return (ZipDirectoryInfo)di;
+
+ di = new ZipDirectoryInfo(FilePath.GetFileName(path));
+ var parent = GetDirectory(FilePath.GetDirectoryName(path));
+ parent.RegisterFile(di);
+ cache.Add(path, di);
+
+ return (ZipDirectoryInfo)di;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static int GetPrefixLength(string path)
+ {
+ //
+ // Check only well-known prefixes.
+ // Note: Since entry names can be arbitrary,
+ // we specifically target only common absolute path patterns.
+ //
+
+ if (path.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase)
+ || path.StartsWith(@"\\.\UNC\", StringComparison.OrdinalIgnoreCase)
+ || path.StartsWith("//?/UNC/", StringComparison.OrdinalIgnoreCase)
+ || path.StartsWith("//./UNC/", StringComparison.OrdinalIgnoreCase))
+ return 8;
+
+ if (path.StartsWith(@"\\?\", StringComparison.Ordinal)
+ || path.StartsWith(@"\\.\", StringComparison.Ordinal)
+ || path.StartsWith("//?/", StringComparison.Ordinal)
+ || path.StartsWith("//./", StringComparison.Ordinal))
+ return path.Length >= 6 && IsAsciiLetter(path[4]) && path[5] == ':' ? 6 : 4;
+
+ if (path.Length >= 2
+ && IsAsciiLetter(path[0]) && path[1] == ':')
+ return 2;
+
+ return 0;
+
+ static bool IsAsciiLetter(char ch) =>
+ (uint)((ch | 0x20) - 'a') <= 'z' - 'a';
+ }
+
+ #region Inner type: ZipDirectoryInfo
+
+ ///
+ /// Represents directory contents and file information within a ZIP archive for the specified path.
+ /// This class is used to provide both and interfaces for directory entries in the ZIP archive.
+ ///
+ /// The name of the directory, not including any path.
+ [DebuggerDisplay("{Name,nq}")]
+ [DebuggerTypeProxy(typeof(ZipDirectoryInfoDebuggerProxy))]
+ private sealed class ZipDirectoryInfo(string name) : IDirectoryContents, IFileInfo
+ {
+ ///
+ /// The list of the within this directory.
+ ///
+ private readonly List _files = [];
+
+ ///
+ public bool Exists => true;
+
+ ///
+ public long Length => -1;
+
+ ///
+ public string? PhysicalPath => null;
+
+ ///
+ public string Name => name;
+
+ ///
+ public DateTimeOffset LastModified => default;
+
+ ///
+ public bool IsDirectory => true;
+
+ ///
+ public Stream CreateReadStream() =>
+ throw new NotSupportedException("Cannot create a read stream for a directory.");
+
+ ///
+ public IEnumerator GetEnumerator() =>
+ _files.AsEnumerable().GetEnumerator();
+
+ ///
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+
+ ///
+ /// Register a file associated with this directory.
+ ///
+ /// The file associated with this directory.
+ public void RegisterFile(IFileInfo file) =>
+ _files.Add(file);
+ }
+
+ #endregion
+
+ #region Inner type: ZipFileInfo
+
+ ///
+ /// Represents a file within a ZIP archive as an implementation of the interface.
+ ///
+ /// The name of the file, not including any path.
+ /// The ZIP archive entry representing the file.
+ [DebuggerDisplay("{ToStringDebugger(),nq}")]
+ private sealed class ZipFileInfo(string name, ZipArchiveEntry entry) : IFileInfo
+ {
+ ///
+ public bool Exists => true;
+
+ ///
+ public bool IsDirectory => false;
+
+ ///
+ public DateTimeOffset LastModified => entry.LastWriteTime;
+
+ ///
+ public long Length => entry.Length;
+
+ ///
+ public string? PhysicalPath => null;
+
+ ///
+ public string Name => name;
+
+ ///
+ public Stream CreateReadStream() =>
+ entry.Open();
+
+ private string ToStringDebugger() =>
+ entry.FullName;
+ }
+
+ #endregion
+
+ #region Inner type: ZipDirectoryInfoDebuggerProxy
+
+ ///
+ /// Represents a debugger proxy for viewing the contents of a instance in a more user-friendly way during debugging.
+ ///
+ /// The instance to provide debugging information for.
+ private sealed class ZipDirectoryInfoDebuggerProxy(ZipDirectoryInfo directoryInfo)
+ {
+ ///
+ /// Gets an array of instances representing the files within the associated .
+ ///
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public IFileInfo[] Files { get; } = directoryInfo.ToArray();
+ }
+
+ #endregion
+}
diff --git a/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs
new file mode 100644
index 0000000..4a3bfa6
--- /dev/null
+++ b/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs
@@ -0,0 +1,193 @@
+using System.IO.Compression;
+
+using Ramstack.FileProviders.Utilities;
+
+namespace Ramstack.FileProviders;
+
+[TestFixture]
+public class ZipFileProviderTests : AbstractFileProviderTests
+{
+ private readonly TempFileStorage _storage = new TempFileStorage();
+ private readonly string _path =
+ Path.Combine(
+ Path.GetTempPath(),
+ Path.GetRandomFileName()
+ ) + ".zip";
+
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ ZipFile.CreateFromDirectory(
+ sourceDirectoryName: _storage.Root,
+ destinationArchiveFileName: _path,
+ compressionLevel: CompressionLevel.SmallestSize,
+ includeBaseDirectory: false);
+ }
+
+ [OneTimeTearDown]
+ public void Cleanup()
+ {
+ _storage.Dispose();
+ File.Delete(_path);
+ }
+
+ [Test]
+ public void ZipArchive_WithIdenticalNameEntries()
+ {
+ using var provider = new ZipFileProvider(CreateArchive());
+
+ var list = provider
+ .EnumerateFiles("/1")
+ .ToArray();
+
+ Assert.That(
+ list.Length,
+ Is.EqualTo(1));
+
+ Assert.That(
+ list[0].ReadAllBytes(),
+ Is.EquivalentTo("Hello, World!"u8.ToArray()));
+
+ static MemoryStream CreateArchive()
+ {
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ var a = archive.CreateEntry("1/text.txt");
+ using (var writer = a.Open())
+ writer.Write("Hello, World!"u8);
+
+ archive.CreateEntry("1/text.txt");
+ archive.CreateEntry(@"1\text.txt");
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+ }
+
+ [Test]
+ public void ZipArchive_PrefixedEntries()
+ {
+ var archive = new ZipArchive(CreateArchive(), ZipArchiveMode.Read, leaveOpen: true);
+ using var provider = new ZipFileProvider(archive);
+
+ var directories = provider
+ .EnumerateDirectories("/", "**")
+ .Select(f =>
+ f.FullName)
+ .OrderBy(f => f)
+ .ToArray();
+
+ var files = provider
+ .EnumerateFiles("/", "**")
+ .Select(f =>
+ f.FullName)
+ .OrderBy(f => f)
+ .ToArray();
+
+ Assert.That(files, Is.EquivalentTo(
+ [
+ "/1/text.txt",
+ "/2/text.txt",
+ "/3/text.txt",
+ "/4/text.txt",
+ "/5/text.txt",
+ "/localhost/backup/text.txt",
+ "/localhost/share/text.txt",
+ "/server/backup/text.txt",
+ "/server/share/text.txt",
+ "/text.txt",
+ "/text.xml"
+ ]));
+
+ Assert.That(directories, Is.EquivalentTo(
+ [
+ "/1",
+ "/2",
+ "/3",
+ "/4",
+ "/5",
+ "/localhost",
+ "/localhost/backup",
+ "/localhost/share",
+ "/server",
+ "/server/backup",
+ "/server/share"
+ ]));
+
+ static MemoryStream CreateArchive()
+ {
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ archive.CreateEntry(@"D:\1/text.txt");
+ archive.CreateEntry(@"D:2\text.txt");
+
+ archive.CreateEntry(@"\\?\D:\text.txt");
+ archive.CreateEntry(@"\\?\D:text.xml");
+ archive.CreateEntry(@"\\.\D:\3\text.txt");
+ archive.CreateEntry(@"//?/D:/4\text.txt");
+ archive.CreateEntry(@"//./D:\5/text.txt");
+
+ archive.CreateEntry(@"\\?\UNC\localhost\share\text.txt");
+ archive.CreateEntry(@"\\.\unc\server\share\text.txt");
+ archive.CreateEntry(@"//?/UNC/localhost/backup\text.txt");
+ archive.CreateEntry(@"//./unc/server/backup\text.txt");
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+ }
+
+ [Test]
+ public void ZipArchive_Directories()
+ {
+ using var provider = new ZipFileProvider(CreateArchive());
+
+ var directories = provider
+ .EnumerateDirectories("/", "**")
+ .Select(f =>
+ f.FullName)
+ .OrderBy(f => f)
+ .ToArray();
+
+ Assert.That(directories, Is.EquivalentTo(
+ [
+ "/1",
+ "/2",
+ "/2/3",
+ "/4",
+ "/4/5",
+ "/4/5/6"
+ ]));
+
+ static MemoryStream CreateArchive()
+ {
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ archive.CreateEntry(@"\1/");
+ archive.CreateEntry(@"\2/");
+ archive.CreateEntry(@"/2\");
+ archive.CreateEntry(@"/2\");
+ archive.CreateEntry(@"/2\");
+ archive.CreateEntry(@"/2\3/");
+ archive.CreateEntry(@"/2\3/");
+ archive.CreateEntry(@"/2\3/");
+ archive.CreateEntry(@"4\5/6\");
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+ }
+
+ protected override IFileProvider GetFileProvider() =>
+ new ZipFileProvider(_path);
+
+ protected override DirectoryInfo GetDirectoryInfo() =>
+ new DirectoryInfo(_storage.Root);
+
+}