Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
3cc4ccedfe Fix decompression performance by delaying stream wrapping
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 10:58:53 +00:00
copilot-swe-agent[bot]
f27ea2d5f6 Initial plan for decompression performance fix
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-10-27 10:53:39 +00:00
copilot-swe-agent[bot]
292729028b Initial plan 2025-10-27 10:45:19 +00:00
58 changed files with 190 additions and 3818 deletions

View File

@@ -3,13 +3,11 @@
This repository includes a minimal opt-in configuration and CI workflow to allow the GitHub Copilot coding agent to open and validate PRs.
- .copilot-agent.yml: opt-in config for automated agents
- .github/agents/copilot-agent.yml: detailed agent policy configuration
- .github/workflows/dotnetcore.yml: CI runs on PRs touching the solution, source, or tests to validate changes
- AGENTS.md: general instructions for Copilot coding agent with project-specific guidelines
- AGENTS.yml: general information for this project
Maintainers can adjust the allowed paths or disable the agent by editing or removing .copilot-agent.yml.
Notes:
- The agent can create, modify, and delete files within the allowed paths (src, tests, README.md, AGENTS.md)
- All changes require review before merge
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Test test project.
- Do not change any other files in the repository.
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Tests test project.

107
AGENTS.md
View File

@@ -1,24 +1,19 @@
---
description: 'Guidelines for building SharpCompress - A C# compression library'
description: 'Guidelines for building C# applications'
applyTo: '**/*.cs'
---
# SharpCompress Development
## About SharpCompress
SharpCompress is a pure C# compression library supporting multiple archive formats (Zip, Tar, GZip, BZip2, 7Zip, Rar, LZip, XZ, ZStandard) for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0. The library provides both seekable Archive APIs and forward-only Reader/Writer APIs for streaming scenarios.
# C# Development
## C# Instructions
- Always use the latest version C#, currently C# 13 features.
- Write clear and concise comments for each function.
- Follow the existing code style and patterns in the codebase.
## General Instructions
- Make only high confidence suggestions when reviewing code changes.
- Write code with good maintainability practices, including comments on why certain design decisions were made.
- Handle edge cases and write clear exception handling.
- For libraries or external dependencies, mention their usage and purpose in comments.
- Preserve backward compatibility when making changes to public APIs.
## Naming Conventions
@@ -28,26 +23,21 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
## Code Formatting
- Use CSharpier for code formatting to ensure consistent style across the project
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
- Restore tools with: `dotnet tool restore`
- Format files from the project root with: `dotnet csharpier .`
- **Run `dotnet csharpier .` from the project root after making code changes before committing**
- Configure your IDE to format on save using CSharpier for the best experience
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
- Use CSharpier for all code formatting to ensure consistent style across the project.
- Install CSharpier globally: `dotnet tool install -g csharpier`
- Format files with: `dotnet csharpier format .`
- **ALWAYS run `dotnet csharpier format .` after making code changes before committing.**
- Configure your IDE to format on save using CSharpier.
- CSharpier configuration can be customized via `.csharpierrc` file in the project root.
- Trust CSharpier's opinionated formatting decisions to maintain consistency.
## Project Setup and Structure
- The project targets multiple frameworks: .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0
- Main library is in `src/SharpCompress/`
- Tests are in `tests/SharpCompress.Test/`
- Performance tests are in `tests/SharpCompress.Performance/`
- Test archives are in `tests/TestArchives/`
- Build project is in `build/`
- Use `dotnet build` to build the solution
- Use `dotnet test` to run tests
- Solution file: `SharpCompress.sln`
- Guide users through creating a new .NET project with the appropriate templates.
- Explain the purpose of each generated file and folder to build understanding of the project structure.
- Demonstrate how to organize code using feature folders or domain-driven design principles.
- Show proper separation of concerns with models, services, and data access layers.
- Explain the Program.cs and configuration system in ASP.NET Core 9 including environment-specific settings.
## Nullable Reference Types
@@ -55,64 +45,21 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
- Always use `is null` or `is not null` instead of `== null` or `!= null`.
- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
## SharpCompress-Specific Guidelines
### Supported Formats
SharpCompress supports multiple archive and compression formats:
- **Archive Formats**: Zip, Tar, 7Zip, Rar (read-only)
- **Compression**: DEFLATE, BZip2, LZMA/LZMA2, PPMd, ZStandard (decompress only), Deflate64 (decompress only)
- **Combined Formats**: Tar.GZip, Tar.BZip2, Tar.LZip, Tar.XZ, Tar.ZStandard
- See FORMATS.md for complete format support matrix
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal
- Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal
- Always dispose of readers, writers, and archives in `using` blocks
- For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs
### Async/Await Patterns
- All I/O operations support async/await with `CancellationToken`
- Async methods follow the naming convention: `MethodNameAsync`
- Key async methods:
- `WriteEntryToAsync` - Extract entry asynchronously
- `WriteAllToDirectoryAsync` - Extract all entries asynchronously
- `WriteAsync` - Write entry asynchronously
- `WriteAllAsync` - Write directory asynchronously
- `OpenEntryStreamAsync` - Open entry stream asynchronously
- Always provide `CancellationToken` parameter in async methods
### Archive APIs vs Reader/Writer APIs
- **Archive API**: Use for random access with seekable streams (e.g., `ZipArchive`, `TarArchive`)
- **Reader API**: Use for forward-only reading on non-seekable streams (e.g., `ZipReader`, `TarReader`)
- **Writer API**: Use for forward-only writing on streams (e.g., `ZipWriter`, `TarWriter`)
- 7Zip only supports Archive API due to format limitations
### Tar-Specific Considerations
- Tar format requires file size in the header
- If no size is specified to TarWriter and the stream is not seekable, an exception will be thrown
- Tar combined with compression (GZip, BZip2, LZip, XZ) is supported
### Zip-Specific Considerations
- Supports Zip64 for large files (seekable streams only)
- Supports PKWare and WinZip AES encryption
- Multiple compression methods: None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA, PPMd
- Encrypted LZMA is not supported
### Performance Considerations
- For large files, use Reader/Writer APIs with non-seekable streams to avoid loading entire file in memory
- Leverage async I/O for better scalability
- Consider compression level trade-offs (speed vs. size)
- Use appropriate buffer sizes for stream operations
## Testing
- Always include test cases for critical paths of the application.
- Test with multiple archive formats when making changes to core functionality.
- Include tests for both Archive and Reader/Writer APIs when applicable.
- Test async operations with cancellation tokens.
- Guide users through creating unit tests.
- Do not emit "Act", "Arrange" or "Assert" comments.
- Copy existing style in nearby files for test method names and capitalization.
- Use test archives from `tests/TestArchives` directory for consistency.
- Test stream disposal and `LeaveStreamOpen` behavior.
- Test edge cases: empty archives, large files, corrupted archives, encrypted archives.
- Explain integration testing approaches for API endpoints.
- Demonstrate how to mock dependencies for effective testing.
- Show how to test authentication and authorization logic.
- Explain test-driven development principles as applied to API development.
## Performance Optimization
- Guide users on implementing caching strategies (in-memory, distributed, response caching).
- Explain asynchronous programming patterns and why they matter for API performance.
- Demonstrate pagination, filtering, and sorting for large data sets.
- Show how to implement compression and other performance optimizations.
- Explain how to measure and benchmark API performance.

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Writers;
@@ -96,9 +94,6 @@ 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,
@@ -139,22 +134,6 @@ 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
@@ -162,18 +141,6 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
SaveTo(stream, options, OldEntries, newEntries);
}
public async Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
)
{
//reset streams of new entries
newEntries.Cast<IWritableArchiveEntry>().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin));
await SaveToAsync(stream, options, OldEntries, newEntries, cancellationToken)
.ConfigureAwait(false);
}
protected TEntry CreateEntry(
string key,
Stream source,
@@ -199,8 +166,6 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
bool closeStream
);
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
protected abstract void SaveTo(
Stream stream,
WriterOptions options,
@@ -208,14 +173,6 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
IEnumerable<TEntry> newEntries
);
protected abstract Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TEntry> oldEntries,
IEnumerable<TEntry> newEntries,
CancellationToken cancellationToken = default
);
public override void Dispose()
{
base.Dispose();

View File

@@ -20,8 +20,9 @@ public static class ArchiveFactory
public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions();
var factory = FindFactory<IArchiveFactory>(stream);
stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize);
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
return factory.Open(stream, readerOptions);
}
public static IWritableArchive Create(ArchiveType type)

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.GZip;
using SharpCompress.IO;
@@ -138,16 +136,6 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
SaveTo(stream, new WriterOptions(CompressionType.GZip));
}
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
SaveToAsync(new FileInfo(filePath), cancellationToken);
public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
.ConfigureAwait(false);
}
public static bool IsGZipFile(Stream stream)
{
// read the header on the first read
@@ -185,11 +173,6 @@ 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,
@@ -213,28 +196,6 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<GZipArchiveEntry> oldEntries,
IEnumerable<GZipArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
if (Entries.Count > 1)
{
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
}
using var writer = new GZipWriter(stream, new GZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
await writer
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
.ConfigureAwait(false);
}
}
protected override IEnumerable<GZipArchiveEntry> LoadEntries(IEnumerable<GZipVolume> volumes)
{
var stream = volumes.Single().Stream;

View File

@@ -15,10 +15,9 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
{
//this is to reset the stream to be read multiple times
var part = (GZipFilePart)Parts.Single();
var rawStream = part.GetRawStream();
if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition)
if (part.GetRawStream().Position != part.EntryStartPosition)
{
rawStream.Position = part.EntryStartPosition;
part.GetRawStream().Position = part.EntryStartPosition;
}
return Parts.Single().GetCompressedStream().NotNull();
}

View File

