Merge branch 'master' into async-reader-methods

This commit is contained in:
Adam Hathcock
2025-10-28 11:39:35 +00:00
committed by GitHub
24 changed files with 1114 additions and 57 deletions

View File

@@ -96,6 +96,9 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
DateTime? modified
) => AddEntry(key, source, closeStream, size, modified);
IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) =>
AddDirectoryEntry(key, modified);
public TEntry AddEntry(
string key,
Stream source,
@@ -136,6 +139,22 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
return false;
}
public TEntry AddDirectoryEntry(string key, DateTime? modified = null)
{
if (key.Length > 0 && key[0] is '/' or '\\')
{
key = key.Substring(1);
}
if (DoesKeyMatchExisting(key))
{
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
}
var entry = CreateDirectoryEntry(key, modified);
newEntries.Add(entry);
RebuildModifiedCollection();
return entry;
}
public void SaveTo(Stream stream, WriterOptions options)
{
//reset streams of new entries
@@ -180,6 +199,8 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
bool closeStream
);
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
protected abstract void SaveTo(
Stream stream,
WriterOptions options,

View File

@@ -185,6 +185,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
}
protected override GZipArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => throw new NotSupportedException("GZip archives do not support directory entries.");
protected override void SaveTo(
Stream stream,
WriterOptions options,

View File

@@ -18,6 +18,8 @@ public interface IWritableArchive : IArchive
DateTime? modified = null
);
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
void SaveTo(Stream stream, WriterOptions options);
Task SaveToAsync(

View File

@@ -224,6 +224,11 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
closeStream
);
protected override TarArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => new TarWritableArchiveEntry(this, directoryPath, modified);
protected override void SaveTo(
Stream stream,
WriterOptions options,
@@ -232,15 +237,25 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
}
}
}
@@ -253,18 +268,31 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size,
cancellationToken
)
.ConfigureAwait(false);
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size,
cancellationToken
)
.ConfigureAwait(false);
}
}
}

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Tar;
internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
internal TarWritableArchiveEntry(
TarArchive archive,
@@ -27,6 +28,22 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal TarWritableArchiveEntry(
TarArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null, CompressionType.None)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,15 +64,19 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -63,7 +84,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
internal override void Close()
{
if (closeStream)
if (closeStream && stream is not null)
{
stream.Dispose();
}

View File

@@ -308,14 +308,24 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
if (entry.IsDirectory)
{
writer.WriteDirectory(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime
);
}
else
{
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
}
}
}
@@ -328,12 +338,29 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
foreach (var entry in oldEntries.Concat(newEntries))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
.ConfigureAwait(false);
if (entry.IsDirectory)
{
await writer
.WriteDirectoryAsync(
entry.Key.NotNull("Entry Key is null"),
entry.LastModifiedTime,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(
entry.Key.NotNull("Entry Key is null"),
entryStream,
cancellationToken
)
.ConfigureAwait(false);
}
}
}
@@ -345,6 +372,11 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
bool closeStream
) => new ZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
protected override ZipArchiveEntry CreateDirectoryEntry(
string directoryPath,
DateTime? modified
) => new ZipWritableArchiveEntry(this, directoryPath, modified);
public static ZipArchive Create() => new();
protected override IReader CreateReaderForSolidExtraction()

View File

