#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(() => new WriterOptions(CompressionType.Deflate, 10) ); Assert.Throws(() => new WriterOptions(CompressionType.ZStandard, 0) ); Assert.Throws(() => new WriterOptions(CompressionType.BZip2, 1) ); } [Fact] public void ZipWriterOptions_Invalid_CompressionLevels_Throw() { Assert.Throws(() => new ZipWriterOptions(CompressionType.Deflate, 10) ); Assert.Throws(() => new ZipWriterOptions(CompressionType.ZStandard, 23) ); } [Fact] public void GZipWriterOptions_Invalid_Settings_Throw() { Assert.Throws(() => new GZipWriterOptions(10)); Assert.Throws(() => 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(() => 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(() => 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); Assert.True(new ExtractionOptions().CheckCrc); } [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 OpenEntryStreamAsync(CancellationToken cancellationToken = default) { source.Position = 0; return new ValueTask(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 MoveToNextEntryAsync( CancellationToken cancellationToken = default ) => new(false); public EntryStream OpenEntryStream() { Source.Position = 0; return new TrackingEntryStream( this, Source, bufferSize => EntryStreamCopyBufferSize = bufferSize ); } public ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) => new(OpenEntryStream()); } private sealed class TrackingEntryStream( IReader reader, Stream stream, Action 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