@@ -1,6 +1,4 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
@@ -32,34 +30,6 @@ public static class IArchiveEntryExtensions
streamListener.FireEntryExtractionEnd(archiveEntry);
}
public static async Task WriteToAsync(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
CancellationToken cancellationToken = default
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
streamListener.EnsureEntriesLoaded();
streamListener.FireEntryExtractionBegin(archiveEntry);
streamListener.FireFilePartExtractionBegin(
archiveEntry.Key ?? "Key",
archiveEntry.Size,
archiveEntry.CompressedSize
);
var entryStream = archiveEntry.OpenEntryStream();
using (entryStream)
{
using Stream s = new ListeningStream(streamListener, entryStream);
await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false);
}
streamListener.FireEntryExtractionEnd(archiveEntry);
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
@@ -75,23 +45,6 @@ public static class IArchiveEntryExtensions
entry.WriteToFile
);
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public static Task WriteToDirectoryAsync(
this IArchiveEntry entry,
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
cancellationToken
);
/// <summary>
/// Extract to specific file
/// </summary>
@@ -110,24 +63,4 @@ public static class IArchiveEntryExtensions
entry.WriteTo(fs);
}
);
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public static Task WriteToFileAsync(
this IArchiveEntry entry,
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
ExtractionMethods.WriteEntryToFileAsync(
entry,
destinationFileName,
options,
async (x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
}
);
}

View File

@@ -1,7 +1,5 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -18,16 +16,8 @@ public interface IWritableArchive : IArchive
DateTime? modified = null
);
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
void SaveTo(Stream stream, WriterOptions options);
Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
);
/// <summary>
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
/// </summary>

View File

@@ -1,7 +1,5 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -44,24 +42,6 @@ public static class IWritableArchiveExtensions
writableArchive.SaveTo(stream, options);
}
public static Task SaveToAsync(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options,
CancellationToken cancellationToken = default
) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
public static async Task SaveToAsync(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options,
CancellationToken cancellationToken = default
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false);
}
public static void AddAllFromDirectory(
this IWritableArchive writableArchive,
string filePath,

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar;
using SharpCompress.Common.Tar.Headers;
@@ -224,11 +222,6 @@ 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,
@@ -237,62 +230,15 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
{
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
);
}
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TarArchiveEntry> oldEntries,
IEnumerable<TarArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
using var writer = new TarWriter(stream, new TarWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
{
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);
}
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime,
entry.Size
);
}
}

View File

@@ -9,8 +9,7 @@ namespace SharpCompress.Archives.Tar;
internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream? stream;
private readonly bool isDirectory;
private readonly Stream stream;
internal TarWritableArchiveEntry(
TarArchive archive,
@@ -28,22 +27,6 @@ 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;
@@ -64,19 +47,15 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
public override bool IsEncrypted => false;
public override bool IsDirectory => isDirectory;
public override bool IsDirectory => false;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
Stream IWritableArchiveEntry.Stream => stream;
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);
@@ -84,7 +63,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
internal override void Close()
{
if (closeStream && stream is not null)
if (closeStream)
{
stream.Dispose();
}

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
@@ -308,59 +306,14 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
{
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
);
}
}
}
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<ZipArchiveEntry> oldEntries,
IEnumerable<ZipArchiveEntry> newEntries,
CancellationToken cancellationToken = default
)
{
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
foreach (var entry in oldEntries.Concat(newEntries))
{
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);
}
using var entryStream = entry.OpenEntryStream();
writer.Write(
entry.Key.NotNull("Entry Key is null"),
entryStream,
entry.LastModifiedTime
);
}
}
@@ -372,11 +325,6 @@ 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,8 +9,7 @@ namespace SharpCompress.Archives.Zip;
internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
{
private readonly bool closeStream;
private readonly Stream? stream;
private readonly bool isDirectory;
private readonly Stream stream;
private bool isDisposed;
internal ZipWritableArchiveEntry(
@@ -28,22 +27,6 @@ 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;
@@ -64,20 +47,16 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
public override bool IsEncrypted => false;
public override bool IsDirectory => isDirectory;
public override bool IsDirectory => false;
public override bool IsSplitAfter => false;
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
Stream IWritableArchiveEntry.Stream => stream;
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);
@@ -85,7 +64,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
internal override void Close()
{
if (closeStream && !isDisposed && stream is not null)
if (closeStream && !isDisposed)
{
stream.Dispose();
isDisposed = true;

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -8,15 +7,6 @@ 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>
@@ -58,7 +48,7 @@ internal static class ExtractionMethods
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
@@ -78,7 +68,12 @@ internal static class ExtractionMethods
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
if (
!destinationFileName.StartsWith(
fullDestinationDirectoryPath,
StringComparison.Ordinal
)
)
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."
@@ -163,7 +158,7 @@ internal static class ExtractionMethods
if (!Directory.Exists(destdir))
{
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
{
throw new ExtractionException(
"Entry is trying to create a directory outside of the destination directory."
@@ -183,7 +178,12 @@ internal static class ExtractionMethods
{
destinationFileName = Path.GetFullPath(destinationFileName);
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
if (
!destinationFileName.StartsWith(
fullDestinationDirectoryPath,
StringComparison.Ordinal
)
)
{
throw new ExtractionException(
"Entry is trying to write a file outside of the destination directory."

View File

@@ -24,14 +24,8 @@ internal sealed class GZipFilePart : FilePart
stream.Position = stream.Length - 8;
ReadTrailer();
stream.Position = position;
EntryStartPosition = position;
}
else
{
// For non-seekable streams, we can't read the trailer or track position.
// Set to 0 since the stream will be read sequentially from its current position.
EntryStartPosition = 0;
}
EntryStartPosition = stream.Position;
}
internal long EntryStartPosition { get; }

View File

@@ -67,7 +67,6 @@ internal class SevenZipFilePart : FilePart
}
}
private const uint K_COPY = 0x0;
private const uint K_LZMA2 = 0x21;
private const uint K_LZMA = 0x030101;
private const uint K_PPMD = 0x030401;
@@ -83,7 +82,6 @@ internal class SevenZipFilePart : FilePart
var coder = Folder.NotNull()._coders.First();
return coder._methodId._id switch
{
K_COPY => CompressionType.None,
K_LZMA or K_LZMA2 => CompressionType.LZMA,
K_PPMD => CompressionType.PPMd,
K_B_ZIP2 => CompressionType.BZip2,

View File

@@ -36,10 +36,11 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream));
}
}
var rewindableStream = (SharpCompressStream)stream;
SharpCompressStream rewindableStream = (SharpCompressStream)stream;
while (true)
{
ZipHeader? header;
var reader = new BinaryReader(rewindableStream);
uint headerBytes = 0;
if (
@@ -154,7 +155,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
}
_lastEntryHeader = null;
var header = ReadHeader(headerBytes, reader);
header = ReadHeader(headerBytes, reader);
if (header is null)
{
yield break;

View File

@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.IO;
@@ -39,6 +39,7 @@ public sealed class Deflate64Stream : Stream, IStreamStack
private const int DEFAULT_BUFFER_SIZE = 8192;
private Stream _stream;
private CompressionMode _mode;
private InflaterManaged _inflater;
private byte[] _buffer;
@@ -61,23 +62,61 @@ public sealed class Deflate64Stream : Stream, IStreamStack
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
}
if (!stream.CanRead)
{
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
}
_inflater = new InflaterManaged(true);
_stream = stream;
_buffer = new byte[DEFAULT_BUFFER_SIZE];
InitializeInflater(stream, ZipCompressionMethod.Deflate64);
#if DEBUG_STREAMS
this.DebugConstruct(typeof(Deflate64Stream));
#endif
}
public override bool CanRead => _stream.CanRead;
/// <summary>
/// Sets up this DeflateManagedStream to be used for Inflation/Decompression
/// </summary>
private void InitializeInflater(
Stream stream,
ZipCompressionMethod method = ZipCompressionMethod.Deflate
)
{
Debug.Assert(stream != null);
Debug.Assert(
method == ZipCompressionMethod.Deflate || method == ZipCompressionMethod.Deflate64
);
if (!stream.CanRead)
{
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
}
public override bool CanWrite => false;
_inflater = new InflaterManaged(method == ZipCompressionMethod.Deflate64);
_stream = stream;
_mode = CompressionMode.Decompress;
_buffer = new byte[DEFAULT_BUFFER_SIZE];
}
public override bool CanRead
{
get
{
if (_stream is null)
{
return false;
}
return (_mode == CompressionMode.Decompress && _stream.CanRead);
}
}
public override bool CanWrite
{
get
{
if (_stream is null)
{
return false;
}
return (_mode == CompressionMode.Compress && _stream.CanWrite);
}
}
public override bool CanSeek => false;
@@ -99,6 +138,7 @@ public sealed class Deflate64Stream : Stream, IStreamStack
public override int Read(byte[] array, int offset, int count)
{
EnsureDecompressionMode();
ValidateParameters(array, offset, count);
EnsureNotDisposed();
@@ -145,106 +185,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
return count - remainingCount;
}
public override async Task<int> ReadAsync(
byte[] array,
int offset,
int count,
CancellationToken cancellationToken
)
{
ValidateParameters(array, offset, count);
EnsureNotDisposed();
int bytesRead;
var currentOffset = offset;
var remainingCount = count;
while (true)
{
bytesRead = _inflater.Inflate(array, currentOffset, remainingCount);
currentOffset += bytesRead;
remainingCount -= bytesRead;
if (remainingCount == 0)
{
break;
}
if (_inflater.Finished())
{
// if we finished decompressing, we can't have anything left in the outputwindow.
Debug.Assert(
_inflater.AvailableOutput == 0,
"We should have copied all stuff out!"
);
break;
}
var bytes = await _stream
.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken)
.ConfigureAwait(false);
if (bytes <= 0)
{
break;
}
else if (bytes > _buffer.Length)
{
// The stream is either malicious or poorly implemented and returned a number of
// bytes larger than the buffer supplied to it.
throw new InvalidFormatException("Deflate64: invalid data");
}
_inflater.SetInput(_buffer, 0, bytes);
}
return count - remainingCount;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
EnsureNotDisposed();
// InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays
// For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available
if (
System.Runtime.InteropServices.MemoryMarshal.TryGetArray<byte>(
buffer,
out var arraySegment
)
)
{
// Fast path: the Memory<byte> is backed by an array
return await ReadAsync(
arraySegment.Array!,
arraySegment.Offset,
arraySegment.Count,
cancellationToken
)
.ConfigureAwait(false);
}
else
{
// Slow path: rent a temporary array
var tempBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer);
return bytesRead;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(tempBuffer);
}
}
}
#endif
private void ValidateParameters(byte[] array, int offset, int count)
{
if (array is null)
@@ -280,6 +220,26 @@ public sealed class Deflate64Stream : Stream, IStreamStack
private static void ThrowStreamClosedException() =>
throw new ObjectDisposedException(null, "Deflate64: stream has been disposed");
private void EnsureDecompressionMode()
{
if (_mode != CompressionMode.Decompress)
{
ThrowCannotReadFromDeflateManagedStreamException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCannotReadFromDeflateManagedStreamException() =>
throw new InvalidOperationException("Deflate64: cannot read from this stream");
private void EnsureCompressionMode()
{
if (_mode != CompressionMode.Compress)
{
ThrowCannotWriteToDeflateManagedStreamException();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowCannotWriteToDeflateManagedStreamException() =>
throw new InvalidOperationException("Deflate64: cannot write to this stream");
@@ -321,17 +281,20 @@ public sealed class Deflate64Stream : Stream, IStreamStack
#endif
if (disposing)
{
_stream.Dispose();
_stream?.Dispose();
}
}
finally
{
_stream = null;
try
{
_inflater.Dispose();
_inflater?.Dispose();
}
finally
{
_inflater = null;
base.Dispose(disposing);
}
}

View File

@@ -1,13 +1,12 @@
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 : SharpCompressException
internal class DataErrorException : Exception
{
public DataErrorException()
: base("Data Error") { }
@@ -16,7 +15,7 @@ internal class DataErrorException : SharpCompressException
/// <summary>
/// The exception that is thrown when the value of an argument is outside the allowable range.
/// </summary>
internal class InvalidParamException : SharpCompressException
internal class InvalidParamException : Exception
{
public InvalidParamException()
: base("Invalid Parameter") { }

View File

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

View File

@@ -1,8 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.IO;
@@ -95,47 +93,6 @@ internal class ReadOnlySubStream : SharpCompressStream, IStreamStack
}
#endif
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (BytesLeftToRead < count)
{
count = (int)BytesLeftToRead;
}
var read = await Stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_position += read;
}
return read;
}
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length;
var read = await Stream
.ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken)
.ConfigureAwait(false);
if (read > 0)
{
BytesLeftToRead -= read;
_position += read;
}
return read;
}
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();

View File

@@ -96,33 +96,6 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
return false;
}
public async Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default)
{
if (_completed)
{
return false;
}
if (Cancelled)
{
throw new ReaderCancelledException("Reader has been cancelled.");
}
if (_entriesForCurrentReadStream is null)
{
return LoadStreamForReading(RequestInitialStream());
}
if (!_wroteCurrentEntry)
{
await SkipEntryAsync(cancellationToken).ConfigureAwait(false);
}
_wroteCurrentEntry = false;
if (NextEntryForCurrentStream())
{
return true;
}
_completed = true;
return false;
}
protected bool LoadStreamForReading(Stream stream)
{
_entriesForCurrentReadStream?.Dispose();
@@ -156,14 +129,6 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
}
}
private async Task SkipEntryAsync(CancellationToken cancellationToken)
{
if (!Entry.IsDirectory)
{
await SkipAsync(cancellationToken).ConfigureAwait(false);
}
}
private void Skip()
{
var part = Entry.Parts.First();
@@ -186,33 +151,6 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
s.SkipEntry();
}
private async Task SkipAsync(CancellationToken cancellationToken)
{
var part = Entry.Parts.First();
if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0)
{
//not solid and has a known compressed size then we can skip raw bytes.
var rawStream = part.GetRawStream();
if (rawStream != null)
{
var bytesToAdvance = Entry.CompressedSize;
await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false);
part.Skipped = true;
return;
}
}
//don't know the size so we have to try to decompress to skip
#if NETFRAMEWORK || NETSTANDARD2_0
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#else
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
#endif
}
public void WriteEntryTo(Stream writableStream)
{
if (_wroteCurrentEntry)
@@ -294,19 +232,6 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
return stream;
}
public Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
if (_wroteCurrentEntry)
{
throw new ArgumentException(
"WriteEntryToAsync or OpenEntryStreamAsync can only be called once."
);
}
var stream = GetEntryStream();
_wroteCurrentEntry = true;
return Task.FromResult(stream);
}
/// <summary>
/// Retains a reference to the entry stream, so we can check whether it completed later.
/// </summary>

