< Summary

Line coverage
80%
Covered lines: 120
Uncovered lines: 29
Coverable lines: 149
Total lines: 282
Line coverage: 80.5%
Branch coverage
70%
Covered branches: 41
Total branches: 58
Branch coverage: 70.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%11100%
Dispose()100%22100%
Prepare(...)100%22100%
AddBytes(...)100%88100%
Inflate(...)77.77%181896.29%
Finish(...)42.85%141450%
ReleaseBuffer()100%22100%
IsFinished(...)0%220%
Inflate(...)70%101085.71%
CreateInflater()100%1157.14%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/Compression/WebSocketInflater.cs

#LineLine coverage
 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
 4using System.Buffers;
 5using System.Diagnostics;
 6using static System.IO.Compression.ZLibNative;
 7
 8namespace System.Net.WebSockets.Compression
 9{
 10    /// <summary>
 11    /// Provides a wrapper around the ZLib decompression API.
 12    /// </summary>
 13    internal sealed class WebSocketInflater : IDisposable
 14    {
 15        internal const int FlushMarkerLength = 4;
 19516        internal static ReadOnlySpan<byte> FlushMarker => [0x00, 0x00, 0xFF, 0xFF];
 17
 18        private readonly int _windowBits;
 19        private ZLibStreamHandle? _stream;
 20        private readonly bool _persisted;
 21
 22        /// <summary>
 23        /// There is no way of knowing, when decoding data, if the underlying inflater
 24        /// has flushed all outstanding data to consumer other than to provide a buffer
 25        /// and see whether any bytes are written. There are cases when the consumers
 26        /// provide a buffer exactly the size of the uncompressed data and in this case
 27        /// to avoid requiring another read we will use this field.
 28        /// </summary>
 29        private byte? _remainingByte;
 30
 31        /// <summary>
 32        /// The last added bytes to the inflater were part of the final
 33        /// payload for the message being sent.
 34        /// </summary>
 35        private bool _endOfMessage;
 36
 37        private byte[]? _buffer;
 38
 39        /// <summary>
 40        /// The position for the next unconsumed byte in the inflate buffer.
 41        /// </summary>
 42        private int _position;
 43
 44        /// <summary>
 45        /// How many unconsumed bytes are left in the inflate buffer.
 46        /// </summary>
 47        private int _available;
 48
 202549        internal WebSocketInflater(int windowBits, bool persisted)
 202550        {
 202551            _windowBits = -windowBits; // Negative for raw deflate
 202552            _persisted = persisted;
 202553        }
 54
 3855        public Memory<byte> Memory => _buffer.AsMemory(_position + _available);
 56
 33657        public Span<byte> Span => _buffer.AsSpan(_position + _available);
 58
 59        public void Dispose()
 202560        {
 202561            _stream?.Dispose();
 202562            ReleaseBuffer();
 202563        }
 64
 65        /// <summary>
 66        /// Initializes the inflater by allocating a buffer so the websocket can receive directly onto it.
 67        /// </summary>
 68        /// <param name="payloadLength">the length of the message payload</param>
 69        /// <param name="userBufferLength">the length of the buffer where the payload will be inflated</param>
 70        public void Prepare(long payloadLength, int userBufferLength)
 16571        {
 16572            if (_buffer is not null)
 1473            {
 1474                Debug.Assert(_available > 0);
 75
 1476                _buffer.AsSpan(_position, _available).CopyTo(_buffer);
 1477                _position = 0;
 1478            }
 79            else
 15180            {
 81                // Rent a buffer as close to the size of the user buffer as possible.
 82                // If the payload is smaller than the user buffer, rent only as much as we need.
 15183                _buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(userBufferLength, payloadLength));
 15184            }
 16585        }
 86
 87        public void AddBytes(int totalBytesReceived, bool endOfMessage)
 80888        {
 80889            Debug.Assert(totalBytesReceived == 0 || _buffer is not null, "Prepare must be called.");
 90
 80891            _available += totalBytesReceived;
 80892            _endOfMessage = endOfMessage;
 93
 80894            if (endOfMessage)
 15895            {
 15896                if (_buffer is null)
 11397                {
 11398                    Debug.Assert(_available == 0);
 99
 113100                    _buffer = ArrayPool<byte>.Shared.Rent(FlushMarkerLength);
 113101                    _available = FlushMarkerLength;
 113102                    FlushMarker.CopyTo(_buffer);
 113103                }
 104                else
 45105                {
 45106                    if (_buffer.Length < _available + FlushMarkerLength)
 1107                    {
 1108                        byte[] newBuffer = ArrayPool<byte>.Shared.Rent(_available + FlushMarkerLength);
 1109                        _buffer.AsSpan(0, _available).CopyTo(newBuffer);
 110
 1111                        byte[] toReturn = _buffer;
 1112                        _buffer = newBuffer;
 113
 1114                        ArrayPool<byte>.Shared.Return(toReturn);
 1115                    }
 116
 45117                    FlushMarker.CopyTo(_buffer.AsSpan(_available));
 45118                    _available += FlushMarkerLength;
 45119                }
 158120            }
 808121        }
 122
 123        /// <summary>
 124        /// Inflates the last receive payload into the provided buffer.
 125        /// </summary>
 126        public unsafe bool Inflate(Span<byte> output, out int written)
 682127        {
 682128            _stream ??= CreateInflater();
 129
 682130            bool streamEnded = false;
 131
 682132            if (_available > 0 && output.Length > 0)
 152133            {
 134                int consumed;
 135
 152136                fixed (byte* bufferPtr = _buffer)
 152137                {
 152138                    _stream.NextIn = (IntPtr)(bufferPtr + _position);
 152139                    _stream.AvailIn = (uint)_available;
 140
 152141                    written = Inflate(_stream, output, FlushCode.NoFlush, out streamEnded);
 127142                    consumed = _available - (int)_stream.AvailIn;
 127143                }
 144
 127145                _position += consumed;
 127146                _available -= consumed;
 127147            }
 148            else
 530149            {
 530150                written = 0;
 530151            }
 152
 657153            if (_available == 0)
 653154            {
 653155                ReleaseBuffer();
 653156                return _endOfMessage ? Finish(output, ref written) : true;
 157            }
 158
 4159            if (streamEnded && _available > 0)
 4160            {
 161                // zlib reached the end of the DEFLATE stream (a BFINAL=1 final block) while compressed
 162                // bytes still remain that it will never consume. permessage-deflate messages are not
 163                // expected to contain a final block; continuing would make no forward progress (the
 164                // inflater would report empty results forever and hang the caller's receive loop), so
 165                // reject the message.
 4166                throw new WebSocketException(SR.net_WebSockets_DataAfterBFinal);
 167            }
 168
 0169            return false;
 653170        }
 171
 172        /// <summary>
 173        /// Finishes the decoding by flushing any outstanding data to the output.
 174        /// </summary>
 175        /// <returns>true if the flush completed, false to indicate that there is more outstanding data.</returns>
 176        private bool Finish(Span<byte> output, ref int written)
 17177        {
 17178            Debug.Assert(_stream is not null && _stream.AvailIn == 0);
 17179            Debug.Assert(_available == 0);
 180
 17181            if (_remainingByte is not null)
 0182            {
 0183                if (output.Length == written)
 0184                {
 0185                    return false;
 186                }
 0187                output[written] = _remainingByte.GetValueOrDefault();
 0188                _remainingByte = null;
 0189                written += 1;
 0190            }
 191
 192            // If we have more space in the output, try to inflate
 17193            if (output.Length > written)
 17194            {
 17195                written += Inflate(_stream, output[written..], FlushCode.SyncFlush, out _);
 17196            }
 197
 198            // After inflate, if we have more space in the output then it means that we
 199            // have finished. Otherwise we need to manually check for more data.
 17200            if (written < output.Length || IsFinished(_stream, out _remainingByte))
 17201            {
 17202                if (!_persisted)
 0203                {
 0204                    _stream.Dispose();
 0205                    _stream = null;
 0206                }
 17207                return true;
 208            }
 209
 0210            return false;
 17211        }
 212
 213        private void ReleaseBuffer()
 2678214        {
 2678215            if (_buffer is byte[] toReturn)
 264216            {
 264217                _buffer = null;
 264218                _available = 0;
 264219                _position = 0;
 220
 264221                ArrayPool<byte>.Shared.Return(toReturn);
 264222            }
 2678223        }
 224
 225        private static bool IsFinished(ZLibStreamHandle stream, out byte? remainingByte)
 0226        {
 227            // There is no other way to make sure that we've consumed all data
 228            // but to try to inflate again with at least one byte of output buffer.
 0229            byte b = 0;
 0230            if (Inflate(stream, new Span<byte>(ref b), FlushCode.SyncFlush, out _) == 0)
 0231            {
 0232                remainingByte = null;
 0233                return true;
 234            }
 235
 0236            remainingByte = b;
 0237            return false;
 0238        }
 239
 240        private static unsafe int Inflate(ZLibStreamHandle stream, Span<byte> destination, FlushCode flushCode, out bool
 169241        {
 169242            Debug.Assert(destination.Length > 0);
 243            ErrorCode errorCode;
 244
 169245            fixed (byte* bufPtr = destination)
 169246            {
 169247                stream.NextOut = (IntPtr)bufPtr;
 169248                stream.AvailOut = (uint)destination.Length;
 249
 169250                errorCode = stream.Inflate(flushCode);
 251
 169252                if (errorCode is ErrorCode.Ok or ErrorCode.StreamEnd or ErrorCode.BufError)
 144253                {
 144254                    streamEnded = errorCode == ErrorCode.StreamEnd;
 144255                    return destination.Length - (int)stream.AvailOut;
 256                }
 25257            }
 258
 25259            string message = errorCode switch
 25260            {
 0261                ErrorCode.MemError => SR.ZLibErrorNotEnoughMemory,
 25262                ErrorCode.DataError => SR.ZLibUnsupportedCompression,
 0263                ErrorCode.StreamError => SR.ZLibErrorInconsistentStream,
 0264                _ => SR.Format(SR.ZLibErrorUnexpected, (int)errorCode)
 25265            };
 25266            throw new WebSocketException(message);
 144267        }
 268
 269        private ZLibStreamHandle CreateInflater()
 241270        {
 271            try
 241272            {
 241273                return ZLibStreamHandle.CreateForInflate(_windowBits);
 274            }
 0275            catch (Exception ex)
 0276            {
 0277                throw new WebSocketException(ex.Message, ex.InnerException);
 278            }
 241279        }
 280    }
 281}
 282