-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathZipFileSystem.cs
More file actions
174 lines (148 loc) · 6.47 KB
/
ZipFileSystem.cs
File metadata and controls
174 lines (148 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
using System.IO.Compression;
using System.Runtime.CompilerServices;
using Ramstack.FileSystem.Null;
namespace Ramstack.FileSystem.Zip;
/// <summary>
/// Represents a file system backed by a ZIP archive.
/// </summary>
/// <remarks>
/// <b>WARNING:</b>
/// <para>
/// The <see cref="ZipFileSystem"/> is not thread-safe and allows reading only one file at a time, as it relies on
/// <see cref="ZipArchive"/>, which does not support parallel read operations or simultaneous opening of multiple streams.
/// </para>
/// <para>
/// You may use this class only if you can guarantee that:
/// <list type="bullet">
/// <item><description>Only one file is open for reading at a time.</description></item>
/// <item><description>No file is accessed concurrently.</description></item>
/// </list>
/// </para>
/// </remarks>
[Obsolete("Deprecated due to thread safety limitations and parallel file access capabilities.")]
public sealed class ZipFileSystem : IVirtualFileSystem
{
private readonly ZipArchive _archive;
private readonly Dictionary<string, VirtualNode> _directories;
/// <inheritdoc />
public bool IsReadOnly => true;
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileSystem"/> class
/// using a ZIP archive located at the specified file path.
/// </summary>
/// <param name="path">The path to the ZIP archive file.</param>
public ZipFileSystem(string path)
: this(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileSystem"/> class
/// using a stream containing a ZIP archive.
/// </summary>
/// <param name="stream">The stream containing the ZIP archive.</param>
/// <param name="leaveOpen"><see langword="true" /> to leave the stream open
/// after the <see cref="ZipFileSystem"/> object is disposed; otherwise, <see langword="false" />.</param>
public ZipFileSystem(Stream stream, bool leaveOpen = false)
: this(new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZipFileSystem"/> class
/// using an existing <see cref="ZipArchive"/>.
/// </summary>
/// <param name="archive">The <see cref="ZipArchive"/> instance
/// to use for providing access to ZIP archive content.</param>
public ZipFileSystem(ZipArchive archive)
{
_archive = archive;
_directories = new Dictionary<string, VirtualNode>
{
["/"] = new ZipDirectory(this, "/")
};
Initialize(archive, _directories);
}
/// <inheritdoc />
public VirtualFile GetFile(string path) =>
FindNode(path) as VirtualFile ?? new NotFoundFile(this, path);
/// <inheritdoc />
public VirtualDirectory GetDirectory(string path) =>
FindNode(path) as VirtualDirectory ?? new NotFoundDirectory(this, path);
/// <inheritdoc />
public void Dispose() =>
_archive.Dispose();
/// <summary>
/// Finds a <see cref="VirtualNode"/> by its path.
/// </summary>
/// <param name="path">The path of the node to find.</param>
/// <returns>
/// The <see cref="VirtualNode"/> if found; otherwise, <see langword="null"/>.
/// </returns>
private VirtualNode? FindNode(string path) =>
_directories.GetValueOrDefault(VirtualPath.Normalize(path));
/// <summary>
/// Initializes the file system with entries from the specified ZIP archive.
/// </summary>
/// <param name="archive">The ZIP archive to read entries from.</param>
/// <param name="cache">A dictionary to cache the directory and file nodes.</param>
private void Initialize(ZipArchive archive, Dictionary<string, VirtualNode> cache)
{
foreach (var entry in archive.Entries)
{
//
// Strip common path prefixes from zip entries to handle archives
// saved with absolute paths.
//
var path = VirtualPath.Normalize(
entry.FullName[GetPrefixLength(entry.FullName)..]);
if (VirtualPath.HasTrailingSlash(entry.FullName))
{
GetOrCreateDirectory(path);
continue;
}
var directory = GetOrCreateDirectory(VirtualPath.GetDirectoryName(path));
var file = new ZipFile(this, 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.RegisterNode(file);
}
ZipDirectory GetOrCreateDirectory(string path)
{
if (cache.TryGetValue(path, out var di))
return (ZipDirectory)di;
di = new ZipDirectory(this, path);
var parent = GetOrCreateDirectory(VirtualPath.GetDirectoryName(path));
parent.RegisterNode(di);
cache.Add(path, di);
return (ZipDirectory)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';
}
}