View File

@@ -39,23 +39,9 @@ public interface IReader : IDisposable
/// <returns></returns>
bool MoveToNextEntry();
/// <summary>
/// Moves to the next entry asynchronously by reading more data from the underlying stream. This skips if data has not been read.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
EntryStream OpenEntryStream();
/// <summary>
/// Opens the current entry asynchronously as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
/// <param name="cancellationToken"></param>
Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
}

View File

@@ -37,20 +37,6 @@ 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,7 +50,4 @@ 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,10 +16,4 @@ 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,9 +55,6 @@ 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,
@@ -124,10 +121,4 @@ 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

@@ -1,7 +1,5 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
@@ -74,44 +72,6 @@ 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)
@@ -131,40 +91,6 @@ public class TarWriter : AbstractWriter
PadTo512(size.Value);
}
public override async Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
CancellationToken cancellationToken = default
) => await WriteAsync(filename, source, modificationTime, null, cancellationToken);
public async Task WriteAsync(
string filename,
Stream source,
DateTime? modificationTime,
long? size,
CancellationToken cancellationToken = default
)
{
if (!source.CanSeek && size is null)
{
throw new ArgumentException("Seekable stream is required if no size is given.");
}
var realSize = size ?? source.Length;
var header = new TarHeader(WriterOptions.ArchiveEncoding);
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
header.Name = NormalizeFilename(filename);
header.Size = realSize;
header.Write(OutputStream);
var written = await source
.TransferToAsync(OutputStream, realSize, cancellationToken)
.ConfigureAwait(false);
PadTo512(written);
}
private void PadTo512(long size)
{
var zeros = unchecked((int)(((size + 511L) & ~511L) - size));

View File

@@ -3,8 +3,6 @@ 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;
@@ -137,73 +135,6 @@ 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

@@ -335,9 +335,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.17, )",
"resolved": "8.0.17",
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
"requested": "[8.0.20, )",
"resolved": "8.0.20",
"contentHash": "Rhcto2AjGvTO62+/VTmBpumBOmqIGp7nYEbTbmEXkCq4yPGxV8whju3/HsIA/bKyo2+DggaYk5+/8sxb1AbPTw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Compressors.Xz;
@@ -570,56 +569,4 @@ public class ArchiveTests : ReaderTests
return (extractedData, crc);
}
protected async Task ArchiveStreamReadAsync(
string testArchive,
ReaderOptions? readerOptions = null
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
await ArchiveStreamReadAsync(
ArchiveFactory.AutoFactory,
readerOptions,
new[] { testArchive }
);
}
protected async Task ArchiveStreamReadAsync(
IArchiveFactory archiveFactory,
ReaderOptions? readerOptions,
IEnumerable<string> testArchives
)
{
foreach (var path in testArchives)
{
using (
var stream = SharpCompressStream.Create(
File.OpenRead(path),
leaveOpen: true,
throwOnDispose: true
)
)
using (var archive = archiveFactory.Open(stream, readerOptions))
{
try
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
await entry.WriteToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
catch (IndexOutOfRangeException)
{
//SevenZipArchive_BZip2_Split test needs this
stream.ThrowOnDispose = false;
throw;
}
stream.ThrowOnDispose = false;
}
VerifyFiles();
}
}
}

View File

@@ -1,116 +0,0 @@
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

@@ -1,99 +0,0 @@
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

