mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-07-08 18:16:30 +00:00
548 lines
18 KiB
C#
548 lines
18 KiB
C#
#if !LEGACY_DOTNET
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using SharpCompress.Archives;
|
|
using SharpCompress.Common;
|
|
using SharpCompress.Readers;
|
|
using SharpCompress.Test.Mocks;
|
|
using SharpCompress.Writers;
|
|
using SharpCompress.Writers.GZip;
|
|
using SharpCompress.Writers.Tar;
|
|
using SharpCompress.Writers.Zip;
|
|
using Xunit;
|
|
|
|
namespace SharpCompress.Test;
|
|
|
|
public class OptionsUsabilityTests : TestBase
|
|
{
|
|
[Fact]
|
|
public void ReaderFactory_Stream_Default_Leaves_Stream_Open()
|
|
{
|
|
using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
|
using var testStream = new TestStream(file);
|
|
|
|
using (var reader = ReaderFactory.OpenReader(testStream))
|
|
{
|
|
reader.MoveToNextEntry();
|
|
}
|
|
|
|
Assert.False(testStream.IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ArchiveFactory_Stream_Default_Leaves_Stream_Open()
|
|
{
|
|
using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
|
using var testStream = new TestStream(file);
|
|
|
|
using (var archive = ArchiveFactory.OpenArchive(testStream))
|
|
{
|
|
_ = archive.Entries;
|
|
}
|
|
|
|
Assert.False(testStream.IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReaderFactory_Stream_Default_Leaves_Stream_Open_Async()
|
|
{
|
|
using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
|
using var testStream = new TestStream(file);
|
|
|
|
await using (
|
|
var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(testStream))
|
|
)
|
|
{
|
|
await reader.MoveToNextEntryAsync();
|
|
}
|
|
|
|
Assert.False(testStream.IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterOptions_Invalid_CompressionLevels_Throw()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new WriterOptions(CompressionType.Deflate, 10)
|
|
);
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new WriterOptions(CompressionType.ZStandard, 0)
|
|
);
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new WriterOptions(CompressionType.BZip2, 1)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZipWriterOptions_Invalid_CompressionLevels_Throw()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new ZipWriterOptions(CompressionType.Deflate, 10)
|
|
);
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new ZipWriterOptions(CompressionType.ZStandard, 23)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void GZipWriterOptions_Invalid_Settings_Throw()
|
|
{
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new GZipWriterOptions(10));
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
new GZipWriterOptions { CompressionType = CompressionType.Deflate }
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZipWriterEntryOptions_Invalid_CompressionLevel_Throws()
|
|
{
|
|
using var destination = new MemoryStream();
|
|
using var source = new MemoryStream(new byte[] { 1, 2, 3 });
|
|
using var writer = new ZipWriter(
|
|
destination,
|
|
new ZipWriterOptions(CompressionType.Deflate)
|
|
);
|
|
|
|
var options = new ZipWriterEntryOptions { CompressionLevel = 11 };
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
writer.Write("entry.bin", source, options)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterOptions_Factory_Methods_Create_Valid_Options()
|
|
{
|
|
// ForZip
|
|
var zipOptions = WriterOptions.ForZip();
|
|
Assert.Equal(CompressionType.Deflate, zipOptions.CompressionType);
|
|
Assert.True(zipOptions.LeaveStreamOpen);
|
|
|
|
// ForTar
|
|
var tarOptions = WriterOptions.ForTar();
|
|
Assert.Equal(CompressionType.None, tarOptions.CompressionType);
|
|
|
|
// ForGZip
|
|
var gzipOptions = WriterOptions.ForGZip();
|
|
Assert.Equal(CompressionType.GZip, gzipOptions.CompressionType);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterOptions_Fluent_Methods_Modify_Correctly()
|
|
{
|
|
var options = WriterOptions
|
|
.ForZip()
|
|
.WithLeaveStreamOpen(false)
|
|
.WithCompressionLevel(9)
|
|
.WithBufferSize(65536);
|
|
|
|
Assert.Equal(CompressionType.Deflate, options.CompressionType);
|
|
Assert.Equal(9, options.CompressionLevel);
|
|
Assert.False(options.LeaveStreamOpen);
|
|
Assert.Equal(65536, options.BufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterOptions_Factory_And_Fluent_Equivalent_To_Constructor()
|
|
{
|
|
// Factory + fluent approach
|
|
var factoryApproach = WriterOptions
|
|
.ForZip()
|
|
.WithLeaveStreamOpen(false)
|
|
.WithCompressionLevel(9);
|
|
|
|
// Traditional constructor approach
|
|
var constructorApproach = new WriterOptions(CompressionType.Deflate)
|
|
{
|
|
CompressionLevel = 9,
|
|
LeaveStreamOpen = false,
|
|
};
|
|
|
|
Assert.Equal(factoryApproach.CompressionType, constructorApproach.CompressionType);
|
|
Assert.Equal(factoryApproach.CompressionLevel, constructorApproach.CompressionLevel);
|
|
Assert.Equal(factoryApproach.LeaveStreamOpen, constructorApproach.LeaveStreamOpen);
|
|
}
|
|
|
|
[Fact]
|
|
public void WriterOptions_Default_BufferSize_Uses_Constants_BufferSize()
|
|
{
|
|
Assert.Equal(Constants.BufferSize, WriterOptions.ForZip().BufferSize);
|
|
Assert.Equal(
|
|
Constants.BufferSize,
|
|
new ZipWriterOptions(CompressionType.Deflate).BufferSize
|
|
);
|
|
Assert.Equal(
|
|
Constants.BufferSize,
|
|
new TarWriterOptions(CompressionType.None, true).BufferSize
|
|
);
|
|
Assert.Equal(Constants.BufferSize, new GZipWriterOptions().BufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Format_WriterOptions_Copy_BufferSize()
|
|
{
|
|
var options = WriterOptions.ForZip().WithBufferSize(12345);
|
|
|
|
Assert.Equal(12345, new ZipWriterOptions(options).BufferSize);
|
|
Assert.Equal(12345, new TarWriterOptions(options).BufferSize);
|
|
Assert.Equal(12345, new GZipWriterOptions(options).BufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZipWriter_Uses_WriterOptions_BufferSize()
|
|
{
|
|
using var source = new TrackingReadStream(new byte[100]);
|
|
using var destination = new MemoryStream();
|
|
using var writer = new ZipWriter(
|
|
destination,
|
|
new ZipWriterOptions(CompressionType.None) { BufferSize = 17 }
|
|
);
|
|
|
|
writer.Write("buffer-size.txt", source, DateTime.Now);
|
|
|
|
Assert.Equal(17, source.CopyBufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void GZipWriter_Uses_WriterOptions_BufferSize()
|
|
{
|
|
using var source = new TrackingReadStream(new byte[100]);
|
|
using var destination = new MemoryStream();
|
|
using var writer = new GZipWriter(destination, new GZipWriterOptions { BufferSize = 19 });
|
|
|
|
writer.Write("buffer-size.txt", source, DateTime.Now);
|
|
|
|
Assert.Equal(19, source.CopyBufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void TarWriter_Uses_WriterOptions_BufferSize_ForTransfer()
|
|
{
|
|
using var source = new MemoryStream(new byte[100]);
|
|
using var destination = new MemoryStream();
|
|
using var writer = new TarWriter(
|
|
destination,
|
|
new TarWriterOptions(CompressionType.None, true) { BufferSize = 0 }
|
|
);
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
writer.Write("buffer-size.txt", source, DateTime.Now)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Fluent_Methods_Modify_Correctly()
|
|
{
|
|
var options = ReaderOptions
|
|
.ForExternalStream.WithLeaveStreamOpen(false)
|
|
.WithPassword("secret")
|
|
.WithLookForHeader(true)
|
|
.WithBufferSize(65536);
|
|
|
|
Assert.False(options.LeaveStreamOpen);
|
|
Assert.Equal("secret", options.Password);
|
|
Assert.True(options.LookForHeader);
|
|
Assert.Equal(65536, options.BufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Fluent_And_Initializer_Equivalent()
|
|
{
|
|
// Fluent approach
|
|
var fluentApproach = ReaderOptions
|
|
.ForExternalStream.WithLeaveStreamOpen(false)
|
|
.WithPassword("secret")
|
|
.WithLookForHeader(true)
|
|
.WithBufferSize(65536)
|
|
.WithDisableCheckIncomplete(true);
|
|
|
|
// Preset + with-expression approach
|
|
var initializerApproach = ReaderOptions.ForExternalStream with
|
|
{
|
|
LeaveStreamOpen = false,
|
|
Password = "secret",
|
|
LookForHeader = true,
|
|
BufferSize = 65536,
|
|
DisableCheckIncomplete = true,
|
|
};
|
|
|
|
Assert.Equal(fluentApproach.LeaveStreamOpen, initializerApproach.LeaveStreamOpen);
|
|
Assert.Equal(fluentApproach.Password, initializerApproach.Password);
|
|
Assert.Equal(fluentApproach.LookForHeader, initializerApproach.LookForHeader);
|
|
Assert.Equal(fluentApproach.BufferSize, initializerApproach.BufferSize);
|
|
Assert.Equal(
|
|
fluentApproach.DisableCheckIncomplete,
|
|
initializerApproach.DisableCheckIncomplete
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Presets_Have_Correct_Defaults()
|
|
{
|
|
var external = ReaderOptions.ForExternalStream;
|
|
Assert.True(external.LeaveStreamOpen);
|
|
|
|
var owned = ReaderOptions.ForFilePath;
|
|
Assert.False(owned.LeaveStreamOpen);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtractionOptions_Presets_Have_Correct_Defaults()
|
|
{
|
|
var safe = ExtractionOptions.SafeExtract;
|
|
Assert.False(safe.Overwrite);
|
|
|
|
var flat = ExtractionOptions.FlatExtract;
|
|
Assert.False(flat.ExtractFullPath);
|
|
Assert.True(flat.Overwrite);
|
|
|
|
var preserveMetadata = ExtractionOptions.PreserveMetadata;
|
|
Assert.True(preserveMetadata.PreserveFileTime);
|
|
Assert.True(preserveMetadata.PreserveAttributes);
|
|
|
|
Assert.Equal(Constants.BufferSize, new ExtractionOptions().BufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reader_WriteEntryToFile_Uses_ExtractionOptions_BufferSize()
|
|
{
|
|
using var reader = new TrackingReader();
|
|
var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size.txt");
|
|
|
|
reader.WriteEntryToFile(destination, new ExtractionOptions { BufferSize = 11 });
|
|
|
|
Assert.Equal(11, reader.EntryStreamCopyBufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Reader_WriteEntryToFileAsync_Uses_ExtractionOptions_BufferSize()
|
|
{
|
|
await using var reader = new TrackingReader();
|
|
var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size-async.txt");
|
|
|
|
await reader.WriteEntryToFileAsync(destination, new ExtractionOptions { BufferSize = 13 });
|
|
|
|
Assert.Equal(13, reader.EntryStreamCopyBufferSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void Public_Api_Does_Not_Expose_CSharp_9_Required_Metadata()
|
|
{
|
|
var assembly = typeof(ReaderOptions).Assembly;
|
|
const string RequiredMemberAttributeName =
|
|
"System.Runtime.CompilerServices.RequiredMemberAttribute";
|
|
const string SetsRequiredMembersAttributeName =
|
|
"System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute";
|
|
var initOnlyProperties = assembly
|
|
.GetExportedTypes()
|
|
.SelectMany(type =>
|
|
type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
|
.Where(property => property.SetMethod?.IsPublic == true)
|
|
.Where(property =>
|
|
property
|
|
.SetMethod!.ReturnParameter.GetRequiredCustomModifiers()
|
|
.Contains(typeof(IsExternalInit))
|
|
)
|
|
.Select(property => $"{type.FullName}.{property.Name}")
|
|
)
|
|
.ToArray();
|
|
|
|
Assert.Empty(initOnlyProperties);
|
|
|
|
var requiredMembers = assembly
|
|
.GetExportedTypes()
|
|
.SelectMany(type =>
|
|
type.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)
|
|
.Where(member =>
|
|
member
|
|
.GetCustomAttributesData()
|
|
.Any(attribute =>
|
|
attribute.AttributeType.FullName == RequiredMemberAttributeName
|
|
)
|
|
)
|
|
.Select(member => $"{type.FullName}.{member.Name}")
|
|
)
|
|
.ToArray();
|
|
|
|
Assert.Empty(requiredMembers);
|
|
|
|
var constructorsWithRequiredMembers = assembly
|
|
.GetExportedTypes()
|
|
.SelectMany(type =>
|
|
type.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
|
|
.Where(constructor =>
|
|
constructor
|
|
.GetCustomAttributesData()
|
|
.Any(attribute =>
|
|
attribute.AttributeType.FullName == SetsRequiredMembersAttributeName
|
|
)
|
|
)
|
|
.Select(_ => $"{type.FullName}.ctor")
|
|
)
|
|
.ToArray();
|
|
|
|
Assert.Empty(constructorsWithRequiredMembers);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Factory_ForEncryptedArchive_Sets_Password()
|
|
{
|
|
var options = ReaderOptions.ForEncryptedArchive("myPassword");
|
|
Assert.Equal("myPassword", options.Password);
|
|
|
|
var noPassword = ReaderOptions.ForEncryptedArchive();
|
|
Assert.Null(noPassword.Password);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Factory_ForEncoding_Sets_Encoding()
|
|
{
|
|
var encoding = new ArchiveEncoding { Default = System.Text.Encoding.UTF8 };
|
|
var options = ReaderOptions.ForEncoding(encoding);
|
|
Assert.Equal(encoding, options.ArchiveEncoding);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReaderOptions_Factory_ForSelfExtractingArchive_Configures_Correctly()
|
|
{
|
|
var options = ReaderOptions.ForSelfExtractingArchive("password");
|
|
Assert.True(options.LookForHeader);
|
|
Assert.Equal("password", options.Password);
|
|
Assert.Equal(1_048_576, options.RewindableBufferSize);
|
|
|
|
var noPassword = ReaderOptions.ForSelfExtractingArchive();
|
|
Assert.True(noPassword.LookForHeader);
|
|
Assert.Null(noPassword.Password);
|
|
Assert.Equal(1_048_576, noPassword.RewindableBufferSize);
|
|
}
|
|
|
|
private sealed class TestArchiveEntry(Stream source) : IArchiveEntry
|
|
{
|
|
public CompressionType CompressionType => CompressionType.None;
|
|
public DateTime? ArchivedTime => null;
|
|
public long CompressedSize => source.Length;
|
|
public long Crc => 0;
|
|
public DateTime? CreatedTime => null;
|
|
public string? Key => "buffer-size.txt";
|
|
public string? LinkTarget => null;
|
|
public bool IsDirectory => false;
|
|
public bool IsEncrypted => false;
|
|
public bool IsSplitAfter => false;
|
|
public bool IsSolid => false;
|
|
public int VolumeIndexFirst => 0;
|
|
public int VolumeIndexLast => 0;
|
|
public DateTime? LastAccessedTime => null;
|
|
public DateTime? LastModifiedTime => null;
|
|
public long Size => source.Length;
|
|
public int? Attrib => null;
|
|
public SharpCompress.Common.Options.IReaderOptions Options =>
|
|
ReaderOptions.ForExternalStream;
|
|
public bool IsComplete => true;
|
|
public IArchive Archive => throw new NotSupportedException();
|
|
|
|
public Stream OpenEntryStream()
|
|
{
|
|
source.Position = 0;
|
|
return source;
|
|
}
|
|
|
|
public ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
source.Position = 0;
|
|
return new ValueTask<Stream>(source);
|
|
}
|
|
}
|
|
|
|
private sealed class TrackingReadStream(byte[] data) : MemoryStream(data)
|
|
{
|
|
public int? CopyBufferSize { get; private set; }
|
|
|
|
public override void CopyTo(Stream destination, int bufferSize)
|
|
{
|
|
CopyBufferSize = bufferSize;
|
|
base.CopyTo(destination, bufferSize);
|
|
}
|
|
|
|
public override Task CopyToAsync(
|
|
Stream destination,
|
|
int bufferSize,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
CopyBufferSize = bufferSize;
|
|
return base.CopyToAsync(destination, bufferSize, cancellationToken);
|
|
}
|
|
}
|
|
|
|
private sealed class TrackingReader : IReader, IAsyncReader
|
|
{
|
|
public ArchiveType Type => ArchiveType.Zip;
|
|
public MemoryStream Source { get; } = new(new byte[100]);
|
|
public IEntry Entry => new TestArchiveEntry(Source);
|
|
public bool Cancelled => false;
|
|
public int? EntryStreamCopyBufferSize { get; private set; }
|
|
|
|
public void Dispose() { }
|
|
|
|
public ValueTask DisposeAsync() => default;
|
|
|
|
public void WriteEntryTo(Stream writableStream) => throw new NotSupportedException();
|
|
|
|
public ValueTask WriteEntryToAsync(
|
|
Stream writableStream,
|
|
CancellationToken cancellationToken = default
|
|
) => throw new NotSupportedException();
|
|
|
|
public void Cancel() { }
|
|
|
|
public bool MoveToNextEntry() => false;
|
|
|
|
public ValueTask<bool> MoveToNextEntryAsync(
|
|
CancellationToken cancellationToken = default
|
|
) => new(false);
|
|
|
|
public EntryStream OpenEntryStream()
|
|
{
|
|
Source.Position = 0;
|
|
return new TrackingEntryStream(
|
|
this,
|
|
Source,
|
|
bufferSize => EntryStreamCopyBufferSize = bufferSize
|
|
);
|
|
}
|
|
|
|
public ValueTask<EntryStream> OpenEntryStreamAsync(
|
|
CancellationToken cancellationToken = default
|
|
) => new(OpenEntryStream());
|
|
}
|
|
|
|
private sealed class TrackingEntryStream(
|
|
IReader reader,
|
|
Stream stream,
|
|
Action<int> copyBufferSize
|
|
) : EntryStream(reader, stream)
|
|
{
|
|
public override void CopyTo(Stream destination, int bufferSize)
|
|
{
|
|
copyBufferSize(bufferSize);
|
|
base.CopyTo(destination, bufferSize);
|
|
}
|
|
|
|
public override Task CopyToAsync(
|
|
Stream destination,
|
|
int bufferSize,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
copyBufferSize(bufferSize);
|
|
return base.CopyToAsync(destination, bufferSize, cancellationToken);
|
|
}
|
|
}
|
|
}
|
|
#endif
|