@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Zip;
internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream stream;
private readonly Stream? stream;
private readonly bool isDirectory;
private bool isDisposed;
internal ZipWritableArchiveEntry(
@@ -27,6 +28,22 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
Size = size;
LastModifiedTime = lastModified;
this.closeStream = closeStream;
isDirectory = false;
}
internal ZipWritableArchiveEntry(
ZipArchive archive,
string directoryPath,
DateTime? lastModified
)
: base(archive, null)
{
stream = null;
Key = directoryPath;
Size = 0;
LastModifiedTime = lastModified;
closeStream = false;
isDirectory = true;
}
public override long Crc => 0;
@@ -47,16 +64,20 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
public override bool IsEncrypted => false;
public override bool IsDirectory => false;
public override bool IsDirectory => isDirectory;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream;
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
public override Stream OpenEntryStream()
{
if (stream is null)
{
return Stream.Null;
}
//ensure new stream is at the start, this could be reset
stream.Seek(0, SeekOrigin.Begin);
return SharpCompressStream.Create(stream, leaveOpen: true);
@@ -64,7 +85,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
internal override void Close()
{
if (closeStream && !isDisposed)
if (closeStream && !isDisposed && stream is not null)
{
stream.Dispose();
isDisposed = true;

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -7,6 +8,15 @@ namespace SharpCompress.Common;
internal static class ExtractionMethods
{
/// <summary>
/// Gets the appropriate StringComparison for path checks based on the file system.
/// Windows uses case-insensitive file systems, while Unix-like systems use case-sensitive file systems.
/// </summary>
private static StringComparison PathComparison =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
@@ -48,7 +58,7 @@ internal static class ExtractionMethods
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
@@ -68,12 +78,7 @@ internal static class ExtractionMethods
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (
!destinationFileName.StartsWith(
fullDestinationDirectoryPath,
StringComparison.Ordinal
)
)
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."
@@ -158,7 +163,7 @@ internal static class ExtractionMethods
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
@@ -178,12 +183,7 @@ internal static class ExtractionMethods
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (
!destinationFileName.StartsWith(
fullDestinationDirectoryPath,
StringComparison.Ordinal
)
)
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."

View File

@@ -1,12 +1,13 @@
using System;
using System.IO;
using SharpCompress.Common;
namespace SharpCompress.Compressors.LZMA;
/// <summary>
/// The exception that is thrown when an error in input stream occurs during decoding.
/// </summary>
internal class DataErrorException : Exception
internal class DataErrorException : SharpCompressException
{
public DataErrorException()
: base("Data Error") { }
@@ -15,7 +16,7 @@ internal class DataErrorException : Exception
/// <summary>
/// The exception that is thrown when the value of an argument is outside the allowable range.
/// </summary>
internal class InvalidParamException : Exception
internal class InvalidParamException : SharpCompressException
{
public InvalidParamException()
: base("Invalid Parameter") { }

View File

@@ -1,5 +1,5 @@
using System;
using SharpCompress.Common;
namespace SharpCompress.Compressors.Xz;
public class XZIndexMarkerReachedException : Exception { }
public class XZIndexMarkerReachedException : SharpCompressException { }

View File

@@ -37,6 +37,20 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio
await Task.CompletedTask.ConfigureAwait(false);
}
public abstract void WriteDirectory(string directoryName, DateTime? modificationTime);
public virtual async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Default implementation calls synchronous version
// Derived classes should override for true async behavior
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)

View File

@@ -50,4 +50,7 @@ public sealed class GZipWriter : AbstractWriter
source.CopyTo(stream);
_wroteToStream = true;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime) =>
throw new NotSupportedException("GZip archives do not support directory entries.");
}

View File

@@ -16,4 +16,10 @@ public interface IWriter : IDisposable
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
void WriteDirectory(string directoryName, DateTime? modificationTime);
Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
);
}

View File

@@ -55,6 +55,9 @@ public static class IWriterExtensions
}
}
public static void WriteDirectory(this IWriter writer, string directoryName) =>
writer.WriteDirectory(directoryName, null);
// Async extensions
public static Task WriteAsync(
this IWriter writer,
@@ -121,4 +124,10 @@ public static class IWriterExtensions
.ConfigureAwait(false);
}
}
public static Task WriteDirectoryAsync(
this IWriter writer,
string directoryName,
CancellationToken cancellationToken = default
) => writer.WriteDirectoryAsync(directoryName, null, cancellationToken);
}

View File