@@ -1,127 +0,0 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveAsyncTests : ArchiveTests
{
public GZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task GZip_Archive_Generic_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
using (var archive = ArchiveFactory.Open(stream))
{
var entry = archive.Entries.First();
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
var size = entry.Size;
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
Assert.Equal(size, scratch.Length);
Assert.Equal(size, test.Length);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
);
}
[Fact]
public async Task GZip_Archive_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
using (var archive = GZipArchive.Open(stream))
{
var entry = archive.Entries.First();
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
var size = entry.Size;
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
Assert.Equal(size, scratch.Length);
Assert.Equal(size, test.Length);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
);
}
[Fact]
public async Task GZip_Archive_NoAdd_Async()
{
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
Assert.Throws<InvalidFormatException>(() => archive.AddEntry("jpg\\test.jpg", jpg));
await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
}
[Fact]
public async Task GZip_Archive_Multiple_Reads_Async()
{
var inputStream = new MemoryStream();
using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
{
await fileStream.CopyToAsync(inputStream);
inputStream.Position = 0;
}
using var archive = GZipArchive.Open(inputStream);
var archiveEntry = archive.Entries.First();
MemoryStream tarStream;
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
var size = tarStream.Length;
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
Assert.Equal(size, tarStream.Length);
using (var entryStream = archiveEntry.OpenEntryStream())
{
var result = TarArchive.IsTarFile(entryStream);
Assert.True(result);
}
Assert.Equal(size, tarStream.Length);
using (var entryStream = archiveEntry.OpenEntryStream())
{
tarStream = new MemoryStream();
await entryStream.CopyToAsync(tarStream);
}
Assert.Equal(size, tarStream.Length);
}
[Fact]
public void TestGzCrcWithMostSignificantBitNotNegative_Async()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
Assert.InRange(entry.Crc, 0L, 0xFFFFFFFFL);
}
}
[Fact]
public void TestGzArchiveTypeGzip_Async()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var archive = GZipArchive.Open(stream);
Assert.Equal(archive.Type, ArchiveType.GZip);
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
@@ -125,60 +124,4 @@ public class GZipArchiveTests : ArchiveTests
using var archive = GZipArchive.Open(stream);
Assert.Equal(archive.Type, ArchiveType.GZip);
}
[Fact]
public void GZip_Archive_NonSeekableStream()
{
// Test that GZip extraction works with non-seekable streams (like HttpBaseStream)
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
var buffer = new MemoryStream();
fileStream.CopyTo(buffer);
buffer.Position = 0;
// Create a non-seekable wrapper around the MemoryStream
using var nonSeekableStream = new NonSeekableStream(buffer);
using var reader = SharpCompress.Readers.GZip.GZipReader.Open(nonSeekableStream);
// Verify we can move to the first entry and read it without exceptions
Assert.True(reader.MoveToNextEntry());
Assert.NotNull(reader.Entry);
// Extract and verify the entry can be read
using var outputStream = new MemoryStream();
reader.WriteEntryTo(outputStream);
Assert.True(outputStream.Length > 0);
}
// Helper class to simulate a non-seekable stream like HttpBaseStream
private class NonSeekableStream : Stream
{
private readonly Stream _baseStream;
public NonSeekableStream(Stream baseStream) => _baseStream = baseStream;
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => false; // Simulate non-seekable stream
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush() => _baseStream.Flush();
public override int Read(byte[] buffer, int offset, int count) =>
_baseStream.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
}

View File

@@ -1,83 +0,0 @@
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipWriterAsyncTests : WriterTests
{
public GZipWriterAsyncTests()
: base(ArchiveType.GZip) => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task GZip_Writer_Generic_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.GZip))
{
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
[Fact]
public async Task GZip_Writer_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = new GZipWriter(stream))
{
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
[Fact]
public void GZip_Writer_Generic_Bad_Compression_Async() =>
Assert.Throws<InvalidFormatException>(() =>
{
using Stream stream = File.OpenWrite(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
using var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.BZip2);
});
[Fact]
public async Task GZip_Writer_Entry_Path_With_Dir_Async()
{
using (
Stream stream = File.Open(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
FileMode.OpenOrCreate,
FileAccess.Write
)
)
using (var writer = new GZipWriter(stream))
{
var path = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
await writer.WriteAsync(path, path);
}
CompareArchivesByPath(
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
);
}
}

View File

@@ -1,19 +0,0 @@
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

@@ -1,7 +1,5 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -59,7 +57,7 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override bool CanWrite => false;
public override void Flush() { }
@@ -74,41 +72,10 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
public override int Read(byte[] buffer, int offset, int count) =>
stream.Read(buffer, offset, count);
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => stream.ReadAsync(buffer, offset, count, cancellationToken);
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => stream.ReadAsync(buffer, cancellationToken);
#endif
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
stream.Write(buffer, offset, count);
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => stream.WriteAsync(buffer, offset, count, cancellationToken);
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
) => stream.WriteAsync(buffer, cancellationToken);
#endif
public override Task FlushAsync(CancellationToken cancellationToken) =>
stream.FlushAsync(cancellationToken);
throw new NotSupportedException();
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -74,72 +72,6 @@ public abstract class ReaderTests : TestBase
}
}
protected async Task ReadAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
options ??= new ReaderOptions() { BufferSize = 0x20000 };
options.LeaveStreamOpen = true;
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
options.LeaveStreamOpen = false;
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
VerifyFiles();
}
private async Task ReadImplAsync(
string testArchive,
CompressionType expectedCompression,
ReaderOptions options,
CancellationToken cancellationToken = default
)
{
using var file = File.OpenRead(testArchive);
using var protectedStream = SharpCompressStream.Create(
new ForwardOnlyStream(file, options.BufferSize),
leaveOpen: true,
throwOnDispose: true,
bufferSize: options.BufferSize
);
using var testStream = new TestStream(protectedStream);
using (var reader = ReaderFactory.Open(testStream, options))
{
await UseReaderAsync(reader, expectedCompression, cancellationToken);
protectedStream.ThrowOnDispose = false;
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
}
var message =
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
}
public async Task UseReaderAsync(
IReader reader,
CompressionType expectedCompression,
CancellationToken cancellationToken = default
)
{
while (await reader.MoveToNextEntryAsync(cancellationToken))
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(expectedCompression, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken
);
}
}
}
protected void Iterate(
string testArchive,
string fileOrder,

View File

@@ -122,25 +122,6 @@ public class SevenZipArchiveTests : ArchiveTests
//"7Zip.BZip2.split.006",
//"7Zip.BZip2.split.007"
[Fact]
public void SevenZipArchive_Copy_StreamRead() => ArchiveStreamRead("7Zip.Copy.7z");
[Fact]
public void SevenZipArchive_Copy_PathRead() => ArchiveFileRead("7Zip.Copy.7z");
[Fact]
public void SevenZipArchive_Copy_CompressionType()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.Copy.7z")))
using (var archive = SevenZipArchive.Open(stream))
{
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
Assert.Equal(CompressionType.None, entry.CompressionType);
}
}
}
[Fact]
public void SevenZipArchive_ZSTD_StreamRead() => ArchiveStreamRead("7Zip.ZSTD.7z");

View File

@@ -12,9 +12,6 @@
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<DefineConstants>$(DefineConstants);LINUX</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
</ItemGroup>

View File

@@ -1,223 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;
using SharpCompress.Writers;
using SharpCompress.Writers.Tar;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarArchiveAsyncTests : ArchiveTests
{
public TarArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar");
[Fact]
public async Task Tar_FileName_Exactly_100_Characters_Async()
{
var archive = "Tar_FileName_Exactly_100_Characters.tar";
// create the 100 char filename
var filename =
"filename_with_exactly_100_characters_______________________________________________________________X";
// Step 1: create a tar file containing a file with the test name
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None))
using (Stream inputStream = new MemoryStream())
{
var sw = new StreamWriter(inputStream);
await sw.WriteAsync("dummy filecontent");
await sw.FlushAsync();
inputStream.Position = 0;
await writer.WriteAsync(filename, inputStream, null);
}
// Step 2: check if the written tar file can be read correctly
var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive);
using (var archive2 = TarArchive.Open(unmodified))
{
Assert.Equal(1, archive2.Entries.Count);
Assert.Contains(filename, archive2.Entries.Select(entry => entry.Key));
foreach (var entry in archive2.Entries)
{
Assert.Equal(
"dummy filecontent",
await new StreamReader(entry.OpenEntryStream()).ReadLineAsync()
);
}
}
}
[Fact]
public async Task Tar_VeryLongFilepathReadback_Async()
{
var archive = "Tar_VeryLongFilepathReadback.tar";
// create a very long filename
var longFilename = "";
for (var i = 0; i < 600; i = longFilename.Length)
{
longFilename += i.ToString("D10") + "-";
}
longFilename += ".txt";
// Step 1: create a tar file containing a file with a long name
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None))
using (Stream inputStream = new MemoryStream())
{
var sw = new StreamWriter(inputStream);
await sw.WriteAsync("dummy filecontent");
await sw.FlushAsync();
inputStream.Position = 0;
await writer.WriteAsync(longFilename, inputStream, null);
}
// Step 2: check if the written tar file can be read correctly
var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive);
using (var archive2 = TarArchive.Open(unmodified))
{
Assert.Equal(1, archive2.Entries.Count);
Assert.Contains(longFilename, archive2.Entries.Select(entry => entry.Key));
foreach (var entry in archive2.Entries)
{
Assert.Equal(
"dummy filecontent",
await new StreamReader(entry.OpenEntryStream()).ReadLineAsync()
);
}
}
}
[Fact]
public async Task Tar_Create_New_Async()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.tar");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
using (var archive = TarArchive.Create())
{
archive.AddAllFromDirectory(ORIGINAL_FILES_PATH);
var twopt = new TarWriterOptions(CompressionType.None, true);
twopt.ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(866) };
await archive.SaveToAsync(scratchPath, twopt);
}
CompareArchivesByPath(unmodified, scratchPath);
}
[Fact]
public async Task Tar_Random_Write_Add_Async()
{
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar");
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
using (var archive = TarArchive.Open(unmodified))
{
archive.AddEntry("jpg\\test.jpg", jpg);
await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None));
}
CompareArchivesByPath(modified, scratchPath);
}
[Fact]
public async Task Tar_Random_Write_Remove_Async()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar");
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
using (var archive = TarArchive.Open(unmodified))
{
var entry = archive.Entries.Single(x =>
x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase)
);
archive.RemoveEntry(entry);
await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None));
}
CompareArchivesByPath(modified, scratchPath);
}
[Theory]
[InlineData(10)]
[InlineData(128)]
public async Task Tar_Japanese_Name_Async(int length)
{
using var mstm = new MemoryStream();
var enc = new ArchiveEncoding { Default = Encoding.UTF8 };
var twopt = new TarWriterOptions(CompressionType.None, true);
twopt.ArchiveEncoding = enc;
var fname = new string((char)0x3042, length);
using (var tw = new TarWriter(mstm, twopt))
using (var input = new MemoryStream(new byte[32]))
{
await tw.WriteAsync(fname, input, null);
}
using (var inputMemory = new MemoryStream(mstm.ToArray()))
{
var tropt = new ReaderOptions { ArchiveEncoding = enc };
using (var tr = TarReader.Open(inputMemory, tropt))
{
while (tr.MoveToNextEntry())
{
Assert.Equal(fname, tr.Entry.Key);
}
}
}
}
[Fact]
public async Task Tar_Read_One_At_A_Time_Async()
{
var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 };
var tarWriterOptions = new TarWriterOptions(CompressionType.None, true)
{
ArchiveEncoding = archiveEncoding,
};
var testBytes = Encoding.UTF8.GetBytes("This is a test.");
using var memoryStream = new MemoryStream();
using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions))
using (var testFileStream = new MemoryStream(testBytes))
{
await tarWriter.WriteAsync("test1.txt", testFileStream, null);
testFileStream.Position = 0;
await tarWriter.WriteAsync("test2.txt", testFileStream, null);
}
memoryStream.Position = 0;
var numberOfEntries = 0;
using (var archiveFactory = TarArchive.Open(memoryStream))
{
foreach (var entry in archiveFactory.Entries)
{
++numberOfEntries;
using var tarEntryStream = entry.OpenEntryStream();
using var testFileStream = new MemoryStream();
await tarEntryStream.CopyToAsync(testFileStream);
Assert.Equal(testBytes.Length, testFileStream.Length);
}
}
Assert.Equal(2, numberOfEntries);
}
}

