diff --git a/Directory.Packages.props b/Directory.Packages.props index 6864bd94..a9438d7b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + diff --git a/src/SharpCompress/Common/ArchiveEncodingExtensions.cs b/src/SharpCompress/Common/ArchiveEncodingExtensions.cs index 88dc35b4..08e64329 100644 --- a/src/SharpCompress/Common/ArchiveEncodingExtensions.cs +++ b/src/SharpCompress/Common/ArchiveEncodingExtensions.cs @@ -24,14 +24,6 @@ public enum EncodingType /// public static class ArchiveEncodingExtensions { -#if !NETFRAMEWORK - /// - /// Registers the code pages encoding provider. - /// - static ArchiveEncodingExtensions() => - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); -#endif - extension(IArchiveEncoding encoding) { /// diff --git a/src/SharpCompress/Common/EncodingProviderRegistration.cs b/src/SharpCompress/Common/EncodingProviderRegistration.cs new file mode 100644 index 00000000..bcb88256 --- /dev/null +++ b/src/SharpCompress/Common/EncodingProviderRegistration.cs @@ -0,0 +1,52 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace SharpCompress.Common; + +/// +/// Registers so that legacy code pages (e.g. 437, 866) +/// used by archive headers are resolvable via . +/// +/// +/// +/// This runs from a module initializer rather than a static constructor. Registration must happen +/// before any encoding lookup, including lookups a caller performs itself while building an +/// — for example Encoding.GetEncoding(866). A static constructor +/// only fires when its own type is first touched, which made registration order-dependent and caused +/// for callers that resolved a code page before touching +/// any other SharpCompress type. +/// +/// +/// .NET Framework resolves these code pages natively, so registration is only needed elsewhere. +/// +/// +internal static class EncodingProviderRegistration +{ +#if !NETFRAMEWORK + // CA2255 discourages [ModuleInitializer] in libraries because load-time work can surprise consumers. + // Registering an encoding provider is the exception it does not account for: the registration has to be + // in place before the first Encoding.GetEncoding call, and that call may be made by the consumer before + // it touches any SharpCompress type. Every lazier trigger reintroduces the ordering bug. Callers who need + // registration to be explicit can call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) themselves. +#pragma warning disable CA2255 + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void Initialize() => RegisterCodePagesProvider(); + + private static bool _registered; + + /// + /// Registers the code pages provider if it has not already been registered. Idempotent. + /// + internal static void RegisterCodePagesProvider() + { + if (_registered) + { + return; + } + + _registered = true; + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + } +#endif +} diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs index f6ddfb2c..3c5bd692 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs @@ -313,7 +313,7 @@ internal sealed partial class StreamingZipHeaderFactory var nextHeaderBytes = await _reader .ReadUInt32Async(_cancellationToken) .ConfigureAwait(false); - _sharpCompressStream.Rewind(sizeof(uint)); + _sharpCompressStream.RewindBytes(sizeof(uint)); // Check if next data is PostDataDescriptor, streamed file with 0 length header.HasData = !IsHeader(nextHeaderBytes); diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index 8e775727..df993a43 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -143,11 +142,11 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory _lastEntryHeader.IsCrcAvailable = true; // The DataDescriptor can be either 64bit or 32bit - var compressed_size = reader.ReadUInt32(); - var uncompressed_size = reader.ReadUInt32(); + var compressedSize = reader.ReadUInt32(); + var uncompressedSize = reader.ReadUInt32(); - var test_64bit = ((long)uncompressed_size << 32) | compressed_size; - if (test_64bit == _lastEntryHeader.CompressedSize) + var test64Bit = ((long)uncompressedSize << 32) | compressedSize; + if (test64Bit == _lastEntryHeader.CompressedSize) { _lastEntryHeader.UncompressedSize = ((long)reader.ReadUInt32() << 32) | headerBytes; @@ -155,7 +154,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory } else { - _lastEntryHeader.UncompressedSize = uncompressed_size; + _lastEntryHeader.UncompressedSize = uncompressedSize; } if (pos.HasValue) @@ -193,34 +192,34 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory //entry could be zero bytes so we need to know that. if (header.ZipHeaderType == ZipHeaderType.LocalEntry) { - var local_header = ((LocalEntryHeader)header); - var dir_header = _entries?.FirstOrDefault(entry => - entry.Key == local_header.Name - && local_header.CompressedSize == 0 - && local_header.UncompressedSize == 0 - && local_header.Crc == 0 - && local_header.IsDirectory == false + var localHeader = (LocalEntryHeader)header; + var dirHeader = _entries?.FirstOrDefault(entry => + entry.Key == localHeader.Name + && localHeader.CompressedSize == 0 + && localHeader.UncompressedSize == 0 + && localHeader.Crc == 0 + && localHeader.IsDirectory == false ); - if (dir_header != null) + if (dirHeader != null) { - local_header.UncompressedSize = dir_header.Size; - local_header.CompressedSize = dir_header.CompressedSize; - local_header.Crc = (uint)dir_header.Crc; - local_header.IsCrcAvailable = true; + localHeader.UncompressedSize = dirHeader.Size; + localHeader.CompressedSize = dirHeader.CompressedSize; + localHeader.Crc = (uint)dirHeader.Crc; + localHeader.IsCrcAvailable = true; } // If we have CompressedSize, there is data to be read - if (local_header.CompressedSize > 0) + if (localHeader.CompressedSize > 0) { header.HasData = true; } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor ) - else if (local_header.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) + else if (localHeader.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor)) { // Peek ahead to check if next data is a header or file data. // Use the IStreamStack.Rewind mechanism to give back the peeked bytes. var nextHeaderBytes = reader.ReadUInt32(); - sharpCompressStream.Rewind(sizeof(uint)); + sharpCompressStream.RewindBytes(sizeof(uint)); // Check if next data is PostDataDescriptor, streamed file with 0 length header.HasData = !IsHeader(nextHeaderBytes); diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index c7ea2a6f..04ac63cf 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -622,7 +622,7 @@ internal class ZlibBaseStream : Stream, IStreamStack // This handles the case where the decompressor over-read past the end of the entry if (_stream is IStreamStack stack) { - stack.Rewind(z.AvailableBytesIn); + stack.RewindBytes(z.AvailableBytesIn); } z.AvailableBytesIn = 0; } @@ -643,7 +643,7 @@ internal class ZlibBaseStream : Stream, IStreamStack // This handles the case where the decompressor over-read past the end of the entry if (_stream is IStreamStack stack) { - stack.Rewind(z.AvailableBytesIn); + stack.RewindBytes(z.AvailableBytesIn); } z.AvailableBytesIn = 0; } @@ -1023,7 +1023,7 @@ internal class ZlibBaseStream : Stream, IStreamStack if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) { //rewind the buffer - this.Rewind(z.AvailableBytesIn); + this.RewindBytes(z.AvailableBytesIn); z.AvailableBytesIn = 0; } @@ -1217,7 +1217,7 @@ internal class ZlibBaseStream : Stream, IStreamStack if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) { //rewind the buffer - this.Rewind(z.AvailableBytesIn); + this.RewindBytes(z.AvailableBytesIn); z.AvailableBytesIn = 0; } diff --git a/src/SharpCompress/Compressors/LZMA/Log.cs b/src/SharpCompress/Compressors/LZMA/Log.cs deleted file mode 100644 index e0c9bcd9..00000000 --- a/src/SharpCompress/Compressors/LZMA/Log.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using SharpCompress.Common; - -namespace SharpCompress.Compressors.LZMA; - -internal static class Log -{ - private static readonly Stack INDENT = new(); - private static bool NEEDS_INDENT = true; - - static Log() => INDENT.Push(""); - - public static void PushIndent(string indent = " ") => INDENT.Push(INDENT.Peek() + indent); - - public static void PopIndent() - { - if (INDENT.Count == 1) - { - throw new ArchiveOperationException(); - } - - INDENT.Pop(); - } - - private static void EnsureIndent() - { - if (NEEDS_INDENT) - { - NEEDS_INDENT = false; - } - } - - public static void Write(object value) - { - EnsureIndent(); - } - - public static void Write(string text) - { - EnsureIndent(); - } - - public static void Write(string format, params object[] args) - { - EnsureIndent(); - } - - public static void WriteLine() - { - NEEDS_INDENT = true; - } - - public static void WriteLine(object value) - { - EnsureIndent(); - NEEDS_INDENT = true; - } - - public static void WriteLine(string text) - { - EnsureIndent(); - NEEDS_INDENT = true; - } - - public static void WriteLine(string format, params object[] args) - { - EnsureIndent(); - NEEDS_INDENT = true; - } -} diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs index c24a9c90..f6baa199 100644 --- a/src/SharpCompress/IO/IStreamStack.cs +++ b/src/SharpCompress/IO/IStreamStack.cs @@ -36,20 +36,16 @@ public static class StreamStackExtensions } /// - /// Gets the root underlying stream at the bottom of the stack. - /// This is useful for seeking when the intermediate streams don't support it. + /// Rewinds by bytes within the buffered region of the nearest + /// in the stack. /// - public static Stream GetRootStream(this IStreamStack stack) - { - var current = stack.BaseStream(); - while (current is IStreamStack streamStack) - { - current = streamStack.BaseStream(); - } - return current; - } - - internal static void Rewind(this IStreamStack stream, int count) + /// + /// Named distinctly from and + /// , which rewind to the recording anchor instead. + /// A single Rewind name across both would let Rewind(4) and Rewind(true) select + /// unrelated semantics on the same variable with no compiler complaint. + /// + internal static void RewindBytes(this IStreamStack stream, int count) { IStreamStack? current = stream; diff --git a/src/SharpCompress/Polyfills/ModuleInitializerAttribute.cs b/src/SharpCompress/Polyfills/ModuleInitializerAttribute.cs new file mode 100644 index 00000000..a129cdbe --- /dev/null +++ b/src/SharpCompress/Polyfills/ModuleInitializerAttribute.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// This file is required for [ModuleInitializer] to work on older target frameworks +// (.NET Framework 4.8, .NET Standard 2.0/2.1). The attribute is recognised by the C# compiler +// by name, so supplying our own definition is enough to enable module initializers there. + +#if NETFRAMEWORK || NETSTANDARD2_0 || NETSTANDARD2_1 +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Used to indicate to the compiler that a method should be called in its containing module's initializer. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +[AttributeUsage(AttributeTargets.Method, Inherited = false)] +internal sealed class ModuleInitializerAttribute : Attribute { } +#endif diff --git a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs index 3a516a04..31fa98fa 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs @@ -13,14 +13,6 @@ 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; diff --git a/tests/SharpCompress.Test/Tar/TarWriterTests.cs b/tests/SharpCompress.Test/Tar/TarWriterTests.cs index b4cada92..45660128 100644 --- a/tests/SharpCompress.Test/Tar/TarWriterTests.cs +++ b/tests/SharpCompress.Test/Tar/TarWriterTests.cs @@ -11,14 +11,6 @@ namespace SharpCompress.Test.Tar; public class TarWriterTests : WriterTests { - static TarWriterTests() - { -#if !NETFRAMEWORK - //fix issue where these tests could not be ran in isolation - System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); -#endif - } - public TarWriterTests() : base(ArchiveType.Tar) => UseExtensionInsteadOfNameToVerify = true; diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index 82d81b92..4be1c126 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -4,9 +4,9 @@ ".NETFramework,Version=v4.8": { "AwesomeAssertions": { "type": "Direct", - "requested": "[9.4.0, )", - "resolved": "9.4.0", - "contentHash": "dJxkWiQ8D+xT6Gr2sSL83+Mar+Vpy2JTcUPxFcckpPJ8VYBfSgnk+zqpS6t7kcGnjz8NLyF14qfuoL4bKzzoew==", + "requested": "[9.5.0, )", + "resolved": "9.5.0", + "contentHash": "F92MsjoF8B7IdcWETru4QGZx6VAl38qQPKNGS3kwPwlLB5lb0O4bhFqG8a8sz2estQJWkKmZkVy1r19HTaFcpg==", "dependencies": { "System.Threading.Tasks.Extensions": "4.5.4" } @@ -337,9 +337,9 @@ "net10.0": { "AwesomeAssertions": { "type": "Direct", - "requested": "[9.4.0, )", - "resolved": "9.4.0", - "contentHash": "dJxkWiQ8D+xT6Gr2sSL83+Mar+Vpy2JTcUPxFcckpPJ8VYBfSgnk+zqpS6t7kcGnjz8NLyF14qfuoL4bKzzoew==" + "requested": "[9.5.0, )", + "resolved": "9.5.0", + "contentHash": "F92MsjoF8B7IdcWETru4QGZx6VAl38qQPKNGS3kwPwlLB5lb0O4bhFqG8a8sz2estQJWkKmZkVy1r19HTaFcpg==" }, "Microsoft.NET.Test.Sdk": { "type": "Direct",