diff --git a/Source/Testably.Abstractions.Compression/Internal/ZipUtilities.cs b/Source/Testably.Abstractions.Compression/Internal/ZipUtilities.cs index c4a3528e2..a7bbb65f6 100644 --- a/Source/Testably.Abstractions.Compression/Internal/ZipUtilities.cs +++ b/Source/Testably.Abstractions.Compression/Internal/ZipUtilities.cs @@ -8,6 +8,7 @@ namespace Testably.Abstractions.Internal; internal static class ZipUtilities { private const string SearchPattern = "*"; + private static readonly DateTime FallbackTime = new(1980, 1, 1, 0, 0, 0); internal static IZipArchiveEntry CreateEntryFromFile( IZipArchive destination, @@ -32,7 +33,7 @@ internal static IZipArchiveEntry CreateEntryFromFile( if (lastWrite.Year is < 1980 or > 2107) { - lastWrite = new DateTime(1980, 1, 1, 0, 0, 0); + lastWrite = FallbackTime; } entry.LastWriteTime = new DateTimeOffset(lastWrite); @@ -135,10 +136,7 @@ internal static void CreateFromDirectory( ArgumentNullException.ThrowIfNull(destination); if (!destination.CanWrite) { - throw new ArgumentException("The stream is unwritable.", nameof(destination)) - { - HResult = -2147024809 - }; + throw new ArgumentException("The stream is unwritable.", nameof(destination)); } sourceDirectoryName = fileSystem.Path.GetFullPath(sourceDirectoryName); @@ -206,13 +204,6 @@ internal static void ExtractRelativeToDirectory(this IZipArchiveEntry source, source.FullName.TrimStart( source.FileSystem.Path.DirectorySeparatorChar, source.FileSystem.Path.AltDirectorySeparatorChar)); - string? directoryPath = - source.FileSystem.Path.GetDirectoryName(fileDestinationPath); - if (directoryPath != null && - !source.FileSystem.Directory.Exists(directoryPath)) - { - source.FileSystem.Directory.CreateDirectory(directoryPath); - } if (source.FullName.EndsWith('/')) { @@ -226,6 +217,8 @@ internal static void ExtractRelativeToDirectory(this IZipArchiveEntry source, } else { + source.FileSystem.Directory.CreateDirectory( + source.FileSystem.Path.GetDirectoryName(fileDestinationPath) ?? "."); ExtractToFile(source, fileDestinationPath, overwrite); } } @@ -274,10 +267,7 @@ internal static void ExtractToDirectory(IFileSystem fileSystem, ArgumentNullException.ThrowIfNull(source); if (!source.CanRead) { - throw new ArgumentException("The stream is unreadable.", nameof(source)) - { - HResult = -2147024809 - }; + throw new ArgumentException("The stream is unreadable.", nameof(source)); } using (ZipArchive archive = new(source, ZipArchiveMode.Read, true, entryNameEncoding)) diff --git a/Tests/Testably.Abstractions.Compression.Tests/Internal/ExecuteTests.cs b/Tests/Testably.Abstractions.Compression.Tests/Internal/ExecuteTests.cs index 99af40b2b..36481336b 100644 --- a/Tests/Testably.Abstractions.Compression.Tests/Internal/ExecuteTests.cs +++ b/Tests/Testably.Abstractions.Compression.Tests/Internal/ExecuteTests.cs @@ -5,28 +5,80 @@ namespace Testably.Abstractions.Compression.Tests.Internal; public sealed class ExecuteTests { [Fact] - public void WhenRealFileSystem_MockFileSystem_ShouldExecuteOnMockFileSystem() + public void WhenRealFileSystem_MockFileSystem_WithActionCallback_ShouldExecuteOnMockFileSystem() { bool onRealFileSystemExecuted = false; bool onMockFileSystemExecuted = false; MockFileSystem fileSystem = new(); Execute.WhenRealFileSystem(fileSystem, - () => onRealFileSystemExecuted = true, - () => onMockFileSystemExecuted = true); + () => + { + onRealFileSystemExecuted = true; + }, + () => + { + onMockFileSystemExecuted = true; + }); onRealFileSystemExecuted.Should().BeFalse(); onMockFileSystemExecuted.Should().BeTrue(); } [Fact] - public void WhenRealFileSystem_RealFileSystem_ShouldExecuteOnRealFileSystem() + public void WhenRealFileSystem_MockFileSystem_WithFuncCallback_ShouldExecuteOnMockFileSystem() + { + bool onRealFileSystemExecuted = false; + bool onMockFileSystemExecuted = false; + MockFileSystem fileSystem = new(); + Execute.WhenRealFileSystem(fileSystem, + () => + { + return onRealFileSystemExecuted = true; + }, + () => + { + return onMockFileSystemExecuted = true; + }); + + onRealFileSystemExecuted.Should().BeFalse(); + onMockFileSystemExecuted.Should().BeTrue(); + } + + [Fact] + public void WhenRealFileSystem_RealFileSystem_WithActionCallback_ShouldExecuteOnRealFileSystem() + { + bool onRealFileSystemExecuted = false; + bool onMockFileSystemExecuted = false; + RealFileSystem fileSystem = new(); + Execute.WhenRealFileSystem(fileSystem, + () => + { + onRealFileSystemExecuted = true; + }, + () => + { + onMockFileSystemExecuted = true; + }); + + onRealFileSystemExecuted.Should().BeTrue(); + onMockFileSystemExecuted.Should().BeFalse(); + } + + [Fact] + public void WhenRealFileSystem_RealFileSystem_WithFuncCallback_ShouldExecuteOnRealFileSystem() { bool onRealFileSystemExecuted = false; bool onMockFileSystemExecuted = false; RealFileSystem fileSystem = new(); Execute.WhenRealFileSystem(fileSystem, - () => onRealFileSystemExecuted = true, - () => onMockFileSystemExecuted = true); + () => + { + return onRealFileSystemExecuted = true; + }, + () => + { + return onMockFileSystemExecuted = true; + }); onRealFileSystemExecuted.Should().BeTrue(); onMockFileSystemExecuted.Should().BeFalse(); diff --git a/Tests/Testably.Abstractions.Compression.Tests/Internal/ZipUtilitiesTests.cs b/Tests/Testably.Abstractions.Compression.Tests/Internal/ZipUtilitiesTests.cs new file mode 100644 index 000000000..3ec44a582 --- /dev/null +++ b/Tests/Testably.Abstractions.Compression.Tests/Internal/ZipUtilitiesTests.cs @@ -0,0 +1,102 @@ +using System.IO; +using Testably.Abstractions.Internal; + +namespace Testably.Abstractions.Compression.Tests.Internal; + +public sealed class ZipUtilitiesTests +{ + [Theory] + [AutoData] + public void ExtractRelativeToDirectory_FileWithTrailingSlash_ShouldThrowIOException( + byte[] bytes) + { + MockFileSystem fileSystem = new(); + using MemoryStream stream = new(bytes); + DummyZipArchiveEntry zipArchiveEntry = new(fileSystem, fullName: "foo/", stream: stream); + + Exception? exception = Record.Exception(() => + { + zipArchiveEntry.ExtractRelativeToDirectory("foo", false); + }); + + exception.Should().BeException( + messageContains: + "Zip entry name ends in directory separator character but contains data"); + } + + [Fact] + public void ExtractRelativeToDirectory_WithSubdirectory_ShouldCreateSubdirectory() + { + MockFileSystem fileSystem = new(); + DummyZipArchiveEntry zipArchiveEntry = new(fileSystem, fullName: "foo/"); + + zipArchiveEntry.ExtractRelativeToDirectory("bar", false); + + fileSystem.Directory.Exists("bar").Should().BeTrue(); + fileSystem.Directory.Exists("bar/foo").Should().BeTrue(); + } + + private sealed class DummyZipArchiveEntry( + IFileSystem fileSystem, + IZipArchive? archive = null, + string? fullName = "", + string? name = "", + string comment = "", + bool isEncrypted = false, + Stream? stream = null) + : IZipArchiveEntry + { + #region IZipArchiveEntry Members + + /// + public IZipArchive Archive => archive ?? throw new NotSupportedException(); + + /// + public string Comment { get; set; } = comment; + + /// + public long CompressedLength => stream?.Length ?? 0L; + + /// + public uint Crc32 => 0u; + + /// + public int ExternalAttributes { get; set; } + + /// + public IFileSystem FileSystem { get; } = fileSystem; + + /// + public string FullName { get; } = fullName ?? ""; + + /// + public bool IsEncrypted { get; } = isEncrypted; + + /// + public DateTimeOffset LastWriteTime { get; set; } + + /// + public long Length => stream?.Length ?? 0L; + + /// + public string Name { get; } = name ?? ""; + + /// + public void Delete() + => throw new NotSupportedException(); + + /// + public void ExtractToFile(string destinationFileName) + => throw new NotSupportedException(); + + /// + public void ExtractToFile(string destinationFileName, bool overwrite) + => throw new NotSupportedException(); + + /// + public Stream Open() + => stream ?? throw new NotSupportedException(); + + #endregion + } +} diff --git a/Tests/Testably.Abstractions.Compression.Tests/ZipFile/CreateFromDirectoryTests.cs b/Tests/Testably.Abstractions.Compression.Tests/ZipFile/CreateFromDirectoryTests.cs index 40a111df3..8cb1fa03f 100644 --- a/Tests/Testably.Abstractions.Compression.Tests/ZipFile/CreateFromDirectoryTests.cs +++ b/Tests/Testably.Abstractions.Compression.Tests/ZipFile/CreateFromDirectoryTests.cs @@ -159,6 +159,30 @@ public void CreateFromDirectory_ShouldZipDirectoryContent() FileSystem.File.ReadAllBytes("foo/bar/test.txt")); } +#if FEATURE_COMPRESSION_STREAM + [SkippableFact] + public void CreateFromDirectory_WithReadOnlyStream_ShouldThrowArgumentException() + { + FileSystem.Initialize() + .WithFile("target.zip") + .WithSubdirectory("foo").Initialized(s => s + .WithFile("test.txt")); + using FileSystemStream stream = FileSystem.FileStream.New( + "target.zip", FileMode.Open, FileAccess.Read); + + Exception? exception = Record.Exception(() => + { + // ReSharper disable once AccessToDisposedClosure + FileSystem.ZipFile().CreateFromDirectory("foo", stream); + }); + + exception.Should().BeException( + paramName: "destination", + hResult: -2147024809, + messageContains: "stream is unwritable"); + } +#endif + #if FEATURE_COMPRESSION_STREAM [SkippableTheory] [AutoData] diff --git a/Tests/Testably.Abstractions.Compression.Tests/ZipFile/ExtractToDirectoryTests.cs b/Tests/Testably.Abstractions.Compression.Tests/ZipFile/ExtractToDirectoryTests.cs index 0e774144c..c2602e3d9 100644 --- a/Tests/Testably.Abstractions.Compression.Tests/ZipFile/ExtractToDirectoryTests.cs +++ b/Tests/Testably.Abstractions.Compression.Tests/ZipFile/ExtractToDirectoryTests.cs @@ -330,4 +330,30 @@ public void ExtractToDirectory_WithStream_WithoutOverwriteAndExistingFile_Should .Should().NotBe(contents); } #endif + +#if FEATURE_COMPRESSION_STREAM + [SkippableFact] + public void ExtractToDirectory_WithWriteOnlyStream_ShouldThrowArgumentException() + { + FileSystem.Initialize() + .WithFile("target.zip") + .WithSubdirectory("foo").Initialized(s => s + .WithFile("test.txt")); + using FileSystemStream stream = FileSystem.FileStream.New( + "target.zip", FileMode.Open, FileAccess.Write); + + FileSystem.ZipFile().CreateFromDirectory("foo", stream); + + Exception? exception = Record.Exception(() => + { + // ReSharper disable once AccessToDisposedClosure + FileSystem.ZipFile().ExtractToDirectory(stream, "bar"); + }); + + exception.Should().BeException( + paramName: "source", + hResult: -2147024809, + messageContains: "stream is unreadable"); + } +#endif }