View File

@@ -1,112 +0,0 @@
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

@@ -1,272 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarReaderAsyncTests : ReaderTests
{
public TarReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task Tar_Reader_Async() => await ReadAsync("Tar.tar", CompressionType.None);
[Fact]
public async Task Tar_Skip_Async()
{
using Stream stream = new ForwardOnlyStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"))
);
using var reader = ReaderFactory.Open(stream);
var x = 0;
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
x++;
if (x % 2 == 0)
{
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
}
[Fact]
public async Task Tar_Z_Reader_Async() => await ReadAsync("Tar.tar.Z", CompressionType.Lzw);
[Fact]
public async Task Tar_BZip2_Reader_Async() =>
await ReadAsync("Tar.tar.bz2", CompressionType.BZip2);
[Fact]
public async Task Tar_GZip_Reader_Async() =>
await ReadAsync("Tar.tar.gz", CompressionType.GZip);
[Fact]
public async Task Tar_ZStandard_Reader_Async() =>
await ReadAsync("Tar.tar.zst", CompressionType.ZStandard);
[Fact]
public async Task Tar_LZip_Reader_Async() =>
await ReadAsync("Tar.tar.lz", CompressionType.LZip);
[Fact]
public async Task Tar_Xz_Reader_Async() => await ReadAsync("Tar.tar.xz", CompressionType.Xz);
[Fact]
public async Task Tar_GZip_OldGnu_Reader_Async() =>
await ReadAsync("Tar.oldgnu.tar.gz", CompressionType.GZip);
[Fact]
public async Task Tar_BZip2_Entry_Stream_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2")))
using (var reader = TarReader.Open(stream))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
using var entryStream = reader.OpenEntryStream();
var file = Path.GetFileName(reader.Entry.Key);
var folder =
Path.GetDirectoryName(reader.Entry.Key)
?? throw new ArgumentNullException();
var destdir = Path.Combine(SCRATCH_FILES_PATH, folder);
if (!Directory.Exists(destdir))
{
Directory.CreateDirectory(destdir);
}
var destinationFileName = Path.Combine(destdir, file.NotNull());
using var fs = File.OpenWrite(destinationFileName);
await entryStream.CopyToAsync(fs);
}
}
}
VerifyFiles();
}
[Fact]
public void Tar_LongNamesWithLongNameExtension_Async()
{
var filePaths = new List<string>();
using (
Stream stream = File.OpenRead(
Path.Combine(TEST_ARCHIVES_PATH, "Tar.LongPathsWithLongNameExtension.tar")
)
)
using (var reader = TarReader.Open(stream))
{
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
filePaths.Add(reader.Entry.Key.NotNull("Entry Key is null"));
}
}
}
Assert.Equal(3, filePaths.Count);
Assert.Contains("a.txt", filePaths);
Assert.Contains(
"wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php",
filePaths
);
Assert.Contains(
"wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Foo.php",
filePaths
);
}
[Fact]
public void Tar_BZip2_Skip_Entry_Stream_Async()
{
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"));
using var reader = TarReader.Open(stream);
var names = new List<string>();
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
using var entryStream = reader.OpenEntryStream();
entryStream.SkipEntry();
names.Add(reader.Entry.Key.NotNull());
}
}
Assert.Equal(3, names.Count);
}
[Fact]
public void Tar_Containing_Rar_Reader_Async()
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsRar.tar");
using Stream stream = File.OpenRead(archiveFullPath);
using var reader = ReaderFactory.Open(stream);
Assert.True(reader.ArchiveType == ArchiveType.Tar);
}
[Fact]
public void Tar_With_TarGz_With_Flushed_EntryStream_Async()
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsTarGz.tar");
using Stream stream = File.OpenRead(archiveFullPath);
using var reader = ReaderFactory.Open(stream);
Assert.True(reader.MoveToNextEntry());
Assert.Equal("inner.tar.gz", reader.Entry.Key);
using var entryStream = reader.OpenEntryStream();
using var flushingStream = new FlushOnDisposeStream(entryStream);
// Extract inner.tar.gz
using var innerReader = ReaderFactory.Open(flushingStream);
Assert.True(innerReader.MoveToNextEntry());
Assert.Equal("test", innerReader.Entry.Key);
}
[Fact]
public async Task Tar_Broken_Stream_Async()
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
using Stream stream = File.OpenRead(archiveFullPath);
using var reader = ReaderFactory.Open(stream);
var memoryStream = new MemoryStream();
Assert.True(reader.MoveToNextEntry());
Assert.True(reader.MoveToNextEntry());
await reader.WriteEntryToAsync(memoryStream);
stream.Close();
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
}
[Fact]
public async Task Tar_Corrupted_Async()
{
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "TarCorrupted.tar");
using Stream stream = File.OpenRead(archiveFullPath);
using var reader = ReaderFactory.Open(stream);
var memoryStream = new MemoryStream();
Assert.True(reader.MoveToNextEntry());
Assert.True(reader.MoveToNextEntry());
await reader.WriteEntryToAsync(memoryStream);
stream.Close();
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
}
#if !NETFRAMEWORK
[Fact]
public async Task Tar_GZip_With_Symlink_Entries_Async()
{
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
System.Runtime.InteropServices.OSPlatform.Windows
);
using Stream stream = File.OpenRead(
Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz")
);
using var reader = TarReader.Open(stream);
while (reader.MoveToNextEntry())
{
if (reader.Entry.IsDirectory)
{
continue;
}
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true,
WriteSymbolicLink = (sourcePath, targetPath) =>
{
if (!isWindows)
{
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
if (File.Exists(sourcePath))
{
link.Delete(); // equivalent to ln -s -f
}
link.CreateSymbolicLinkTo(targetPath);
}
},
}
);
if (!isWindows)
{
if (reader.Entry.LinkTarget != null)
{
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
if (link.HasContents)
{
// need to convert the link to an absolute path for comparison
var target = reader.Entry.LinkTarget;
var realTarget = Path.GetFullPath(
Path.Combine($"{Path.GetDirectoryName(path)}", target)
);
Assert.Equal(realTarget, link.GetContents().ToString());
}
else
{
Assert.True(false, "Symlink has no target");
}
}
}
}
}
#endif
}

View File

@@ -201,10 +201,13 @@ public class TarReaderTests : ReaderTests
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
}
#if LINUX
#if !NETFRAMEWORK
[Fact]
public void Tar_GZip_With_Symlink_Entries()
{
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
System.Runtime.InteropServices.OSPlatform.Windows
);
using Stream stream = File.OpenRead(
Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz")
);
@@ -223,32 +226,38 @@ public class TarReaderTests : ReaderTests
Overwrite = true,
WriteSymbolicLink = (sourcePath, targetPath) =>
{
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
if (File.Exists(sourcePath))
if (!isWindows)
{
link.Delete(); // equivalent to ln -s -f
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
if (File.Exists(sourcePath))
{
link.Delete(); // equivalent to ln -s -f
}
link.CreateSymbolicLinkTo(targetPath);
}
link.CreateSymbolicLinkTo(targetPath);
},
}
);
if (reader.Entry.LinkTarget != null)
if (!isWindows)
{
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
if (link.HasContents)
if (reader.Entry.LinkTarget != null)
{
// need to convert the link to an absolute path for comparison
var target = reader.Entry.LinkTarget;
var realTarget = Path.GetFullPath(
Path.Combine($"{Path.GetDirectoryName(path)}", target)
);
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
if (link.HasContents)
{
// need to convert the link to an absolute path for comparison
var target = reader.Entry.LinkTarget;
var realTarget = Path.GetFullPath(
Path.Combine($"{Path.GetDirectoryName(path)}", target)
);
Assert.Equal(realTarget, link.GetContents().ToString());
}
else
{
Assert.True(false, "Symlink has no target");
Assert.Equal(realTarget, link.GetContents().ToString());
}
else
{
Assert.True(false, "Symlink has no target");
}
}
}
}

View File

