diff --git a/src/libraries/Common/src/Interop/Interop.Brotli.cs b/src/libraries/Common/src/Interop/Interop.Brotli.cs index 31d6a94d6fdde7..4ec354087f8a33 100644 --- a/src/libraries/Common/src/Interop/Interop.Brotli.cs +++ b/src/libraries/Common/src/Interop/Interop.Brotli.cs @@ -49,5 +49,27 @@ internal static unsafe partial BOOL BrotliEncoderCompressStream( [LibraryImport(Libraries.CompressionNative)] internal static unsafe partial BOOL BrotliEncoderCompress(int quality, int window, int v, nuint availableInput, byte* inBytes, nuint* availableOutput, byte* outBytes); + + internal enum BrotliSharedDictionaryType + { + // Raw LZ77 prefix dictionary. + RAW = 0 + } + + [LibraryImport(Libraries.CompressionNative)] + internal static unsafe partial SafeBrotliPreparedDictionaryHandle BrotliEncoderPrepareDictionary( + BrotliSharedDictionaryType type, nuint size, byte* data, int quality, + IntPtr allocFunc, IntPtr freeFunc, IntPtr opaque); + + [LibraryImport(Libraries.CompressionNative)] + internal static partial BOOL BrotliEncoderAttachPreparedDictionary( + SafeBrotliEncoderHandle state, SafeBrotliPreparedDictionaryHandle preparedDictionary); + + [LibraryImport(Libraries.CompressionNative)] + internal static unsafe partial BOOL BrotliDecoderAttachDictionary( + SafeBrotliDecoderHandle state, BrotliSharedDictionaryType type, nuint size, byte* data); + + [LibraryImport(Libraries.CompressionNative)] + internal static partial void BrotliEncoderDestroyPreparedDictionary(IntPtr dictionary); } } diff --git a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBrotliHandle.cs b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBrotliHandle.cs index 41bf926e038b67..319baef27951dd 100644 --- a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBrotliHandle.cs +++ b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/SafeBrotliHandle.cs @@ -31,4 +31,35 @@ protected override bool ReleaseHandle() public override bool IsInvalid => handle == IntPtr.Zero; } + + internal sealed class SafeBrotliPreparedDictionaryHandle : SafeHandle + { + internal IntPtr _dictionaryBytes; + internal int _dictionaryLength; + + public int DictionaryLength => _dictionaryLength; + public unsafe byte* DictionaryBytes => (byte*)_dictionaryBytes.ToPointer(); + + public SafeBrotliPreparedDictionaryHandle() : base(IntPtr.Zero, true) { } + + public void SetDictionaryBytes(IntPtr dictionaryBytes, int dictionaryLength) + { + _dictionaryBytes = dictionaryBytes; + _dictionaryLength = dictionaryLength; + } + + protected override bool ReleaseHandle() + { + Interop.Brotli.BrotliEncoderDestroyPreparedDictionary(handle); + unsafe + { + NativeMemory.Free(_dictionaryBytes.ToPointer()); + } + _dictionaryBytes = IntPtr.Zero; + _dictionaryLength = 0; + return true; + } + + public override bool IsInvalid => handle == IntPtr.Zero; + } } diff --git a/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs b/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs index 9faa236f84970c..bdce4f37d2e5fb 100644 --- a/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs +++ b/src/libraries/System.IO.Compression.Brotli/ref/System.IO.Compression.Brotli.cs @@ -15,15 +15,24 @@ public partial struct BrotliDecoder : System.IDisposable { private object _dummy; private int _dummyPrimitive; + public void AttachDictionary(System.IO.Compression.BrotliDictionary dictionary) { } public System.Buffers.OperationStatus Decompress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten) { throw null; } public void Dispose() { } public static bool TryDecompress(System.ReadOnlySpan source, System.Span destination, out int bytesWritten) { throw null; } } + public sealed partial class BrotliDictionary : System.IDisposable + { + internal BrotliDictionary() { } + public static System.IO.Compression.BrotliDictionary CreateFromBuffer(System.ReadOnlySpan buffer) { throw null; } + public static System.IO.Compression.BrotliDictionary CreateFromBuffer(System.ReadOnlySpan buffer, int quality) { throw null; } + public void Dispose() { } + } public partial struct BrotliEncoder : System.IDisposable { private object _dummy; private int _dummyPrimitive; public BrotliEncoder(int quality, int window) { throw null; } + public void AttachDictionary(System.IO.Compression.BrotliDictionary dictionary) { } public System.Buffers.OperationStatus Compress(System.ReadOnlySpan source, System.Span destination, out int bytesConsumed, out int bytesWritten, bool isFinalBlock) { throw null; } public void Dispose() { } public System.Buffers.OperationStatus Flush(System.Span destination, out int bytesWritten) { throw null; } @@ -44,6 +53,7 @@ public BrotliStream(System.IO.Stream stream, System.IO.Compression.CompressionMo public override bool CanWrite { get { throw null; } } public override long Length { get { throw null; } } public override long Position { get { throw null; } set { } } + public void AttachDictionary(System.IO.Compression.BrotliDictionary dictionary) { } public override System.IAsyncResult BeginRead(byte[] buffer, int offset, int count, System.AsyncCallback? asyncCallback, object? asyncState) { throw null; } public override System.IAsyncResult BeginWrite(byte[] buffer, int offset, int count, System.AsyncCallback? asyncCallback, object? asyncState) { throw null; } protected override void Dispose(bool disposing) { } diff --git a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx index b4b5a006595aab..bf0ca84966d9f4 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Compression.Brotli/src/Resources/Strings.resx @@ -178,4 +178,17 @@ System.IO.Compression.Brotli is not supported on this platform. + + + Failed to create Brotli dictionary. + + + Dictionary buffer cannot be empty. + + + Failed to attach dictionary to Brotli encoder. + + + Failed to attach dictionary to Brotli decoder. + diff --git a/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj b/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj index 124cd52bc84993..b9b5276f313243 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj +++ b/src/libraries/System.IO.Compression.Brotli/src/System.IO.Compression.Brotli.csproj @@ -25,6 +25,7 @@ + diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliDictionary.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliDictionary.cs new file mode 100644 index 00000000000000..33fae9d5297636 --- /dev/null +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliDictionary.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace System.IO.Compression +{ + /// + /// Represents a Brotli dictionary used for compression and decompression. + /// + public sealed class BrotliDictionary : IDisposable + { + private readonly SafeBrotliPreparedDictionaryHandle _preparedDictionary; + private bool _disposed; + + private BrotliDictionary(SafeBrotliPreparedDictionaryHandle preparedDictionary) + { + _preparedDictionary = preparedDictionary ?? throw new ArgumentNullException(nameof(preparedDictionary)); + _disposed = false; + } + + /// + /// Creates a new from a buffer. + /// + /// The buffer containing the dictionary data. + /// A new instance. + /// Thrown when the buffer is empty. + public static BrotliDictionary CreateFromBuffer(ReadOnlySpan buffer) => CreateFromBuffer(buffer, BrotliUtils.Quality_Max); + + /// + /// Creates a new prepared from a buffer for use with an encoder. + /// + /// The buffer containing the dictionary data. + /// The quality level used for preparing the dictionary. + /// A new instance. + /// Thrown when the buffer is empty. + /// Thrown when quality is not between 0 and 11. + public static BrotliDictionary CreateFromBuffer(ReadOnlySpan buffer, int quality) + { + if (buffer.IsEmpty) + { + throw new ArgumentException(SR.BrotliDictionary_EmptyBuffer, nameof(buffer)); + } + + if (quality < BrotliUtils.Quality_Min || quality > BrotliUtils.Quality_Max) + { + throw new ArgumentOutOfRangeException(nameof(quality), SR.Format(SR.BrotliEncoder_Quality, quality, BrotliUtils.Quality_Min, BrotliUtils.Quality_Max)); + } + + SafeBrotliPreparedDictionaryHandle? preparedDictionary; + + unsafe + { + // BrotliPreparedDictionary references the memory used to create the dictionary, + // so we make a copy of it on the native heap. + + IntPtr nativeMemory = (IntPtr)NativeMemory.Alloc((nuint)buffer.Length, 1); + buffer.CopyTo(new Span(nativeMemory.ToPointer(), buffer.Length)); + + preparedDictionary = Interop.Brotli.BrotliEncoderPrepareDictionary( + Interop.Brotli.BrotliSharedDictionaryType.RAW, + (nuint)buffer.Length, + (byte*)nativeMemory.ToPointer(), + quality, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (preparedDictionary == null || preparedDictionary.IsInvalid) + { + NativeMemory.Free(nativeMemory.ToPointer()); + throw new IOException(SR.BrotliDictionary_Create); + } + + preparedDictionary.SetDictionaryBytes(nativeMemory, buffer.Length); + } + + return new BrotliDictionary(preparedDictionary); + } + + internal bool AttachToEncoder(SafeBrotliEncoderHandle encoderHandle) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + return Interop.Brotli.BrotliEncoderAttachPreparedDictionary(encoderHandle, _preparedDictionary) != Interop.BOOL.FALSE; + } + + internal bool AttachToDecoder(SafeBrotliDecoderHandle decoderHandle) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + unsafe + { + return Interop.Brotli.BrotliDecoderAttachDictionary( + decoderHandle, + Interop.Brotli.BrotliSharedDictionaryType.RAW, + (nuint)_preparedDictionary.DictionaryLength, + _preparedDictionary.DictionaryBytes) != Interop.BOOL.FALSE; + } + } + + /// + /// Releases all resources used by the current instance of the class. + /// + public void Dispose() + { + if (!_disposed) + { + _preparedDictionary.Dispose(); + _disposed = true; + } + } + } +} diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliStream.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliStream.cs index 002186c9b36511..233543456a2fe0 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliStream.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/BrotliStream.cs @@ -59,6 +59,22 @@ public BrotliStream(Stream stream, CompressionMode mode, bool leaveOpen) _buffer = ArrayPool.Shared.Rent(DefaultInternalBufferSize); } + public void AttachDictionary(BrotliDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + EnsureNotDisposed(); + + if (_mode == CompressionMode.Compress) + { + _encoder.AttachDictionary(dictionary); + } + else if (_mode == CompressionMode.Decompress) + { + _decoder.AttachDictionary(dictionary); + } + } + private void EnsureNotDisposed() { ObjectDisposedException.ThrowIf(_stream is null, this); diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs index 8b6730ef523801..1f58d01174e0ba 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/dec/BrotliDecoder.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -21,11 +22,27 @@ internal void InitializeDecoder() throw new IOException(SR.BrotliDecoder_Create); } + /// + /// Attaches a Brotli dictionary to the decoder. + /// + /// The Brotli dictionary to attach. + public void AttachDictionary(BrotliDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + EnsureInitialized(); + + dictionary.AttachToDecoder(_state); + } + + [MemberNotNull(nameof(_state))] internal void EnsureInitialized() { EnsureNotDisposed(); if (_state == null) InitializeDecoder(); + + Debug.Assert(_state != null && !_state.IsInvalid && !_state.IsClosed); } /// Releases all resources used by the current Brotli decoder instance. diff --git a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs index e2f2b74ae38c3d..582fafaa8b4ca1 100644 --- a/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs +++ b/src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliEncoder.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -31,6 +32,19 @@ public BrotliEncoder(int quality, int window) SetWindow(window); } + /// + /// Attaches a Brotli dictionary to the encoder. + /// + /// The Brotli dictionary to attach. + public void AttachDictionary(BrotliDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + EnsureInitialized(); + + dictionary.AttachToEncoder(_state); + } + /// /// Performs a lazy initialization of the native encoder using the default Quality and Window values: /// BROTLI_DEFAULT_WINDOW 22 @@ -44,6 +58,7 @@ internal void InitializeEncoder() throw new IOException(SR.BrotliEncoder_Create); } + [MemberNotNull(nameof(_state))] internal void EnsureInitialized() { EnsureNotDisposed(); @@ -51,6 +66,8 @@ internal void EnsureInitialized() { InitializeEncoder(); } + + Debug.Assert(_state != null && !_state.IsInvalid && !_state.IsClosed); } /// Frees and disposes unmanaged resources. diff --git a/src/libraries/System.IO.Compression.Brotli/tests/BrotliDictionaryTests.cs b/src/libraries/System.IO.Compression.Brotli/tests/BrotliDictionaryTests.cs new file mode 100644 index 00000000000000..4d375022e68d94 --- /dev/null +++ b/src/libraries/System.IO.Compression.Brotli/tests/BrotliDictionaryTests.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace System.IO.Compression +{ + public class BrotliDictionaryTests + { + private const string RepeatedText = "This is a repeated text that should be in the dictionary. "; + + [Fact] + public void CreateFromBuffer_EmptyBuffer_ThrowsArgumentException() + { + Assert.Throws(() => BrotliDictionary.CreateFromBuffer(Array.Empty())); + } + + [Theory] + [InlineData(-1)] + [InlineData(12)] + public void CreateFromBuffer_InvalidQuality_ThrowsArgumentOutOfRangeException(int quality) + { + // Arrange + byte[] dictionaryData = Encoding.UTF8.GetBytes(RepeatedText); + + // Act & Assert + Assert.Throws(() => BrotliDictionary.CreateFromBuffer(dictionaryData, quality)); + } + + [Fact] + public void BrotliEncoder_WithDictionary_CompressesData() + { + // Arrange + byte[] dictionaryData = Encoding.UTF8.GetBytes(RepeatedText); + byte[] dataToCompress = Encoding.UTF8.GetBytes(RepeatedText + RepeatedText + "Additional text"); + byte[] compressedWithDictionary = new byte[BrotliEncoder.GetMaxCompressedLength(dataToCompress.Length)]; + byte[] compressedWithoutDictionary = new byte[BrotliEncoder.GetMaxCompressedLength(dataToCompress.Length)]; + + // Act + using (BrotliDictionary dictionary = BrotliDictionary.CreateFromBuffer(dictionaryData)) + { + BrotliEncoder encoder = new BrotliEncoder(11, 22); + encoder.AttachDictionary(dictionary); + OperationStatus statusWithDict = encoder.Compress(dataToCompress, compressedWithDictionary, out int bytesConsumedWithDict, out int bytesWrittenWithDict, true); + encoder.Dispose(); + + BrotliEncoder encoderWithoutDict = new BrotliEncoder(11, 22); + OperationStatus statusWithoutDict = encoderWithoutDict.Compress(dataToCompress, compressedWithoutDictionary, out int bytesConsumedWithoutDict, out int bytesWrittenWithoutDict, true); + encoderWithoutDict.Dispose(); + + // Resize arrays to actual compressed size + Array.Resize(ref compressedWithDictionary, bytesWrittenWithDict); + Array.Resize(ref compressedWithoutDictionary, bytesWrittenWithoutDict); + + // Assert + Assert.Equal(OperationStatus.Done, statusWithDict); + Assert.Equal(bytesConsumedWithDict, dataToCompress.Length); + Assert.Equal(OperationStatus.Done, statusWithoutDict); + Assert.Equal(bytesConsumedWithoutDict, dataToCompress.Length); + + // With a proper dictionary containing repeated text, the compression with dictionary should be more efficient + Assert.True(bytesWrittenWithDict <= bytesWrittenWithoutDict, + $"Dictionary compression size ({bytesWrittenWithDict}) was not smaller than regular compression ({bytesWrittenWithoutDict})"); + } + } + + [Fact] + public void BrotliDecoder_WithDictionary_DecompressesData() + { + // Arrange + byte[] dictionaryData = Encoding.UTF8.GetBytes(RepeatedText); + byte[] originalData = Encoding.UTF8.GetBytes(RepeatedText + "Additional text that references the dictionary content"); + byte[] compressedData = new byte[BrotliEncoder.GetMaxCompressedLength(originalData.Length)]; + byte[] decompressedData = new byte[originalData.Length]; + + // Act - Compress with dictionary + using (BrotliDictionary dictionary = BrotliDictionary.CreateFromBuffer(dictionaryData)) + { + // Compress with dictionary + BrotliEncoder encoder = new BrotliEncoder(11, 22); + encoder.AttachDictionary(dictionary); + OperationStatus compressStatus = encoder.Compress(originalData, compressedData, out int bytesConsumed, out int bytesWritten, true); + encoder.Dispose(); + + Assert.Equal(OperationStatus.Done, compressStatus); + Assert.Equal(bytesConsumed, originalData.Length); + + // Resize array to actual compressed size + Array.Resize(ref compressedData, bytesWritten); + + // Decompress with dictionary + BrotliDecoder decoder = new BrotliDecoder(); + decoder.AttachDictionary(dictionary); + OperationStatus decompressStatus = decoder.Decompress(compressedData, decompressedData, out int bytesConsumedDecomp, out int bytesWrittenDecomp); + decoder.Dispose(); + + // Assert + Assert.Equal(OperationStatus.Done, decompressStatus); + Assert.Equal(bytesConsumedDecomp, compressedData.Length); + Assert.Equal(bytesWrittenDecomp, originalData.Length); + Assert.Equal(originalData, decompressedData); + } + } + + [Fact] + public void BrotliStream_WithDictionary_RoundTrip() + { + // Arrange + byte[] dictionaryData = Encoding.UTF8.GetBytes(RepeatedText); + byte[] originalData = Encoding.UTF8.GetBytes(RepeatedText + RepeatedText + "More text to compress using the dictionary"); + byte[] compressedData; + byte[] decompressedData = new byte[originalData.Length]; + + using BrotliDictionary dictionary = BrotliDictionary.CreateFromBuffer(dictionaryData); + + // Act - Compress + using (MemoryStream compressedStream = new MemoryStream()) + { + using (BrotliStream compressionStream = new BrotliStream(compressedStream, CompressionMode.Compress)) + { + compressionStream.AttachDictionary(dictionary); + compressionStream.Write(originalData, 0, originalData.Length); + } + compressedData = compressedStream.ToArray(); + } + + // Act - Decompress + using (MemoryStream compressedStream = new MemoryStream(compressedData)) + using (BrotliStream decompressionStream = new BrotliStream(compressedStream, CompressionMode.Decompress)) + { + decompressionStream.AttachDictionary(dictionary); + int bytesRead = decompressionStream.Read(decompressedData, 0, decompressedData.Length); + + // Assert + Assert.Equal(originalData.Length, bytesRead); + Assert.Equal(originalData, decompressedData); + } + } + + [Fact] + public void BrotliDecoder_MismatchedDictionary_ThrowsException() + { + // Arrange + byte[] dictionaryData = Encoding.UTF8.GetBytes(RepeatedText); + byte[] originalData = Encoding.UTF8.GetBytes(RepeatedText + "Additional text that references the dictionary content"); + byte[] compressedData = new byte[BrotliEncoder.GetMaxCompressedLength(originalData.Length)]; + + using (BrotliDictionary dictionary = BrotliDictionary.CreateFromBuffer(dictionaryData)) + { + // Compress with dictionary + BrotliEncoder encoder = new BrotliEncoder(11, 22); + encoder.AttachDictionary(dictionary); + OperationStatus compressStatus = encoder.Compress(originalData, compressedData, out int bytesConsumed, out int bytesWritten, true); + encoder.Dispose(); + + Assert.Equal(OperationStatus.Done, compressStatus); + + // Resize array to actual compressed size + Array.Resize(ref compressedData, bytesWritten); + } + + // Attempt to decompress with a different dictionary + using (BrotliDictionary differentDictionary = BrotliDictionary.CreateFromBuffer(Encoding.UTF8.GetBytes("Different dictionary content"))) + { + BrotliDecoder decoder = new BrotliDecoder(); + decoder.AttachDictionary(differentDictionary); + OperationStatus decompressStatus = decoder.Decompress(compressedData, new byte[originalData.Length], out _, out _); + Assert.Equal(OperationStatus.InvalidData, decompressStatus); + } + + // Attempt to decompress without a dictionary + { + BrotliDecoder decoder = new BrotliDecoder(); + OperationStatus decompressStatus = decoder.Decompress(compressedData, new byte[originalData.Length], out _, out _); + Assert.Equal(OperationStatus.InvalidData, decompressStatus); + } + } + } +} diff --git a/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj b/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj index 936e3768a0e4eb..41526f7c4fb2f7 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj +++ b/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj @@ -4,8 +4,9 @@ true true - + +