@@ -74,6 +74,44 @@ public class TarWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for tar format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var header = new TarHeader(WriterOptions.ArchiveEncoding);
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
header.Name = normalizedName;
header.Size = 0;
header.EntryType = EntryType.Directory;
header.Write(OutputStream);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for header-only write
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
public void Write(string filename, Stream source, DateTime? modificationTime, long? size)
{
if (!source.CanSeek && size is null)

View File

@@ -3,6 +3,8 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
@@ -135,6 +137,73 @@ public class ZipWriter : AbstractWriter
return filename.Trim('/');
}
private string NormalizeDirectoryName(string directoryName)
{
directoryName = NormalizeFilename(directoryName);
// Ensure directory name ends with '/' for zip format
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
{
directoryName += '/';
}
return directoryName;
}
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
{
var normalizedName = NormalizeDirectoryName(directoryName);
if (string.IsNullOrEmpty(normalizedName))
{
return; // Skip empty or root directory
}
var options = new ZipWriterEntryOptions { ModificationDateTime = modificationTime };
WriteDirectoryEntry(normalizedName, options);
}
public override async Task WriteDirectoryAsync(
string directoryName,
DateTime? modificationTime,
CancellationToken cancellationToken = default
)
{
// Synchronous implementation is sufficient for directory entries
WriteDirectory(directoryName, modificationTime);
await Task.CompletedTask.ConfigureAwait(false);
}
private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options)
{
var compression = ZipCompressionMethod.None;
options.ModificationDateTime ??= DateTime.Now;
options.EntryComment ??= string.Empty;
var entry = new ZipCentralDirectoryEntry(
compression,
directoryPath,
(ulong)streamPosition,
WriterOptions.ArchiveEncoding
)
{
Comment = options.EntryComment,
ModificationTime = options.ModificationDateTime,
Crc = 0,
Compressed = 0,
Decompressed = 0,
};
// Use the archive default setting for zip64 and allow overrides
var useZip64 = isZip64;
if (options.EnableZip64.HasValue)
{
useZip64 = options.EnableZip64.Value;
}
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
streamPosition += headersize;
entries.Add(entry);
}
private int WriteHeader(
string filename,
ZipWriterEntryOptions zipWriterEntryOptions,

View File

@@ -0,0 +1,116 @@
using System;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.Xz;
using Xunit;
namespace SharpCompress.Test;
public class ExceptionHierarchyTests
{
[Fact]
public void AllSharpCompressExceptions_InheritFromSharpCompressException()
{
// Verify that ArchiveException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ArchiveException)));
// Verify that ExtractionException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ExtractionException)));
// Verify that InvalidFormatException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(InvalidFormatException))
);
// Verify that CryptographicException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(CryptographicException))
);
// Verify that IncompleteArchiveException inherits from SharpCompressException (through ArchiveException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(IncompleteArchiveException))
);
// Verify that ReaderCancelledException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(ReaderCancelledException))
);
// Verify that MultipartStreamRequiredException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(
typeof(MultipartStreamRequiredException)
)
);
// Verify that MultiVolumeExtractionException inherits from SharpCompressException (through ExtractionException)
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(MultiVolumeExtractionException))
);
// Verify that ZlibException inherits from SharpCompressException
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ZlibException)));
// Verify that XZIndexMarkerReachedException inherits from SharpCompressException
Assert.True(
typeof(SharpCompressException).IsAssignableFrom(typeof(XZIndexMarkerReachedException))
);
}
[Fact]
public void SharpCompressException_CanBeCaughtByBaseType()
{
// Test that a derived exception can be caught as SharpCompressException
var exception = new InvalidFormatException("Test message");
var caughtException = false;
try
{
throw exception;
}
catch (SharpCompressException ex)
{
caughtException = true;
Assert.Same(exception, ex);
}
Assert.True(caughtException, "Exception should have been caught as SharpCompressException");
}
[Fact]
public void InternalLzmaExceptions_InheritFromSharpCompressException()
{
// Use reflection to verify internal exception types
var dataErrorExceptionType = Type.GetType(
"SharpCompress.Compressors.LZMA.DataErrorException, SharpCompress"
);
Assert.NotNull(dataErrorExceptionType);
Assert.True(typeof(SharpCompressException).IsAssignableFrom(dataErrorExceptionType));
var invalidParamExceptionType = Type.GetType(
"SharpCompress.Compressors.LZMA.InvalidParamException, SharpCompress"
);
Assert.NotNull(invalidParamExceptionType);
Assert.True(typeof(SharpCompressException).IsAssignableFrom(invalidParamExceptionType));
}
[Fact]
public void ExceptionConstructors_WorkCorrectly()
{
// Test parameterless constructor
var ex1 = new SharpCompressException();
Assert.NotNull(ex1);
// Test message constructor
var ex2 = new SharpCompressException("Test message");
Assert.Equal("Test message", ex2.Message);
// Test message and inner exception constructor
var inner = new InvalidOperationException("Inner");
var ex3 = new SharpCompressException("Test message", inner);
Assert.Equal("Test message", ex3.Message);
Assert.Same(inner, ex3.InnerException);
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test;
public class ExtractionTests : TestBase
{
[Fact]
public void Extraction_ShouldHandleCaseInsensitivePathsOnWindows()
{
// This test validates that extraction succeeds when Path.GetFullPath returns paths
// with casing that matches the platform's file system behavior. On Windows,
// Path.GetFullPath can return different casing than the actual directory on disk
// (e.g., "system32" vs "System32"), and the extraction should succeed because
// Windows file systems are case-insensitive. On Unix-like systems, this test
// verifies that the case-sensitive comparison is used correctly.
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-extraction.zip");
var extractPath = SCRATCH_FILES_PATH;
// Create a simple test archive with a single file
using (var stream = File.Create(testArchive))
{
using var writer = (ZipWriter)
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
// Create a test file to add to the archive
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile.txt");
File.WriteAllText(testFilePath, "Test content");
writer.Write("testfile.txt", testFilePath);
}
// Extract the archive - this should succeed regardless of path casing
using (var stream = File.OpenRead(testArchive))
{
using var reader = ReaderFactory.Open(stream);
// This should not throw an exception even if Path.GetFullPath returns
// a path with different casing than the actual directory
var exception = Record.Exception(() =>
reader.WriteAllToDirectory(
extractPath,
new ExtractionOptions { ExtractFullPath = false, Overwrite = true }
)
);
Assert.Null(exception);
}
// Verify the file was extracted successfully
var extractedFile = Path.Combine(extractPath, "testfile.txt");
Assert.True(File.Exists(extractedFile));
Assert.Equal("Test content", File.ReadAllText(extractedFile));
}
[Fact]
public void Extraction_ShouldPreventPathTraversalAttacks()
{
// This test ensures that the security check still works to prevent
// path traversal attacks (e.g., using "../" to escape the destination directory)
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-traversal.zip");
var extractPath = SCRATCH_FILES_PATH;
// Create a test archive with a path traversal attempt
using (var stream = File.Create(testArchive))
{
using var writer = (ZipWriter)
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile2.txt");
File.WriteAllText(testFilePath, "Test content");
// Try to write with a path that attempts to escape the destination directory
writer.Write("../../evil.txt", testFilePath);
}
// Extract the archive - this should throw an exception for path traversal
using (var stream = File.OpenRead(testArchive))
{
using var reader = ReaderFactory.Open(stream);
var exception = Assert.Throws<ExtractionException>(() =>
reader.WriteAllToDirectory(
extractPath,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
)
);
Assert.Contains("outside of the destination", exception.Message);
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.IO;
using SharpCompress.Archives.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveDirectoryTests : TestBase
{
[Fact]
public void GZipArchive_AddDirectoryEntry_ThrowsNotSupportedException()
{
using var archive = GZipArchive.Create();
Assert.Throws<NotSupportedException>(() =>
archive.AddDirectoryEntry("test-dir", DateTime.Now)
);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipWriterDirectoryTests : TestBase
{
[Fact]
public void GZipWriter_WriteDirectory_ThrowsNotSupportedException()
{
using var memoryStream = new MemoryStream();
using var writer = new GZipWriter(memoryStream, new GZipWriterOptions());
Assert.Throws<NotSupportedException>(() => writer.WriteDirectory("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarArchiveDirectoryTests : TestBase
{
[Fact]
public void TarArchive_AddDirectoryEntry_CreatesDirectoryEntry()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarArchive_AddDirectoryEntry_MultipleDirectories()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries.All(e => e.IsDirectory));
}
[Fact]
public void TarArchive_AddDirectoryEntry_MixedWithFiles()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries[0].IsDirectory);
Assert.False(entries[1].IsDirectory);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void TarArchive_AddDirectoryEntry_SaveAndReload()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "tar-directory-test.tar");
using (var archive = TarArchive.Create())
{
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
archive.AddEntry(
"dir1/file.txt",
contentStream,
false,
contentStream.Length,
DateTime.Now
);
using (var fileStream = File.Create(scratchPath))
{
archive.SaveTo(fileStream, CompressionType.None);
}
}
using (var archive = TarArchive.Open(scratchPath))
{
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}
[Fact]
public void TarArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
{
using var archive = TarArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
Assert.Throws<ArchiveException>(() => archive.AddDirectoryEntry("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Writers.Tar;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarWriterDirectoryTests : TestBase
{
[Fact]
public void TarWriter_WriteDirectory_CreatesDirectoryEntry()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("test-dir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_WithTrailingSlash()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("test-dir/", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_WithBackslash()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/subdir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_EmptyString_IsSkipped()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
Assert.Empty(archive.Entries);
}
[Fact]
public void TarWriter_WriteDirectory_MultipleDirectories()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("dir1", DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
writer.WriteDirectory("dir1/subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/subdir/", entries[1].Key);
Assert.True(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void TarWriter_WriteDirectory_MixedWithFiles()
{
using var memoryStream = new MemoryStream();
using (
var writer = new TarWriter(
memoryStream,
new TarWriterOptions(CompressionType.None, true)
)
)
{
writer.WriteDirectory("dir1", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = TarArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipArchiveDirectoryTests : TestBase
{
[Fact]
public void ZipArchive_AddDirectoryEntry_CreatesDirectoryEntry()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipArchive_AddDirectoryEntry_MultipleDirectories()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries.All(e => e.IsDirectory));
}
[Fact]
public void ZipArchive_AddDirectoryEntry_MixedWithFiles()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("dir1", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.True(entries[0].IsDirectory);
Assert.False(entries[1].IsDirectory);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void ZipArchive_AddDirectoryEntry_SaveAndReload()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "zip-directory-test.zip");
using (var archive = ZipArchive.Create())
{
archive.AddDirectoryEntry("dir1", DateTime.Now);
archive.AddDirectoryEntry("dir2", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
archive.AddEntry(
"dir1/file.txt",
contentStream,
false,
contentStream.Length,
DateTime.Now
);
using (var fileStream = File.Create(scratchPath))
{
archive.SaveTo(fileStream, CompressionType.Deflate);
}
}
using (var archive = ZipArchive.Open(scratchPath))
{
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}
[Fact]
public void ZipArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
{
using var archive = ZipArchive.Create();
archive.AddDirectoryEntry("test-dir", DateTime.Now);
Assert.Throws<ArchiveException>(() => archive.AddDirectoryEntry("test-dir", DateTime.Now));
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipWriterDirectoryTests : TestBase
{
[Fact]
public void ZipWriter_WriteDirectory_CreatesDirectoryEntry()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("test-dir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_WithTrailingSlash()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("test-dir/", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_WithBackslash()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.ToList();
Assert.Single(entries);
Assert.Equal("test-dir/subdir/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_EmptyString_IsSkipped()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
Assert.Empty(archive.Entries);
}
[Fact]
public void ZipWriter_WriteDirectory_MultipleDirectories()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("dir1", DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
writer.WriteDirectory("dir1/subdir", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/subdir/", entries[1].Key);
Assert.True(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
[Fact]
public void ZipWriter_WriteDirectory_MixedWithFiles()
{
using var memoryStream = new MemoryStream();
using (
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
)
{
writer.WriteDirectory("dir1", DateTime.Now);
using var contentStream = new MemoryStream(
System.Text.Encoding.UTF8.GetBytes("test content")
);
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
writer.WriteDirectory("dir2", DateTime.Now);
}
memoryStream.Position = 0;
using var archive = ZipArchive.Open(memoryStream);
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
Assert.Equal(3, entries.Count);
Assert.Equal("dir1/", entries[0].Key);
Assert.True(entries[0].IsDirectory);
Assert.Equal("dir1/file.txt", entries[1].Key);
Assert.False(entries[1].IsDirectory);
Assert.Equal("dir2/", entries[2].Key);
Assert.True(entries[2].IsDirectory);
}
}