@@ -1,83 +0,0 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Writers.Tar;
using Xunit;
namespace SharpCompress.Test.Tar;
public class TarWriterAsyncTests : WriterTests
{
static TarWriterAsyncTests()
{
#if !NETFRAMEWORK
//fix issue where these tests could not be ran in isolation
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
#endif
}
public TarWriterAsyncTests()
: base(ArchiveType.Tar) => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task Tar_Writer_Async() =>
await WriteAsync(
CompressionType.None,
"Tar.noEmptyDirs.tar",
"Tar.noEmptyDirs.tar",
Encoding.GetEncoding(866)
);
[Fact]
public async Task Tar_BZip2_Writer_Async() =>
await WriteAsync(
CompressionType.BZip2,
"Tar.noEmptyDirs.tar.bz2",
"Tar.noEmptyDirs.tar.bz2",
Encoding.GetEncoding(866)
);
[Fact]
public async Task Tar_LZip_Writer_Async() =>
await WriteAsync(
CompressionType.LZip,
"Tar.noEmptyDirs.tar.lz",
"Tar.noEmptyDirs.tar.lz",
Encoding.GetEncoding(866)
);
[Fact]
public async Task Tar_Rar_Write_Async() =>
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
await WriteAsync(
CompressionType.Rar,
"Zip.ppmd.noEmptyDirs.zip",
"Zip.ppmd.noEmptyDirs.zip"
)
);
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Tar_Finalize_Archive_Async(bool finalizeArchive)
{
using var stream = new MemoryStream();
using Stream content = File.OpenRead(Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"));
using (
var writer = new TarWriter(
stream,
new TarWriterOptions(CompressionType.None, finalizeArchive)
)
)
{
await writer.WriteAsync("doesn't matter", content, null);
}
var paddedContentWithHeader = (content.Length / 512 * 512) + 512 + 512;
var expectedStreamLength = finalizeArchive
? paddedContentWithHeader + (512 * 2)
: paddedContentWithHeader;
Assert.Equal(expectedStreamLength, stream.Length);
}
}

View File

@@ -1,164 +0,0 @@
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

@@ -1,7 +1,5 @@
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -53,49 +51,4 @@ public class WriterTests : TestBase
}
VerifyFiles();
}
protected async Task WriteAsync(
CompressionType compressionType,
string archive,
string archiveToVerifyAgainst,
Encoding? encoding = null,
CancellationToken cancellationToken = default
)
{
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
{
var writerOptions = new WriterOptions(compressionType) { LeaveStreamOpen = true };
writerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default;
using var writer = WriterFactory.Open(stream, _type, writerOptions);
await writer.WriteAllAsync(
ORIGINAL_FILES_PATH,
"*",
SearchOption.AllDirectories,
cancellationToken
);
}
CompareArchivesByPath(
Path.Combine(SCRATCH2_FILES_PATH, archive),
Path.Combine(TEST_ARCHIVES_PATH, archiveToVerifyAgainst)
);
using (Stream stream = File.OpenRead(Path.Combine(SCRATCH2_FILES_PATH, archive)))
{
var readerOptions = new ReaderOptions();
readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default;
using var reader = ReaderFactory.Open(
SharpCompressStream.Create(stream, leaveOpen: true),
readerOptions
);
reader.WriteAllToDirectory(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true }
);
}
VerifyFiles();
}
}

View File

@@ -1,242 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Readers;
using SharpCompress.Readers.Zip;
using SharpCompress.Test.Mocks;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class Zip64AsyncTests : WriterTests
{
public Zip64AsyncTests()
: base(ArchiveType.Zip) { }
// 4GiB + 1
private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1;
//[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Single_Large_File_Async() =>
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
//[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Two_Large_Files_Async() =>
await RunSingleTestAsync(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Two_Small_files_Async() =>
// Multiple files, does not require zip64
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Two_Small_files_stream_Async() =>
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true);
[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Two_Small_Files_Zip64_Async() =>
// Multiple files, use zip64 even though it is not required
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public async Task Zip64_Single_Large_File_Fail_Async()
{
try
{
// One single file, should fail
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: false);
throw new InvalidOperationException("Test did not fail?");
}
catch (NotSupportedException) { }
}
[Fact]
[Trait("zip64", "true")]
public async Task Zip64_Single_Large_File_Zip64_Streaming_Fail_Async()
{
try
{
// One single file, should fail (fast) with zip64
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: true);
throw new InvalidOperationException("Test did not fail?");
}
catch (NotSupportedException) { }
}
[Fact]
[Trait("zip64", "true")]
public async Task Zip64_Single_Large_File_Streaming_Fail_Async()
{
try
{
// One single file, should fail once the write discovers the problem
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: true);
throw new InvalidOperationException("Test did not fail?");
}
catch (NotSupportedException) { }
}
public async Task RunSingleTestAsync(
long files,
long filesize,
bool setZip64,
bool forwardOnly,
long writeChunkSize = 1024 * 1024,
string filename = "zip64-test-async.zip"
)
{
filename = Path.Combine(SCRATCH2_FILES_PATH, filename);
try
{
if (File.Exists(filename))
{
File.Delete(filename);
}
if (!File.Exists(filename))
{
await CreateZipArchiveAsync(
filename,
files,
filesize,
writeChunkSize,
setZip64,
forwardOnly
);
}
var resForward = await ReadForwardOnlyAsync(filename);
if (resForward.Item1 != files)
{
throw new InvalidOperationException(
$"Incorrect number of items reported: {resForward.Item1}, should have been {files}"
);
}
if (resForward.Item2 != files * filesize)
{
throw new InvalidOperationException(
$"Incorrect combined size reported: {resForward.Item2}, should have been {files * filesize}"
);
}
var resArchive = ReadArchive(filename);
if (resArchive.Item1 != files)
{
throw new InvalidOperationException(
$"Incorrect number of items reported: {resArchive.Item1}, should have been {files}"
);
}
if (resArchive.Item2 != files * filesize)
{
throw new InvalidOperationException(
$"Incorrect number of items reported: {resArchive.Item2}, should have been {files * filesize}"
);
}
}
finally
{
if (File.Exists(filename))
{
File.Delete(filename);
}
}
}
public async Task CreateZipArchiveAsync(
string filename,
long files,
long filesize,
long chunksize,
bool setZip64,
bool forwardOnly
)
{
var data = new byte[chunksize];
// Use deflate for speed
var opts = new ZipWriterOptions(CompressionType.Deflate) { UseZip64 = setZip64 };
// Use no compression to ensure we hit the limits (actually inflates a bit, but seems better than using method==Store)
var eo = new ZipWriterEntryOptions { DeflateCompressionLevel = CompressionLevel.None };
using var zip = File.OpenWrite(filename);
using var st = forwardOnly ? (Stream)new ForwardOnlyStream(zip) : zip;
using var zipWriter = (ZipWriter)WriterFactory.Open(st, ArchiveType.Zip, opts);
for (var i = 0; i < files; i++)
{
using var str = zipWriter.WriteToStream(i.ToString(), eo);
var left = filesize;
while (left > 0)
{
var b = (int)Math.Min(left, data.Length);
// Use synchronous Write to match the sync version and avoid ForwardOnlyStream issues
await str.WriteAsync(data, 0, b);
left -= b;
}
}
}
public async Task<Tuple<long, long>> ReadForwardOnlyAsync(string filename)
{
long count = 0;
long size = 0;
ZipEntry? prev = null;
using (var fs = File.OpenRead(filename))
using (var rd = ZipReader.Open(fs, new ReaderOptions { LookForHeader = false }))
{
while (await rd.MoveToNextEntryAsync())
{
#if NETFRAMEWORK || NETSTANDARD2_0
using (var entryStream = await rd.OpenEntryStreamAsync())
{
await entryStream.SkipEntryAsync();
}
#else
await using (var entryStream = await rd.OpenEntryStreamAsync())
{
await entryStream.SkipEntryAsync();
}
#endif
count++;
if (prev != null)
{
size += prev.Size;
}
prev = rd.Entry;
}
}
if (prev != null)
{
size += prev.Size;
}
return new Tuple<long, long>(count, size);
}
public Tuple<long, long> ReadArchive(string filename)
{
using var archive = ArchiveFactory.Open(filename);
return new Tuple<long, long>(
archive.Entries.Count(),
archive.Entries.Select(x => x.Size).Sum()
);
}
}

View File

