diff --git a/src/Zio.Tests/FileSystems/TestZipArchiveFileSystem.cs b/src/Zio.Tests/FileSystems/TestZipArchiveFileSystem.cs index 7888cbf..57a10a0 100644 --- a/src/Zio.Tests/FileSystems/TestZipArchiveFileSystem.cs +++ b/src/Zio.Tests/FileSystems/TestZipArchiveFileSystem.cs @@ -228,4 +228,105 @@ public void TestOpenStreamsMultithreaded() thread1.Join(); thread2.Join(); } + + + [Theory] + [InlineData("TestData/Linux.zip")] + [InlineData("TestData/Windows.zip")] + public void TestCaseInSensitiveZip(string path) + { + using var stream = File.OpenRead(path); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + var fs = new ZipArchiveFileSystem(archive); + + Assert.True(fs.DirectoryExists("/Folder")); + Assert.True(fs.DirectoryExists("/folder")); + + Assert.False(fs.FileExists("/Folder")); + Assert.False(fs.FileExists("/folder")); + + Assert.True(fs.FileExists("/Folder/File.txt")); + Assert.True(fs.FileExists("/folder/file.txt")); + + Assert.False(fs.DirectoryExists("/Folder/file.txt")); + Assert.False(fs.DirectoryExists("/folder/File.txt")); + } + + [Theory] + [InlineData("TestData/Linux.zip")] + [InlineData("TestData/Windows.zip")] + public void TestCaseSensitiveZip(string path) + { + using var stream = File.OpenRead(path); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + var fs = new ZipArchiveFileSystem(archive, true); + + Assert.True(fs.DirectoryExists("/Folder")); + Assert.False(fs.DirectoryExists("/folder")); + + Assert.False(fs.FileExists("/Folder")); + Assert.False(fs.FileExists("/folder")); + + Assert.True(fs.FileExists("/Folder/File.txt")); + Assert.False(fs.FileExists("/folder/file.txt")); + + Assert.False(fs.DirectoryExists("/Folder/file.txt")); + Assert.False(fs.DirectoryExists("/folder/File.txt")); + } + + [Fact] + public void TestSaveStream() + { + var stream = new MemoryStream(); + + using var fs = new ZipArchiveFileSystem(stream); + + fs.WriteAllText("/a/b.txt", "abc"); + fs.Save(); + + stream.Seek(0, SeekOrigin.Begin); + + using (var fs2 = new ZipArchiveFileSystem(stream, ZipArchiveMode.Read, leaveOpen: true)) + { + Assert.Equal("abc", fs2.ReadAllText("/a/b.txt")); + } + + Assert.Equal("abc", fs.ReadAllText("/a/b.txt")); + fs.WriteAllText("/a/b.txt", "def"); + fs.Save(); + + stream.Seek(0, SeekOrigin.Begin); + + using (var fs2 = new ZipArchiveFileSystem(stream, ZipArchiveMode.Read, leaveOpen: true)) + { + Assert.Equal("def", fs2.ReadAllText("/a/b.txt")); + } + } + + [Fact] + public void TestSaveFile() + { + var path = Path.Combine(SystemPath, Guid.NewGuid().ToString("N") + ".zip"); + + try + { + using var fs = new ZipArchiveFileSystem(path); + + Assert.Equal(0, new FileInfo(path).Length); + + fs.WriteAllText("/a/b.txt", "abc"); + fs.Save(); + + // We cannot check the content because the file is still open + Assert.NotEqual(0, new FileInfo(path).Length); + + // Ensure we can save multiple times + fs.WriteAllText("/a/b.txt", "def"); + fs.Save(); + } + finally + { + File.Delete(path); + } + } } \ No newline at end of file diff --git a/src/Zio.Tests/TestData/Linux.zip b/src/Zio.Tests/TestData/Linux.zip new file mode 100644 index 0000000..bc85ddb Binary files /dev/null and b/src/Zio.Tests/TestData/Linux.zip differ diff --git a/src/Zio.Tests/TestData/Windows.zip b/src/Zio.Tests/TestData/Windows.zip new file mode 100644 index 0000000..73db649 Binary files /dev/null and b/src/Zio.Tests/TestData/Windows.zip differ diff --git a/src/Zio.Tests/Zio.Tests.csproj b/src/Zio.Tests/Zio.Tests.csproj index 194790e..686b35f 100644 --- a/src/Zio.Tests/Zio.Tests.csproj +++ b/src/Zio.Tests/Zio.Tests.csproj @@ -20,4 +20,10 @@ + + + + Always + + diff --git a/src/Zio/FileSystems/FileSystemWatcher.cs b/src/Zio/FileSystems/FileSystemWatcher.cs index 08e5832..d12a272 100644 --- a/src/Zio/FileSystems/FileSystemWatcher.cs +++ b/src/Zio/FileSystems/FileSystemWatcher.cs @@ -223,7 +223,7 @@ protected void UnregisterEvents(IFileSystemWatcher watcher) return pathFromEvent; } - private void OnChanged(object sender, FileChangedEventArgs args) + private void OnChanged(object? sender, FileChangedEventArgs args) { var newPath = TryConvertPath(args.FullPath); if (!newPath.HasValue) @@ -235,7 +235,7 @@ private void OnChanged(object sender, FileChangedEventArgs args) RaiseChanged(newArgs); } - private void OnCreated(object sender, FileChangedEventArgs args) + private void OnCreated(object? sender, FileChangedEventArgs args) { var newPath = TryConvertPath(args.FullPath); if (!newPath.HasValue) @@ -247,7 +247,7 @@ private void OnCreated(object sender, FileChangedEventArgs args) RaiseCreated(newArgs); } - private void OnDeleted(object sender, FileChangedEventArgs args) + private void OnDeleted(object? sender, FileChangedEventArgs args) { var newPath = TryConvertPath(args.FullPath); if (!newPath.HasValue) @@ -259,12 +259,12 @@ private void OnDeleted(object sender, FileChangedEventArgs args) RaiseDeleted(newArgs); } - private void OnError(object sender, FileSystemErrorEventArgs args) + private void OnError(object? sender, FileSystemErrorEventArgs args) { RaiseError(args); } - private void OnRenamed(object sender, FileRenamedEventArgs args) + private void OnRenamed(object? sender, FileRenamedEventArgs args) { var newPath = TryConvertPath(args.FullPath); if (!newPath.HasValue) diff --git a/src/Zio/FileSystems/MemoryFileSystem.cs b/src/Zio/FileSystems/MemoryFileSystem.cs index 8180c6c..83e96cc 100644 --- a/src/Zio/FileSystems/MemoryFileSystem.cs +++ b/src/Zio/FileSystems/MemoryFileSystem.cs @@ -189,7 +189,7 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) } finally { - if (deleteRootDirectory) + if (deleteRootDirectory && result.Node != null) { result.Node.DetachFromParent(); result.Node.Dispose(); @@ -1008,7 +1008,7 @@ private void MoveFileOrDirectory(UPath srcPath, UPath destPath, bool expectDirec var parentSrcPath = srcPath.GetDirectory(); var parentDestPath = destPath.GetDirectory(); - void AssertNoDestination(FileSystemNode node) + void AssertNoDestination(FileSystemNode? node) { if (expectDirectory) { @@ -1140,7 +1140,7 @@ private static void ValidateFile([NotNull] FileSystemNode? node, UPath srcPath) } } - private FileSystemNode TryFindNodeSafe(UPath path) + private FileSystemNode? TryFindNodeSafe(UPath path) { EnterFileSystemShared(); try @@ -1193,7 +1193,7 @@ private void CreateDirectoryNode(UPath path) private readonly struct NodeResult { - public NodeResult(DirectoryNode? directory, FileSystemNode node, string? name, FindNodeFlags flags) + public NodeResult(DirectoryNode? directory, FileSystemNode? node, string? name, FindNodeFlags flags) { Directory = directory; Node = node; @@ -1203,7 +1203,7 @@ public NodeResult(DirectoryNode? directory, FileSystemNode node, string? name, F public readonly DirectoryNode? Directory; - public readonly FileSystemNode Node; + public readonly FileSystemNode? Node; public readonly string? Name; diff --git a/src/Zio/FileSystems/ZipArchiveFileSystem.cs b/src/Zio/FileSystems/ZipArchiveFileSystem.cs index 356ceaf..f2477ed 100644 --- a/src/Zio/FileSystems/ZipArchiveFileSystem.cs +++ b/src/Zio/FileSystems/ZipArchiveFileSystem.cs @@ -18,27 +18,26 @@ public class ZipArchiveFileSystem : FileSystem { private readonly bool _isCaseSensitive; - private readonly ZipArchive _archive; + private ZipArchive _archive; + private Dictionary _entries; + + private readonly string? _path; + private readonly Stream? _stream; + private readonly bool _disposeStream; + private readonly CompressionLevel _compressionLevel; private readonly ReaderWriterLockSlim _entriesLock = new(); private FileSystemEventDispatcher? _dispatcher; private readonly object _dispatcherLock = new(); - + private readonly DateTime _creationTime; - private readonly Dictionary _entries; - private readonly Dictionary _openStreams; private readonly object _openStreamsLock = new(); -#if NETFRAMEWORK // .Net4.5 uses a backslash as directory separator - private const char DirectorySeparator = '\\'; -#else private const char DirectorySeparator = '/'; -#endif - /// /// Initializes a new instance of the class. @@ -56,23 +55,10 @@ public ZipArchiveFileSystem(ZipArchive archive, bool isCaseSensitive = false, Co { throw new ArgumentNullException(nameof(archive)); } -#if NETFRAMEWORK // .Net4.5 uses a backslash as directory separator - foreach (var entry in _archive.Entries) - { - entry.FullName.Replace('/', DirectorySeparator); - } -#else - foreach (var entry in _archive.Entries) - { - entry.FullName.Replace('\\', DirectorySeparator); - } -#endif - if (!_isCaseSensitive) - { - _entries = _archive.Entries.ToDictionary(e => e.FullName.ToLowerInvariant(), e => e); - } _openStreams = new Dictionary(); + _entries = null!; // Loaded below + LoadEntries(); } /// @@ -83,8 +69,10 @@ public ZipArchiveFileSystem(ZipArchive archive, bool isCaseSensitive = false, Co /// True to leave the stream open when is disposed /// public ZipArchiveFileSystem(Stream stream, ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression) - : this(new ZipArchive(stream, mode, leaveOpen), isCaseSensitive, compressionLevel) + : this(new ZipArchive(stream, mode, leaveOpen: true), isCaseSensitive, compressionLevel) { + _disposeStream = !leaveOpen; + _stream = stream; } /// @@ -97,6 +85,7 @@ public ZipArchiveFileSystem(Stream stream, ZipArchiveMode mode = ZipArchiveMode. public ZipArchiveFileSystem(string path, ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression) : this(new ZipArchive(File.Open(path, FileMode.OpenOrCreate), mode, leaveOpen), isCaseSensitive, compressionLevel) { + _path = path; } /// @@ -106,40 +95,78 @@ public ZipArchiveFileSystem(string path, ZipArchiveMode mode = ZipArchiveMode.Up /// True to leave the stream open when is disposed /// Specifies if entry names should be case sensitive public ZipArchiveFileSystem(ZipArchiveMode mode = ZipArchiveMode.Update, bool leaveOpen = false, bool isCaseSensitive = false, CompressionLevel compressionLevel = CompressionLevel.NoCompression) - : this(new ZipArchive(new MemoryStream(), mode, leaveOpen), isCaseSensitive, compressionLevel) + : this(new MemoryStream(), mode, leaveOpen, isCaseSensitive, compressionLevel) { } - private ZipArchiveEntry? GetEntry(string path) + /// + /// Saves the archive to the original path or stream. + /// + /// Cannot save archive without a path or stream + public void Save() { -#if NETFRAMEWORK - path = path.Replace('/', DirectorySeparator); -#else - path = path.Replace('\\', DirectorySeparator); -#endif - if (path == null) + var mode = _archive.Mode; + + if (_path != null) + { + _archive.Dispose(); + _archive = new ZipArchive(File.Open(_path, FileMode.OpenOrCreate), mode); + } + else if (_stream != null) + { + if (!_stream.CanSeek) + { + throw new InvalidOperationException("Cannot save archive to a stream that doesn't support seeking"); + } + + _archive.Dispose(); + _stream.Seek(0, SeekOrigin.Begin); + _archive = new ZipArchive(_stream, mode, leaveOpen: true); + } + else { - throw new ArgumentNullException(nameof(path)); + throw new InvalidOperationException("Cannot save archive without a path or stream"); } - path = RemoveLeadingSlash(path); + LoadEntries(); + } + private void LoadEntries() + { + var comparer = _isCaseSensitive ? UPathComparer.Ordinal : UPathComparer.OrdinalIgnoreCase; + + _entries = _archive.Entries.ToDictionary( + e => new UPath(e.FullName).ToAbsolute(), + static e => + { + var lastChar = e.FullName[e.FullName.Length - 1]; + return new InternalZipEntry(e, lastChar is '/' or '\\'); + }, + comparer); + } + + private ZipArchiveEntry? GetEntry(UPath path, out bool isDirectory) + { _entriesLock.EnterReadLock(); try { - if (_isCaseSensitive) + if (_entries.TryGetValue(path, out var foundEntry)) { - return _archive.GetEntry(path); + isDirectory = foundEntry.IsDirectory; + return foundEntry.Entry; } - - return _entries.TryGetValue(path.ToLowerInvariant(), out var foundEntry) ? foundEntry : null; } finally { _entriesLock.ExitReadLock(); } + + isDirectory = false; + return null; } + private ZipArchiveEntry? GetEntry(UPath path) => GetEntry(path, out _); + /// protected override UPath ConvertPathFromInternalImpl(string innerPath) { @@ -160,14 +187,15 @@ protected override void CopyFileImpl(UPath srcPath, UPath destPath, bool overwri throw new IOException("Source and destination path must be different."); } - var srcEntry = GetEntry(srcPath.FullName); - if (srcEntry == null) + var srcEntry = GetEntry(srcPath, out var isDirectory); + + if (isDirectory) { - if (DirectoryExistsImpl(srcPath)) - { - throw new UnauthorizedAccessException(nameof(srcPath) + " is a directory."); - } + throw new UnauthorizedAccessException(nameof(srcPath) + " is a directory."); + } + if (srcEntry == null) + { if (!DirectoryExistsImpl(srcPath.GetDirectory())) { throw new DirectoryNotFoundException(srcPath.GetDirectory().FullName); @@ -190,7 +218,7 @@ protected override void CopyFileImpl(UPath srcPath, UPath destPath, bool overwri } } - var destEntry = GetEntry(destPath.FullName); + var destEntry = GetEntry(destPath); if (destEntry != null) { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER @@ -242,25 +270,18 @@ protected override void CreateDirectoryImpl(UPath path) } } - var entryPath = RemoveLeadingSlash(path); -#if NETFRAMEWORK - entryPath = entryPath.Replace('/', DirectorySeparator); -#else - entryPath = entryPath.Replace('\\', DirectorySeparator); -#endif - CreateEntry(ConvertPathToDirectory(entryPath)); - TryGetDispatcher()?.RaiseCreated(entryPath); + CreateEntry(path, isDirectory: true); + TryGetDispatcher()?.RaiseCreated(path); } /// protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) { - var entryPath = RemoveLeadingSlash(ConvertPathToDirectory(path)); -#if NETFRAMEWORK - entryPath = entryPath.Replace('/', DirectorySeparator); -#else - entryPath = entryPath.Replace('\\', DirectorySeparator); -#endif + if (FileExistsImpl(path)) + { + throw new IOException(nameof(path) + " is a file."); + } + var entries = new List(); if (!isRecursive) { @@ -268,7 +289,11 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) _entriesLock.EnterReadLock(); try { - entries = _archive.Entries.Where(x => x.FullName.StartsWith(entryPath, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)).Take(2).ToList(); + entries = _entries + .Where(x => x.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + .Take(2) + .Select(x => x.Value.Entry) + .ToList(); } finally { @@ -277,11 +302,6 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) if (entries.Count == 0) { - if (FileExistsImpl(path)) - { - throw new IOException($"{path} is a file"); - } - throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path); } @@ -302,15 +322,13 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) _entriesLock.EnterReadLock(); try { - entries = _archive.Entries.Where(x => x.FullName.StartsWith(entryPath, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)).ToList(); + entries = _entries + .Where(x => x.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Value.Entry) + .ToList(); + if (entries.Count == 0) { - entryPath = entryPath.Substring(0, entryPath.Length - 1); - if (_isCaseSensitive ? _archive.GetEntry(entryPath) != null : _entries.ContainsKey(entryPath.ToLowerInvariant())) - { - throw new IOException($"{path} is a file"); - } - throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path); } @@ -331,7 +349,7 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) { if ((entry.ExternalAttributes & (int)FileAttributes.ReadOnly) == (int)FileAttributes.ReadOnly) { - throw entry.FullName == entryPath + throw entry.FullName.Length == path.FullName.Length + 1 ? new IOException("Directory is read only") : new UnauthorizedAccessException($"Cannot delete directory that contains readonly entry {entry.FullName}"); } @@ -348,12 +366,8 @@ protected override void DeleteDirectoryImpl(UPath path, bool isRecursive) { foreach (var entry in entries) { + _entries.Remove(new UPath(entry.FullName).ToAbsolute()); entry.Delete(); - - if (!_isCaseSensitive) - { - _entries.Remove(entry.FullName.ToLowerInvariant()); - } } } finally @@ -372,7 +386,7 @@ protected override void DeleteFileImpl(UPath path) throw new IOException("Cannot delete a directory"); } - var entry = GetEntry(path.FullName); + var entry = GetEntry(path); if (entry == null) { return; @@ -396,7 +410,16 @@ protected override bool DirectoryExistsImpl(UPath path) return true; } - return GetEntry(ConvertPathToDirectory(path.FullName)) != null; + _entriesLock.EnterReadLock(); + + try + { + return _entries.TryGetValue(path, out var entry) && entry.IsDirectory; + } + finally + { + _entriesLock.ExitReadLock(); + } } /// @@ -404,6 +427,11 @@ protected override void Dispose(bool disposing) { _archive.Dispose(); + if (_stream != null && _disposeStream) + { + _stream.Dispose(); + } + if (disposing) { TryGetDispatcher()?.Dispose(); @@ -425,20 +453,21 @@ protected override IEnumerable EnumeratePathsImpl(UPath path, string sear private IEnumerable EnumeratePathsStr(UPath path, string searchPattern, SearchOption searchOption, SearchTarget searchTarget) { var search = SearchPattern.Parse(ref path, ref searchPattern); - var entryPath = RemoveLeadingSlash(path); - var root = ConvertPathToDirectory(entryPath); -#if NETFRAMEWORK - root = root.Replace('/', '\\'); -#else - root = root.Replace('\\', '/'); -#endif + _entriesLock.EnterReadLock(); var entriesList = new List(); try { - entriesList = root == "" - ? _archive.Entries.ToList() - : _archive.Entries.Where(e => e.FullName.StartsWith(root, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) && e.FullName.Length > root.Length).ToList(); + var internEntries = path == UPath.Root + ? _entries + : _entries.Where(kv => kv.Key.FullName.StartsWith(path.FullName, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) && kv.Key.FullName.Length > path.FullName.Length); + + if (searchOption == SearchOption.TopDirectoryOnly) + { + internEntries = internEntries.Where(kv => kv.Key.IsInDirectory(path, false)); + } + + entriesList = internEntries.Select(kv => kv.Value.Entry).ToList(); } finally { @@ -451,11 +480,6 @@ private IEnumerable EnumeratePathsStr(UPath path, string searchPattern, } var entries = (IEnumerable)entriesList; - if (searchOption == SearchOption.TopDirectoryOnly) - { - var dir = GetParent(entries.First().FullName); - entries = entries.Where(e => string.Equals(ConvertPathToDirectory(GetParent(e.FullName)), root, _isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); - } if (searchTarget == SearchTarget.File) { @@ -477,13 +501,22 @@ private IEnumerable EnumeratePathsStr(UPath path, string searchPattern, /// protected override bool FileExistsImpl(UPath path) { - return GetEntry(path.FullName) != null; + _entriesLock.EnterReadLock(); + + try + { + return _entries.TryGetValue(path, out var entry) && !entry.IsDirectory; + } + finally + { + _entriesLock.ExitReadLock(); + } } /// protected override FileAttributes GetAttributesImpl(UPath path) { - var entry = GetEntry(path.FullName) ?? GetEntry(ConvertPathToDirectory(path)); + var entry = GetEntry(path); if (entry is null) { throw FileSystemExceptionHelper.NewFileNotFoundException(path); @@ -501,16 +534,18 @@ protected override FileAttributes GetAttributesImpl(UPath path) } return externalAttributes | attributes; -#endif +#else // return standard attributes if it's not NetStandard2.1 return attributes == FileAttributes.Directory ? FileAttributes.Directory : entry.LastWriteTime >= _creationTime ? FileAttributes.Archive : FileAttributes.Normal; +#endif } /// protected override long GetFileLengthImpl(UPath path) { - var entry = GetEntry(path.FullName); - if (entry == null) + var entry = GetEntry(path, out var isDirectory); + + if (entry == null || isDirectory) { throw FileSystemExceptionHelper.NewFileNotFoundException(path); } @@ -546,7 +581,7 @@ protected override DateTime GetLastAccessTimeImpl(UPath path) /// protected override DateTime GetLastWriteTimeImpl(UPath path) { - var entry = GetEntry(path.FullName) ?? GetEntry(ConvertPathToDirectory(path)); + var entry = GetEntry(path); if (entry == null) { return DefaultFileTime; @@ -563,15 +598,13 @@ protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath) throw new IOException("Cannot move directory to itself or a subdirectory."); } - var srcDir = RemoveLeadingSlash(ConvertPathToDirectory(srcPath.FullName)); - var destDir = RemoveLeadingSlash(ConvertPathToDirectory(destPath.FullName)); -#if NETFRAMEWORK - srcDir = srcDir.Replace('/', DirectorySeparator); - destDir = destDir.Replace('/', DirectorySeparator); -#else - srcDir = srcDir.Replace('\\', DirectorySeparator); - destDir = destDir.Replace('\\', DirectorySeparator); -#endif + if (FileExistsImpl(srcPath)) + { + throw new IOException(nameof(srcPath) + " is a file."); + } + + var srcDir = srcPath.FullName; + _entriesLock.EnterReadLock(); var entries = Array.Empty(); try @@ -585,11 +618,6 @@ protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath) if (entries.Length == 0) { - if (FileExistsImpl(srcPath)) - { - throw new IOException(nameof(srcPath) + " is a file."); - } - throw FileSystemExceptionHelper.NewDirectoryNotFoundException(srcPath); } @@ -605,7 +633,7 @@ protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath) using (var entryStream = entry.Open()) { var entryName = entry.FullName.Substring(srcDir.Length); - var destEntry = CreateEntry(destDir + entryName); + var destEntry = CreateEntry(destPath + entryName, isDirectory: true); using (var destEntryStream = destEntry.Open()) { entryStream.CopyTo(destEntryStream); @@ -621,14 +649,14 @@ protected override void MoveDirectoryImpl(UPath srcPath, UPath destPath) /// protected override void MoveFileImpl(UPath srcPath, UPath destPath) { - var srcEntry = GetEntry(srcPath.FullName) ?? throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath); + var srcEntry = GetEntry(srcPath) ?? throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath); if (!DirectoryExistsImpl(destPath.GetDirectory())) { throw FileSystemExceptionHelper.NewDirectoryNotFoundException(destPath.GetDirectory()); } - var destEntry = GetEntry(destPath.FullName); + var destEntry = GetEntry(destPath); if (destEntry != null) { throw new IOException("Cannot overwrite existing file."); @@ -659,7 +687,12 @@ protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess acc throw new ArgumentException("Cannot write in a read-only access."); } - var entry = GetEntry(path.FullName); + var entry = GetEntry(path, out var isDirectory); + + if (isDirectory) + { + throw new UnauthorizedAccessException(nameof(path) + " is a directory."); + } if (entry == null) { @@ -673,11 +706,6 @@ protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess acc } else { - if (DirectoryExistsImpl(path)) - { - throw new UnauthorizedAccessException(nameof(path) + " is a directory."); - } - if (!DirectoryExistsImpl(path.GetDirectory())) { throw FileSystemExceptionHelper.NewDirectoryNotFoundException(path.GetDirectory()); @@ -727,13 +755,13 @@ protected override Stream OpenFileImpl(UPath path, FileMode mode, FileAccess acc /// protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath destBackupPath, bool ignoreMetadataErrors) { - var sourceEntry = GetEntry(srcPath.FullName); + var sourceEntry = GetEntry(srcPath); if (sourceEntry is null) { throw FileSystemExceptionHelper.NewFileNotFoundException(srcPath); } - var destEntry = GetEntry(destPath.FullName); + var destEntry = GetEntry(destPath); if (destEntry == sourceEntry) { throw new IOException("Cannot replace the file with itself."); @@ -742,7 +770,7 @@ protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath des if (destEntry != null) { // create a backup at destBackupPath if its not null - if (destBackupPath != null) + if (!destBackupPath.IsEmpty) { var destBackupEntry = CreateEntry(destBackupPath.FullName); using var destBackupStream = destBackupEntry.Open(); @@ -777,8 +805,7 @@ protected override void ReplaceFileImpl(UPath srcPath, UPath destPath, UPath des protected override void SetAttributesImpl(UPath path, FileAttributes attributes) { #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - var entryPath = RemoveLeadingSlash(path); - var entry = GetEntry(entryPath) ?? GetEntry(ConvertPathToDirectory(entryPath)); + var entry = GetEntry(path); if (entry == null) { throw FileSystemExceptionHelper.NewFileNotFoundException(path); @@ -786,9 +813,9 @@ protected override void SetAttributesImpl(UPath path, FileAttributes attributes) entry.ExternalAttributes = (int)attributes; TryGetDispatcher()?.RaiseChange(path); - return; -#endif +#else Debug.WriteLine("SetAttributes don't work in NetStandard2.0 or older."); +#endif } /// @@ -810,7 +837,7 @@ protected override void SetLastAccessTimeImpl(UPath path, DateTime time) /// protected override void SetLastWriteTimeImpl(UPath path, DateTime time) { - var entry = GetEntry(path.FullName) ?? GetEntry(ConvertPathToDirectory(path.FullName)); + var entry = GetEntry(path); if (entry is null) { throw FileSystemExceptionHelper.NewFileNotFoundException(path); @@ -852,11 +879,7 @@ private void RemoveEntry(ZipArchiveEntry entry) try { entry.Delete(); - - if (!_isCaseSensitive) - { - _entries.Remove(entry.FullName.ToLowerInvariant()); - } + _entries.Remove(new UPath(entry.FullName).ToAbsolute()); } finally { @@ -864,24 +887,20 @@ private void RemoveEntry(ZipArchiveEntry entry) } } - private ZipArchiveEntry CreateEntry(string path) + private ZipArchiveEntry CreateEntry(UPath path, bool isDirectory = false) { - path = RemoveLeadingSlash(path); -#if NETFRAMEWORK - path = path.Replace('/', DirectorySeparator); -#else - path = path.Replace('\\', DirectorySeparator); -#endif _entriesLock.EnterWriteLock(); try { - var entry = _archive.CreateEntry(path, _compressionLevel); + var internalPath = path.FullName; - if (!_isCaseSensitive) + if (isDirectory) { - _entries.Add(entry.FullName.ToLowerInvariant(), entry); + internalPath += DirectorySeparator; } + var entry = _archive.CreateEntry(internalPath, _compressionLevel); + _entries[path] = new InternalZipEntry(entry, isDirectory); return entry; } finally @@ -906,31 +925,6 @@ private static string GetParent(string path) return lastIndex == -1 ? "" : path.Substring(0, lastIndex); } - private static string ConvertPathToDirectory(UPath path) - { - return ConvertPathToDirectory(path.FullName); - } - - private static string ConvertPathToDirectory(string path) - { - if (string.IsNullOrEmpty(path)) - { - return path; - } - - return path[path.Length - 1] is DirectorySeparator ? path : path + DirectorySeparator; - } - - private static string RemoveLeadingSlash(UPath path) - { - return path.FullName[0] is '\\' or '/' ? path.FullName.Substring(1) : path.FullName; - } - - private static string RemoveLeadingSlash(string path) - { - return path[0] is '\\' or '/' ? path.Substring(1) : path; - } - private FileSystemEventDispatcher? TryGetDispatcher() { lock (_dispatcherLock) @@ -1031,7 +1025,10 @@ public override void Close() _isDisposed = true; lock (_fileSystem._openStreamsLock) { - _fileSystem._openStreams.TryGetValue(_entry, out var fileData); + if (!_fileSystem._openStreams.TryGetValue(_entry, out var fileData)) + { + return; + } fileData.Count--; if (fileData.Count == 0) { @@ -1054,5 +1051,17 @@ public EntryState(FileShare share) public int Count; } + + private readonly struct InternalZipEntry + { + public InternalZipEntry(ZipArchiveEntry entry, bool isDirectory) + { + Entry = entry; + IsDirectory = isDirectory; + } + + public readonly ZipArchiveEntry Entry; + public readonly bool IsDirectory; + } } #endif \ No newline at end of file diff --git a/src/Zio/UPath.cs b/src/Zio/UPath.cs index f80d9a5..3bf7ad1 100644 --- a/src/Zio/UPath.cs +++ b/src/Zio/UPath.cs @@ -36,12 +36,12 @@ namespace Zio; /// /// The default comparer for a that is case sensitive. /// - public static readonly IComparer DefaultComparer = new ComparerCaseSensitive(); + public static readonly IComparer DefaultComparer = UPathComparer.Ordinal; /// /// The default comparer for a that is case insensitive. /// - public static readonly IComparer DefaultComparerIgnoreCase = new ComparerIgnoreCase(); + public static readonly IComparer DefaultComparerIgnoreCase = UPathComparer.OrdinalIgnoreCase; /// /// Initializes a new instance of the struct. @@ -472,20 +472,4 @@ public int CompareTo(UPath other) { return string.Compare(FullName, other.FullName, StringComparison.Ordinal); } - - private class ComparerCaseSensitive : IComparer - { - public int Compare(UPath x, UPath y) - { - return string.Compare(x.FullName, y.FullName, StringComparison.Ordinal); - } - } - - private class ComparerIgnoreCase : IComparer - { - public int Compare(UPath x, UPath y) - { - return string.Compare(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase); - } - } } \ No newline at end of file diff --git a/src/Zio/UPathComparer.cs b/src/Zio/UPathComparer.cs new file mode 100644 index 0000000..9bfdff3 --- /dev/null +++ b/src/Zio/UPathComparer.cs @@ -0,0 +1,35 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +namespace Zio; + +public class UPathComparer : IComparer, IEqualityComparer +{ + public static readonly UPathComparer Ordinal = new(StringComparer.Ordinal); + public static readonly UPathComparer OrdinalIgnoreCase = new(StringComparer.OrdinalIgnoreCase); + public static readonly UPathComparer CurrentCulture = new(StringComparer.CurrentCulture); + public static readonly UPathComparer CurrentCultureIgnoreCase = new(StringComparer.CurrentCultureIgnoreCase); + + private readonly StringComparer _comparer; + + private UPathComparer(StringComparer comparer) + { + _comparer = comparer; + } + + public int Compare(UPath x, UPath y) + { + return _comparer.Compare(x.FullName, y.FullName); + } + + public bool Equals(UPath x, UPath y) + { + return _comparer.Equals(x.FullName, y.FullName); + } + + public int GetHashCode(UPath obj) + { + return _comparer.GetHashCode(obj.FullName); + } +} diff --git a/src/Zio/Zio.csproj b/src/Zio/Zio.csproj index 3c78bda..12a8576 100644 --- a/src/Zio/Zio.csproj +++ b/src/Zio/Zio.csproj @@ -68,6 +68,8 @@ + true + true $(AdditionalConstants);NETSTANDARD;HAS_ZIPARCHIVE;HAS_NULLABLEANNOTATIONS