| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Diagnostics; |
| | | 6 | | using static System.IO.Compression.ZLibNative; |
| | | 7 | | |
| | | 8 | | namespace System.Net.WebSockets.Compression |
| | | 9 | | { |
| | | 10 | | /// <summary> |
| | | 11 | | /// Provides a wrapper around the ZLib compression API. |
| | | 12 | | /// </summary> |
| | | 13 | | internal sealed class WebSocketDeflater : IDisposable |
| | | 14 | | { |
| | | 15 | | private readonly int _windowBits; |
| | | 16 | | private ZLibStreamHandle? _stream; |
| | | 17 | | private readonly bool _persisted; |
| | | 18 | | |
| | | 19 | | private byte[]? _buffer; |
| | | 20 | | |
| | 2025 | 21 | | internal WebSocketDeflater(int windowBits, bool persisted) |
| | 2025 | 22 | | { |
| | 2025 | 23 | | _windowBits = -windowBits; // Negative for raw deflate |
| | 2025 | 24 | | _persisted = persisted; |
| | 2025 | 25 | | } |
| | | 26 | | |
| | | 27 | | public void Dispose() |
| | 2025 | 28 | | { |
| | 2025 | 29 | | _stream?.Dispose(); |
| | 2025 | 30 | | } |
| | | 31 | | |
| | | 32 | | public void ReleaseBuffer() |
| | 3038 | 33 | | { |
| | 3038 | 34 | | if (_buffer is byte[] toReturn) |
| | 37 | 35 | | { |
| | 37 | 36 | | _buffer = null; |
| | 37 | 37 | | ArrayPool<byte>.Shared.Return(toReturn); |
| | 37 | 38 | | } |
| | 3038 | 39 | | } |
| | | 40 | | |
| | | 41 | | public ReadOnlySpan<byte> Deflate(ReadOnlySpan<byte> payload, bool endOfMessage) |
| | 37 | 42 | | { |
| | 37 | 43 | | Debug.Assert(_buffer is null, "Invalid state, ReleaseBuffer not called."); |
| | | 44 | | |
| | | 45 | | // For small payloads there might actually be overhead in the compression and the resulting |
| | | 46 | | // output might be larger than the payload. This is why we rent at least 4KB initially. |
| | | 47 | | const int MinInitialBufferLength = 4 * 1024; |
| | | 48 | | |
| | 37 | 49 | | _buffer = ArrayPool<byte>.Shared.Rent(Math.Max(payload.Length, MinInitialBufferLength)); |
| | 37 | 50 | | int position = 0; |
| | | 51 | | |
| | 37 | 52 | | while (true) |
| | 37 | 53 | | { |
| | 37 | 54 | | DeflatePrivate(payload, _buffer.AsSpan(position), endOfMessage, |
| | 37 | 55 | | out int consumed, out int written, out bool needsMoreOutput); |
| | 37 | 56 | | position += written; |
| | | 57 | | |
| | 37 | 58 | | if (!needsMoreOutput) |
| | 37 | 59 | | { |
| | 37 | 60 | | Debug.Assert(consumed == payload.Length); |
| | 37 | 61 | | break; |
| | | 62 | | } |
| | | 63 | | |
| | 0 | 64 | | payload = payload.Slice(consumed); |
| | | 65 | | |
| | | 66 | | // Rent a 30% bigger buffer |
| | 0 | 67 | | byte[] newBuffer = ArrayPool<byte>.Shared.Rent((int)(_buffer.Length * 1.3)); |
| | 0 | 68 | | _buffer.AsSpan(0, position).CopyTo(newBuffer); |
| | | 69 | | |
| | 0 | 70 | | byte[] toReturn = _buffer; |
| | 0 | 71 | | _buffer = newBuffer; |
| | | 72 | | |
| | 0 | 73 | | ArrayPool<byte>.Shared.Return(toReturn); |
| | 0 | 74 | | } |
| | | 75 | | |
| | 37 | 76 | | return new ReadOnlySpan<byte>(_buffer, 0, position); |
| | 37 | 77 | | } |
| | | 78 | | |
| | | 79 | | private void DeflatePrivate(ReadOnlySpan<byte> payload, Span<byte> output, bool endOfMessage, |
| | | 80 | | out int consumed, out int written, out bool needsMoreOutput) |
| | 37 | 81 | | { |
| | 37 | 82 | | _stream ??= CreateDeflater(); |
| | | 83 | | |
| | 37 | 84 | | if (payload.Length == 0) |
| | 18 | 85 | | { |
| | 18 | 86 | | consumed = 0; |
| | 18 | 87 | | written = 0; |
| | 18 | 88 | | } |
| | | 89 | | else |
| | 19 | 90 | | { |
| | 19 | 91 | | UnsafeDeflate(payload, output, out consumed, out written, out needsMoreOutput); |
| | | 92 | | |
| | 19 | 93 | | if (needsMoreOutput) |
| | 0 | 94 | | { |
| | 0 | 95 | | Debug.Assert(written == output.Length); |
| | 0 | 96 | | return; |
| | | 97 | | } |
| | 19 | 98 | | } |
| | | 99 | | |
| | 37 | 100 | | written += UnsafeFlush(output.Slice(written), out needsMoreOutput); |
| | | 101 | | |
| | 37 | 102 | | if (needsMoreOutput) |
| | 0 | 103 | | { |
| | 0 | 104 | | return; |
| | | 105 | | } |
| | 37 | 106 | | Debug.Assert(output.Slice(written - WebSocketInflater.FlushMarkerLength, WebSocketInflater.FlushMarkerLength |
| | 37 | 107 | | .EndsWith(WebSocketInflater.FlushMarker), "The deflated block must always end with a flus |
| | | 108 | | |
| | 37 | 109 | | if (endOfMessage) |
| | 3 | 110 | | { |
| | | 111 | | // As per RFC we need to remove the flush markers |
| | 3 | 112 | | written -= WebSocketInflater.FlushMarkerLength; |
| | 3 | 113 | | } |
| | | 114 | | |
| | 37 | 115 | | if (endOfMessage && !_persisted) |
| | 0 | 116 | | { |
| | 0 | 117 | | _stream.Dispose(); |
| | 0 | 118 | | _stream = null; |
| | 0 | 119 | | } |
| | 37 | 120 | | } |
| | | 121 | | |
| | | 122 | | private unsafe void UnsafeDeflate(ReadOnlySpan<byte> input, Span<byte> output, out int consumed, out int written |
| | 19 | 123 | | { |
| | 19 | 124 | | Debug.Assert(_stream is not null); |
| | | 125 | | |
| | 19 | 126 | | fixed (byte* fixedInput = input) |
| | 19 | 127 | | fixed (byte* fixedOutput = output) |
| | 19 | 128 | | { |
| | 19 | 129 | | _stream.NextIn = (IntPtr)fixedInput; |
| | 19 | 130 | | _stream.AvailIn = (uint)input.Length; |
| | | 131 | | |
| | 19 | 132 | | _stream.NextOut = (IntPtr)fixedOutput; |
| | 19 | 133 | | _stream.AvailOut = (uint)output.Length; |
| | | 134 | | |
| | | 135 | | // The flush is set to Z_NO_FLUSH, which allows deflate to decide |
| | | 136 | | // how much data to accumulate before producing output, |
| | | 137 | | // in order to maximize compression. |
| | 19 | 138 | | var errorCode = Deflate(_stream, FlushCode.NoFlush); |
| | | 139 | | |
| | 19 | 140 | | consumed = input.Length - (int)_stream.AvailIn; |
| | 19 | 141 | | written = output.Length - (int)_stream.AvailOut; |
| | | 142 | | |
| | | 143 | | // It is important here to also check that we haven't |
| | | 144 | | // exhausted the output buffer because after deflating we're |
| | | 145 | | // always going to issue a flush and a flush with empty output |
| | | 146 | | // is going to throw. |
| | 19 | 147 | | needsMoreBuffer = errorCode == ErrorCode.BufError |
| | 19 | 148 | | || _stream.AvailIn > 0 |
| | 19 | 149 | | || written == output.Length; |
| | 19 | 150 | | } |
| | 19 | 151 | | } |
| | | 152 | | |
| | | 153 | | private unsafe int UnsafeFlush(Span<byte> output, out bool needsMoreBuffer) |
| | 37 | 154 | | { |
| | 37 | 155 | | Debug.Assert(_stream is not null); |
| | 37 | 156 | | Debug.Assert(_stream.AvailIn == 0); |
| | 37 | 157 | | Debug.Assert(output.Length > 0); |
| | | 158 | | |
| | 37 | 159 | | fixed (byte* fixedOutput = output) |
| | 37 | 160 | | { |
| | 37 | 161 | | _stream.NextIn = IntPtr.Zero; |
| | 37 | 162 | | _stream.AvailIn = 0; |
| | | 163 | | |
| | 37 | 164 | | _stream.NextOut = (IntPtr)fixedOutput; |
| | 37 | 165 | | _stream.AvailOut = (uint)output.Length; |
| | | 166 | | |
| | | 167 | | // We need to use Z_BLOCK_FLUSH to instruct the zlib to flush all outstanding |
| | | 168 | | // data but also not to emit a deflate block boundary. After we know that there is no |
| | | 169 | | // more data, we can safely proceed to instruct the library to emit the boundary markers. |
| | 37 | 170 | | ErrorCode errorCode = Deflate(_stream, FlushCode.Block); |
| | 37 | 171 | | Debug.Assert(errorCode is ErrorCode.Ok or ErrorCode.BufError); |
| | | 172 | | |
| | | 173 | | // We need at least 6 bytes to guarantee that we can emit a deflate block boundary. |
| | 37 | 174 | | needsMoreBuffer = _stream.AvailOut < 6; |
| | | 175 | | |
| | 37 | 176 | | if (!needsMoreBuffer) |
| | 37 | 177 | | { |
| | | 178 | | // The flush is set to Z_SYNC_FLUSH, all pending output is flushed |
| | | 179 | | // to the output buffer and the output is aligned on a byte boundary, |
| | | 180 | | // so that the decompressor can get all input data available so far. |
| | | 181 | | // This completes the current deflate block and follows it with an empty |
| | | 182 | | // stored block that is three bits plus filler bits to the next byte, |
| | | 183 | | // followed by four bytes (00 00 ff ff). |
| | 37 | 184 | | errorCode = Deflate(_stream, FlushCode.SyncFlush); |
| | 37 | 185 | | Debug.Assert(errorCode == ErrorCode.Ok); |
| | 37 | 186 | | } |
| | | 187 | | |
| | 37 | 188 | | return output.Length - (int)_stream.AvailOut; |
| | | 189 | | } |
| | 37 | 190 | | } |
| | | 191 | | |
| | | 192 | | private static ErrorCode Deflate(ZLibStreamHandle stream, FlushCode flushCode) |
| | 93 | 193 | | { |
| | 93 | 194 | | ErrorCode errorCode = stream.Deflate(flushCode); |
| | | 195 | | |
| | 93 | 196 | | if (errorCode is ErrorCode.Ok or ErrorCode.StreamEnd or ErrorCode.BufError) |
| | 93 | 197 | | { |
| | 93 | 198 | | return errorCode; |
| | | 199 | | } |
| | | 200 | | |
| | 0 | 201 | | string message = errorCode == ErrorCode.StreamError |
| | 0 | 202 | | ? SR.ZLibErrorInconsistentStream |
| | 0 | 203 | | : SR.Format(SR.ZLibErrorUnexpected, (int)errorCode); |
| | 0 | 204 | | throw new WebSocketException(message); |
| | 93 | 205 | | } |
| | | 206 | | |
| | | 207 | | private ZLibStreamHandle CreateDeflater() |
| | 37 | 208 | | { |
| | | 209 | | try |
| | 37 | 210 | | { |
| | 37 | 211 | | return ZLibStreamHandle.CreateForDeflate(level: CompressionLevel.DefaultCompression, |
| | 37 | 212 | | windowBits: _windowBits, |
| | 37 | 213 | | memLevel: Deflate_DefaultMemLevel, |
| | 37 | 214 | | strategy: CompressionStrategy.DefaultStrategy); |
| | | 215 | | } |
| | 0 | 216 | | catch (Exception ex) |
| | 0 | 217 | | { |
| | 0 | 218 | | throw new WebSocketException(ex.Message, ex.InnerException); |
| | | 219 | | } |
| | 37 | 220 | | } |
| | | 221 | | } |
| | | 222 | | } |
| | | 223 | | |