@@ -22,37 +22,31 @@ public class Zip64Tests : WriterTests
// 4GiB + 1
private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1;
//[Fact]
[Trait("format", "zip64")]
public void Zip64_Single_Large_File() =>
// One single file, requires zip64
RunSingleTest(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
//[Fact]
[Trait("format", "zip64")]
public void Zip64_Two_Large_Files() =>
// One single file, requires zip64
RunSingleTest(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public void Zip64_Two_Small_files() =>
// Multiple files, does not require zip64
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public void Zip64_Two_Small_files_stream() =>
// Multiple files, does not require zip64, and works with streams
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true);
[Fact]
[Trait("format", "zip64")]
public void Zip64_Two_Small_Files_Zip64() =>
// Multiple files, use zip64 even though it is not required
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false);
[Fact]
[Trait("format", "zip64")]
public void Zip64_Single_Large_File_Fail()
{
@@ -65,7 +59,6 @@ public class Zip64Tests : WriterTests
catch (NotSupportedException) { }
}
[Fact]
[Trait("zip64", "true")]
public void Zip64_Single_Large_File_Zip64_Streaming_Fail()
{
@@ -78,7 +71,6 @@ public class Zip64Tests : WriterTests
catch (NotSupportedException) { }
}
[Fact]
[Trait("zip64", "true")]
public void Zip64_Single_Large_File_Streaming_Fail()
{

View File

@@ -1,196 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipArchiveAsyncTests : ArchiveTests
{
public ZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task Zip_ZipX_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.zipx");
[Fact]
public async Task Zip_BZip2_Streamed_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.bzip2.dd.zip");
[Fact]
public async Task Zip_BZip2_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.bzip2.zip");
[Fact]
public async Task Zip_Deflate_Streamed2_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.deflate.dd-.zip");
[Fact]
public async Task Zip_Deflate_Streamed_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.deflate.dd.zip");
[Fact]
public async Task Zip_Deflate_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.deflate.zip");
[Fact]
public async Task Zip_Deflate64_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.deflate64.zip");
[Fact]
public async Task Zip_LZMA_Streamed_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.lzma.dd.zip");
[Fact]
public async Task Zip_LZMA_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.lzma.zip");
[Fact]
public async Task Zip_PPMd_Streamed_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.ppmd.dd.zip");
[Fact]
public async Task Zip_PPMd_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.ppmd.zip");
[Fact]
public async Task Zip_None_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.none.zip");
[Fact]
public async Task Zip_Zip64_ArchiveStreamRead_Async() =>
await ArchiveStreamReadAsync("Zip.zip64.zip");
[Fact]
public async Task Zip_Shrink_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.shrink.zip");
}
[Fact]
public async Task Zip_Implode_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.implode.zip");
}
[Fact]
public async Task Zip_Reduce1_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.reduce1.zip");
}
[Fact]
public async Task Zip_Reduce2_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.reduce2.zip");
}
[Fact]
public async Task Zip_Reduce3_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.reduce3.zip");
}
[Fact]
public async Task Zip_Reduce4_ArchiveStreamRead_Async()
{
UseExtensionInsteadOfNameToVerify = true;
UseCaseInsensitiveToVerify = true;
await ArchiveStreamReadAsync("Zip.reduce4.zip");
}
[Fact]
public async Task Zip_Random_Write_Remove_Async()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip");
using (var archive = ZipArchive.Open(unmodified))
{
var entry = archive.Entries.Single(x =>
x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase)
);
archive.RemoveEntry(entry);
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866);
await archive.SaveToAsync(scratchPath, writerOptions);
}
CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866));
}
[Fact]
public async Task Zip_Random_Write_Add_Async()
{
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip");
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
using (var archive = ZipArchive.Open(unmodified))
{
archive.AddEntry("jpg\\test.jpg", jpg);
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866);
await archive.SaveToAsync(scratchPath, writerOptions);
}
CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866));
}
[Fact]
public async Task Zip_Create_New_Async()
{
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.noEmptyDirs.zip");
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
using (var archive = ZipArchive.Create())
{
archive.DeflateCompressionLevel = Compressors.Deflate.CompressionLevel.BestSpeed;
archive.AddAllFromDirectory(ORIGINAL_FILES_PATH);
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
writerOptions.ArchiveEncoding.Default = Encoding.UTF8;
await archive.SaveToAsync(scratchPath, writerOptions);
}
CompareArchivesByPath(unmodified, scratchPath);
}
[Fact]
public async Task Zip_Deflate_Entry_Stream_Async()
{
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")))
using (var archive = ZipArchive.Open(stream))
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
await entry.WriteToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
VerifyFiles();
}
}

View File

@@ -1,112 +0,0 @@
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

@@ -1,281 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.Xz;
using SharpCompress.Crypto;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests
{
public ZipTypesLevelsWithCrcRatioAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Theory]
[InlineData(CompressionType.Deflate, 1, 1, 0.11f)] // was 0.8f, actual 0.104
[InlineData(CompressionType.Deflate, 3, 1, 0.08f)] // was 0.8f, actual 0.078
[InlineData(CompressionType.Deflate, 6, 1, 0.05f)] // was 0.8f, actual ~0.042
[InlineData(CompressionType.Deflate, 9, 1, 0.04f)] // was 0.7f, actual 0.038
[InlineData(CompressionType.ZStandard, 1, 1, 0.025f)] // was 0.8f, actual 0.023
[InlineData(CompressionType.ZStandard, 3, 1, 0.015f)] // was 0.7f, actual 0.013
[InlineData(CompressionType.ZStandard, 9, 1, 0.006f)] // was 0.7f, actual 0.005
[InlineData(CompressionType.ZStandard, 22, 1, 0.005f)] // was 0.7f, actual 0.004
[InlineData(CompressionType.BZip2, 0, 1, 0.035f)] // was 0.8f, actual 0.033
[InlineData(CompressionType.LZMA, 0, 1, 0.005f)] // was 0.8f, actual 0.004
[InlineData(CompressionType.None, 0, 1, 1.001f)] // was 1.1f, actual 1.000
[InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8f, actual 0.042
[InlineData(CompressionType.ZStandard, 3, 2, 0.012f)] // was 0.7f, actual 0.010
[InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8f, actual 0.032
[InlineData(CompressionType.Deflate, 9, 3, 0.04f)] // was 0.7f, actual 0.038
[InlineData(CompressionType.ZStandard, 9, 3, 0.003f)] // was 0.7f, actual 0.002
public async Task Zip_Create_Archive_With_3_Files_Crc32_Test_Async(
CompressionType compressionType,
int compressionLevel,
int sizeMb,
float expectedRatio
)
{
const int OneMiB = 1024 * 1024;
var baseSize = sizeMb * OneMiB;
// Generate test content for files with sizes based on the sizeMb parameter
var file1Data = TestPseudoTextStream.Create(baseSize);
var file2Data = TestPseudoTextStream.Create(baseSize * 2);
var file3Data = TestPseudoTextStream.Create(baseSize * 3);
var expectedFiles = new Dictionary<string, (byte[] data, uint crc)>
{
[$"file1_{sizeMb}MiB.txt"] = (file1Data, CalculateCrc32(file1Data)),
[$"data/file2_{sizeMb * 2}MiB.txt"] = (file2Data, CalculateCrc32(file2Data)),
[$"deep/nested/file3_{sizeMb * 3}MiB.txt"] = (file3Data, CalculateCrc32(file3Data)),
};
// Create zip archive in memory
using var zipStream = new MemoryStream();
using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel))
{
await writer.WriteAsync($"file1_{sizeMb}MiB.txt", new MemoryStream(file1Data));
await writer.WriteAsync($"data/file2_{sizeMb * 2}MiB.txt", new MemoryStream(file2Data));
await writer.WriteAsync(
$"deep/nested/file3_{sizeMb * 3}MiB.txt",
new MemoryStream(file3Data)
);
}
// Calculate and output actual compression ratio
var originalSize = file1Data.Length + file2Data.Length + file3Data.Length;
var actualRatio = (double)zipStream.Length / originalSize;
//Debug.WriteLine($"Zip_Create_Archive_With_3_Files_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
// Verify compression occurred (except for None compression type)
if (compressionType != CompressionType.None)
{
Assert.True(
zipStream.Length < originalSize,
$"Compression failed: compressed={zipStream.Length}, original={originalSize}"
);
}
// Verify compression ratio
VerifyCompressionRatio(
originalSize,
zipStream.Length,
expectedRatio,
$"{compressionType} level {compressionLevel}"
);
// Verify archive content and CRC32
await VerifyArchiveContentAsync(zipStream, expectedFiles);
// Verify compression type is correctly set
VerifyCompressionType(zipStream, compressionType);
}
[Theory]
[InlineData(CompressionType.Deflate, 1, 4, 0.11f)] // was 0.8, actual 0.105
[InlineData(CompressionType.Deflate, 3, 4, 0.08f)] // was 0.8, actual 0.077
[InlineData(CompressionType.Deflate, 6, 4, 0.045f)] // was 0.8, actual 0.042
[InlineData(CompressionType.Deflate, 9, 4, 0.04f)] // was 0.8, actual 0.037
[InlineData(CompressionType.ZStandard, 1, 4, 0.025f)] // was 0.8, actual 0.022
[InlineData(CompressionType.ZStandard, 3, 4, 0.012f)] // was 0.8, actual 0.010
[InlineData(CompressionType.ZStandard, 9, 4, 0.003f)] // was 0.8, actual 0.002
[InlineData(CompressionType.ZStandard, 22, 4, 0.003f)] // was 0.8, actual 0.002
[InlineData(CompressionType.BZip2, 0, 4, 0.035f)] // was 0.8, actual 0.032
[InlineData(CompressionType.LZMA, 0, 4, 0.003f)] // was 0.8, actual 0.002
public async Task Zip_WriterFactory_Crc32_Test_Async(
CompressionType compressionType,
int compressionLevel,
int sizeMb,
float expectedRatio
)
{
var fileSize = sizeMb * 1024 * 1024;
var testData = TestPseudoTextStream.Create(fileSize);
var expectedCrc = CalculateCrc32(testData);
// Create archive with specified compression level
using var zipStream = new MemoryStream();
var writerOptions = new ZipWriterOptions(compressionType)
{
CompressionLevel = compressionLevel,
};
using (var writer = WriterFactory.Open(zipStream, ArchiveType.Zip, writerOptions))
{
await writer.WriteAsync(
$"{compressionType}_level_{compressionLevel}_{sizeMb}MiB.txt",
new MemoryStream(testData)
);
}
// Calculate and output actual compression ratio
var actualRatio = (double)zipStream.Length / testData.Length;
//Debug.WriteLine($"Zip_WriterFactory_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
VerifyCompressionRatio(
testData.Length,
zipStream.Length,
expectedRatio,
$"{compressionType} level {compressionLevel}"
);
// Verify the archive
zipStream.Position = 0;
using var archive = ZipArchive.Open(zipStream);
var entry = archive.Entries.Single(e => !e.IsDirectory);
using var entryStream = entry.OpenEntryStream();
using var extractedStream = new MemoryStream();
await entryStream.CopyToAsync(extractedStream);
var extractedData = extractedStream.ToArray();
var actualCrc = CalculateCrc32(extractedData);
Assert.Equal(compressionType, entry.CompressionType);
Assert.Equal(expectedCrc, actualCrc);
Assert.Equal(testData.Length, extractedData.Length);
Assert.Equal(testData, extractedData);
}
[Theory]
[InlineData(CompressionType.Deflate, 1, 2, 0.11f)] // was 0.8, actual 0.104
[InlineData(CompressionType.Deflate, 3, 2, 0.08f)] // was 0.8, actual 0.077
[InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8, actual 0.042
[InlineData(CompressionType.Deflate, 9, 2, 0.04f)] // was 0.7, actual 0.038
[InlineData(CompressionType.ZStandard, 1, 2, 0.025f)] // was 0.8, actual 0.023
[InlineData(CompressionType.ZStandard, 3, 2, 0.015f)] // was 0.7, actual 0.012
[InlineData(CompressionType.ZStandard, 9, 2, 0.006f)] // was 0.7, actual 0.005
[InlineData(CompressionType.ZStandard, 22, 2, 0.005f)] // was 0.7, actual 0.004
[InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8, actual 0.032
[InlineData(CompressionType.LZMA, 0, 2, 0.005f)] // was 0.8, actual 0.004
public async Task Zip_ZipArchiveOpen_Crc32_Test_Async(
CompressionType compressionType,
int compressionLevel,
int sizeMb,
float expectedRatio
)
{
var fileSize = sizeMb * 1024 * 1024;
var testData = TestPseudoTextStream.Create(fileSize);
var expectedCrc = CalculateCrc32(testData);
// Create archive with specified compression and level
using var zipStream = new MemoryStream();
using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel))
{
await writer.WriteAsync(
$"{compressionType}_{compressionLevel}_{sizeMb}MiB.txt",
new MemoryStream(testData)
);
}
// Calculate and output actual compression ratio
var actualRatio = (double)zipStream.Length / testData.Length;
//Debug.WriteLine($"Zip_ZipArchiveOpen_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
// Verify the archive
zipStream.Position = 0;
using var archive = ZipArchive.Open(zipStream);
var entry = archive.Entries.Single(e => !e.IsDirectory);
using var entryStream = entry.OpenEntryStream();
using var extractedStream = new MemoryStream();
await entryStream.CopyToAsync(extractedStream);
var extractedData = extractedStream.ToArray();
var actualCrc = CalculateCrc32(extractedData);
Assert.Equal(compressionType, entry.CompressionType);
Assert.Equal(expectedCrc, actualCrc);
Assert.Equal(testData.Length, extractedData.Length);
// For smaller files, verify full content; for larger, spot check
if (testData.Length <= sizeMb * 2)
{
Assert.Equal(testData, extractedData);
}
else
{
VerifyDataSpotCheck(testData, extractedData);
}
VerifyCompressionRatio(
testData.Length,
zipStream.Length,
expectedRatio,
$"{compressionType} Level {compressionLevel}"
);
}
// Helper method for async archive content verification
private async Task VerifyArchiveContentAsync(
MemoryStream zipStream,
Dictionary<string, (byte[] data, uint crc)> expectedFiles
)
{
zipStream.Position = 0;
using var archive = ZipArchive.Open(zipStream);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
Assert.True(
expectedFiles.ContainsKey(entry.Key!),
$"Unexpected file in archive: {entry.Key}"
);
var expected = expectedFiles[entry.Key!];
using var entryStream = entry.OpenEntryStream();
using var extractedStream = new MemoryStream();
await entryStream.CopyToAsync(extractedStream);
var extractedData = extractedStream.ToArray();
var actualCrc = CalculateCrc32(extractedData);
Assert.Equal(expected.crc, actualCrc);
Assert.Equal(expected.data.Length, extractedData.Length);
// For larger files, just spot check, for smaller verify full content
var expectedData = expected.data;
if (expectedData.Length <= 2 * 1024 * 1024)
{
Assert.Equal(expectedData, extractedData);
}
else
{
VerifyDataSpotCheck(expectedData, extractedData);
}
}
Assert.Equal(expectedFiles.Count, archive.Entries.Count(e => !e.IsDirectory));
}
}

