Skip to content

Commit bdc71bb

Browse files
authored
Merge pull request #44 from rameel/zipfs-final-fixes
Final improvements and bug fixes to ZipFileSystem
2 parents f90eca9 + 4c66aa4 commit bdc71bb

File tree

4 files changed

+207
-9
lines changed

4 files changed

+207
-9
lines changed

src/Ramstack.FileSystem.Zip/ZipDirectory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Ramstack.FileSystem.Zip;
77
/// <summary>
88
/// Represents directory contents and file information within a ZIP archive for the specified path.
99
/// </summary>
10+
[Obsolete]
1011
[DebuggerTypeProxy(typeof(ZipDirectoryDebuggerProxy))]
1112
internal sealed class ZipDirectory : VirtualDirectory
1213
{

src/Ramstack.FileSystem.Zip/ZipFile.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace Ramstack.FileSystem.Zip;
55
/// <summary>
66
/// Represents a file within a ZIP archive.
77
/// </summary>
8+
[Obsolete]
89
internal sealed class ZipFile : VirtualFile
910
{
1011
private readonly ZipFileSystem _fileSystem;

src/Ramstack.FileSystem.Zip/ZipFileSystem.cs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.IO.Compression;
2+
using System.Runtime.CompilerServices;
23

34
using Ramstack.FileSystem.Null;
45

@@ -100,22 +101,29 @@ private void Initialize(ZipArchive archive, Dictionary<string, VirtualNode> cach
100101
{
101102
foreach (var entry in archive.Entries)
102103
{
103-
// Skipping directories
104-
// --------------------
105-
// Directory entries are denoted by a trailing slash '/' in their names.
106104
//
107-
// Since we can't rely on all archivers to include directory entries in archives,
108-
// it's simpler to assume their absence and ignore any entries ending with a forward slash '/'.
105+
// Strip common path prefixes from zip entries to handle archives
106+
// saved with absolute paths.
107+
//
108+
var path = VirtualPath.Normalize(
109+
entry.FullName[GetPrefixLength(entry.FullName)..]);
109110

110-
if (entry.FullName.EndsWith('/'))
111+
if (VirtualPath.HasTrailingSlash(entry.FullName))
112+
{
113+
GetOrCreateDirectory(path);
111114
continue;
115+
}
112116

113-
var path = VirtualPath.Normalize(entry.FullName);
114117
var directory = GetOrCreateDirectory(VirtualPath.GetDirectoryName(path));
115118
var file = new ZipFile(this, path, entry);
116119

117-
directory.RegisterNode(file);
118-
cache.Add(path, file);
120+
//
121+
// Archives legitimately may contain entries with identical names,
122+
// so skip if a file with this name has already been added,
123+
// avoiding duplicates in the directory file list.
124+
//
125+
if (cache.TryAdd(path, file))
126+
directory.RegisterNode(file);
119127
}
120128

121129
ZipDirectory GetOrCreateDirectory(string path)
@@ -131,4 +139,36 @@ ZipDirectory GetOrCreateDirectory(string path)
131139
return (ZipDirectory)di;
132140
}
133141
}
142+
143+
[MethodImpl(MethodImplOptions.NoInlining)]
144+
private static int GetPrefixLength(string path)
145+
{
146+
//
147+
// Check only well-known prefixes.
148+
// Note: Since entry names can be arbitrary,
149+
// we specifically target only common absolute path patterns.
150+
//
151+
152+
if (path.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase)
153+
|| path.StartsWith(@"\\.\UNC\", StringComparison.OrdinalIgnoreCase)
154+
|| path.StartsWith("//?/UNC/", StringComparison.OrdinalIgnoreCase)
155+
|| path.StartsWith("//./UNC/", StringComparison.OrdinalIgnoreCase))
156+
return 8;
157+
158+
if (path.StartsWith(@"\\?\", StringComparison.Ordinal)
159+
|| path.StartsWith(@"\\.\", StringComparison.Ordinal)
160+
|| path.StartsWith("//?/", StringComparison.Ordinal)
161+
|| path.StartsWith("//./", StringComparison.Ordinal))
162+
return path.Length >= 6 && IsAsciiLetter(path[4]) && path[5] == ':' ? 6 : 4;
163+
164+
if (path.Length >= 2
165+
&& IsAsciiLetter(path[0]) && path[1] == ':')
166+
return 2;
167+
168+
return 0;
169+
170+
static bool IsAsciiLetter(char ch) =>
171+
(uint)((ch | 0x20) - 'a') <= 'z' - 'a';
172+
}
173+
134174
}

tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemTests.cs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using Ramstack.FileSystem.Specification.Tests;
44
using Ramstack.FileSystem.Specification.Tests.Utilities;
55

6+
#pragma warning disable CS0618 // Type or member is obsolete
7+
68
namespace Ramstack.FileSystem.Zip;
79

810
[TestFixture]
@@ -24,6 +26,160 @@ public void Cleanup()
2426
File.Delete(_path);
2527
}
2628

29+
[Test]
30+
public async Task ZipArchive_WithIdenticalNameEntries()
31+
{
32+
using var fs = new ZipFileSystem(CreateArchive());
33+
34+
var list = await fs
35+
.GetFilesAsync("/1")
36+
.ToArrayAsync();
37+
38+
Assert.That(
39+
list.Length,
40+
Is.EqualTo(1));
41+
42+
Assert.That(
43+
await list[0].ReadAllBytesAsync(),
44+
Is.EquivalentTo("Hello, World!"u8.ToArray()));
45+
46+
static MemoryStream CreateArchive()
47+
{
48+
var stream = new MemoryStream();
49+
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
50+
{
51+
var a = archive.CreateEntry("1/text.txt");
52+
using (var writer = a.Open())
53+
writer.Write("Hello, World!"u8);
54+
55+
archive.CreateEntry("1/text.txt");
56+
archive.CreateEntry(@"1\text.txt");
57+
}
58+
59+
stream.Position = 0;
60+
return stream;
61+
}
62+
}
63+
64+
[Test]
65+
public async Task ZipArchive_PrefixedEntries()
66+
{
67+
var archive = new ZipArchive(CreateArchive(), ZipArchiveMode.Read, leaveOpen: true);
68+
using var fs = new ZipFileSystem(archive);
69+
70+
var directories = await fs
71+
.GetDirectoriesAsync("/", "**")
72+
.Select(f =>
73+
f.FullName)
74+
.OrderBy(f => f)
75+
.ToArrayAsync();
76+
77+
var files = await fs
78+
.GetFilesAsync("/", "**")
79+
.Select(f =>
80+
f.FullName)
81+
.OrderBy(f => f)
82+
.ToArrayAsync();
83+
84+
Assert.That(files, Is.EquivalentTo(
85+
[
86+
"/1/text.txt",
87+
"/2/text.txt",
88+
"/3/text.txt",
89+
"/4/text.txt",
90+
"/5/text.txt",
91+
"/localhost/backup/text.txt",
92+
"/localhost/share/text.txt",
93+
"/server/backup/text.txt",
94+
"/server/share/text.txt",
95+
"/text.txt",
96+
"/text.xml"
97+
]));
98+
99+
Assert.That(directories, Is.EquivalentTo(
100+
[
101+
"/1",
102+
"/2",
103+
"/3",
104+
"/4",
105+
"/5",
106+
"/localhost",
107+
"/localhost/backup",
108+
"/localhost/share",
109+
"/server",
110+
"/server/backup",
111+
"/server/share"
112+
]));
113+
114+
static MemoryStream CreateArchive()
115+
{
116+
var stream = new MemoryStream();
117+
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
118+
{
119+
archive.CreateEntry(@"D:\1/text.txt");
120+
archive.CreateEntry(@"D:2\text.txt");
121+
122+
archive.CreateEntry(@"\\?\D:\text.txt");
123+
archive.CreateEntry(@"\\?\D:text.xml");
124+
archive.CreateEntry(@"\\.\D:\3\text.txt");
125+
archive.CreateEntry(@"//?/D:/4\text.txt");
126+
archive.CreateEntry(@"//./D:\5/text.txt");
127+
128+
archive.CreateEntry(@"\\?\UNC\localhost\share\text.txt");
129+
archive.CreateEntry(@"\\.\unc\server\share\text.txt");
130+
archive.CreateEntry(@"//?/UNC/localhost/backup\text.txt");
131+
archive.CreateEntry(@"//./unc/server/backup\text.txt");
132+
}
133+
134+
stream.Position = 0;
135+
return stream;
136+
}
137+
}
138+
139+
[Test]
140+
public async Task ZipArchive_Directories()
141+
{
142+
using var fs = new ZipFileSystem(CreateArchive());
143+
144+
var directories = await fs
145+
.GetDirectoriesAsync("/", "**")
146+
.Select(f =>
147+
f.FullName)
148+
.OrderBy(f => f)
149+
.ToArrayAsync();
150+
151+
Assert.That(directories, Is.EquivalentTo(
152+
[
153+
"/1",
154+
"/2",
155+
"/2/3",
156+
"/4",
157+
"/4/5",
158+
"/4/5/6"
159+
]));
160+
161+
static MemoryStream CreateArchive()
162+
{
163+
var stream = new MemoryStream();
164+
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
165+
{
166+
archive.CreateEntry(@"\1/");
167+
archive.CreateEntry(@"\2/");
168+
archive.CreateEntry(@"/2\");
169+
archive.CreateEntry(@"/2\");
170+
archive.CreateEntry(@"/2\");
171+
archive.CreateEntry(@"/2\3/");
172+
archive.CreateEntry(@"/2\3/");
173+
archive.CreateEntry(@"/2\3/");
174+
archive.CreateEntry(@"4\5/6\");
175+
}
176+
177+
stream.Position = 0;
178+
return stream;
179+
}
180+
}
181+
182+
27183
/// <inheritdoc />
28184
protected override IVirtualFileSystem GetFileSystem() =>
29185
new ZipFileSystem(_path);

0 commit comments

Comments
 (0)