View File

@@ -1,254 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Zip;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipReaderAsyncTests : ReaderTests
{
public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async Task Issue_269_Double_Skip_Async()
{
var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip");
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
using var reader = ReaderFactory.Open(stream);
var count = 0;
while (await reader.MoveToNextEntryAsync())
{
count++;
if (!reader.Entry.IsDirectory)
{
if (count % 2 != 0)
{
await reader.WriteEntryToAsync(Stream.Null);
}
}
}
}
[Fact]
public async Task Zip_Zip64_Streamed_Read_Async() =>
await ReadAsync("Zip.zip64.zip", CompressionType.Deflate);
[Fact]
public async Task Zip_ZipX_Streamed_Read_Async() =>
await ReadAsync("Zip.zipx", CompressionType.LZMA);
[Fact]
public async Task Zip_BZip2_Streamed_Read_Async() =>
await ReadAsync("Zip.bzip2.dd.zip", CompressionType.BZip2);
[Fact]
public async Task Zip_BZip2_Read_Async() =>
await ReadAsync("Zip.bzip2.zip", CompressionType.BZip2);
[Fact]
public async Task Zip_Deflate_Streamed2_Read_Async() =>
await ReadAsync("Zip.deflate.dd-.zip", CompressionType.Deflate);
[Fact]
public async Task Zip_Deflate_Streamed_Read_Async() =>
await ReadAsync("Zip.deflate.dd.zip", CompressionType.Deflate);
[Fact]
public async Task Zip_Deflate_Streamed_Skip_Async()
{
using Stream stream = new ForwardOnlyStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
);
using var reader = ReaderFactory.Open(stream);
var x = 0;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
x++;
if (x % 2 == 0)
{
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
}
[Fact]
public async Task Zip_Deflate_Read_Async() =>
await ReadAsync("Zip.deflate.zip", CompressionType.Deflate);
[Fact]
public async Task Zip_Deflate64_Read_Async() =>
await ReadAsync("Zip.deflate64.zip", CompressionType.Deflate64);
[Fact]
public async Task Zip_LZMA_Streamed_Read_Async() =>
await ReadAsync("Zip.lzma.dd.zip", CompressionType.LZMA);
[Fact]
public async Task Zip_LZMA_Read_Async() =>
await ReadAsync("Zip.lzma.zip", CompressionType.LZMA);
[Fact]
public async Task Zip_PPMd_Streamed_Read_Async() =>
await ReadAsync("Zip.ppmd.dd.zip", CompressionType.PPMd);
[Fact]
public async Task Zip_PPMd_Read_Async() =>
await ReadAsync("Zip.ppmd.zip", CompressionType.PPMd);
[Fact]
public async Task Zip_None_Read_Async() =>
await ReadAsync("Zip.none.zip", CompressionType.None);
[Fact]
public async Task Zip_Deflate_NoEmptyDirs_Read_Async() =>
await ReadAsync("Zip.deflate.noEmptyDirs.zip", CompressionType.Deflate);
[Fact]
public async Task Zip_BZip2_PkwareEncryption_Read_Async()
{
using (
Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"))
)
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
{
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
VerifyFiles();
}
[Fact]
public async Task Zip_Reader_Disposal_Test_Async()
{
using var stream = new TestStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
);
using (var reader = ReaderFactory.Open(stream))
{
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
Assert.True(stream.IsDisposed);
}
[Fact]
public async Task Zip_Reader_Disposal_Test2_Async()
{
using var stream = new TestStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
);
var reader = ReaderFactory.Open(stream);
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
Assert.False(stream.IsDisposed);
}
[Fact]
public async Task Zip_LZMA_WinzipAES_Read_Async() =>
await Assert.ThrowsAsync<NotSupportedException>(async () =>
{
using (
Stream stream = File.OpenRead(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip")
)
)
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
{
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
VerifyFiles();
});
[Fact]
public async Task Zip_Deflate_WinzipAES_Read_Async()
{
using (
Stream stream = File.OpenRead(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")
)
)
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
{
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
}
}
}
VerifyFiles();
}
[Fact]
public async Task Zip_Deflate_ZipCrypto_Read_Async()
{
var count = 0;
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")))
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
{
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(
SCRATCH_FILES_PATH,
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
);
count++;
}
}
}
Assert.Equal(8, count);
}
}

View File

@@ -1,67 +0,0 @@
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipWriterAsyncTests : WriterTests
{
public ZipWriterAsyncTests()
: base(ArchiveType.Zip) { }
[Fact]
public async Task Zip_Deflate_Write_Async() =>
await WriteAsync(
CompressionType.Deflate,
"Zip.deflate.noEmptyDirs.zip",
"Zip.deflate.noEmptyDirs.zip",
Encoding.UTF8
);
[Fact]
public async Task Zip_BZip2_Write_Async() =>
await WriteAsync(
CompressionType.BZip2,
"Zip.bzip2.noEmptyDirs.zip",
"Zip.bzip2.noEmptyDirs.zip",
Encoding.UTF8
);
[Fact]
public async Task Zip_None_Write_Async() =>
await WriteAsync(
CompressionType.None,
"Zip.none.noEmptyDirs.zip",
"Zip.none.noEmptyDirs.zip",
Encoding.UTF8
);
[Fact]
public async Task Zip_LZMA_Write_Async() =>
await WriteAsync(
CompressionType.LZMA,
"Zip.lzma.noEmptyDirs.zip",
"Zip.lzma.noEmptyDirs.zip",
Encoding.UTF8
);
[Fact]
public async Task Zip_PPMd_Write_Async() =>
await WriteAsync(
CompressionType.PPMd,
"Zip.ppmd.noEmptyDirs.zip",
"Zip.ppmd.noEmptyDirs.zip",
Encoding.UTF8
);
[Fact]
public async Task Zip_Rar_Write_Async() =>
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
await WriteAsync(
CompressionType.Rar,
"Zip.ppmd.noEmptyDirs.zip",
"Zip.ppmd.noEmptyDirs.zip"
)
);
}

View File

@@ -1,146 +0,0 @@
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);
}
}