Summary

Class:CBAM.HTTP.HTTPResponseContentFromStream_KnownLength
Assembly:CBAM.HTTP
File(s):/repo-dir/contents/Source/Code/CBAM.HTTP/HTTP.Implementation.cs
Covered lines:39
Uncovered lines:8
Coverable lines:47
Total lines:387
Line coverage:82.9%
Branch coverage:93.3%

Coverage History

Metrics

MethodCyclomatic complexity NPath complexity Sequence coverage Branch coverage
.ctor(...)101%0%
ReadToBuffer()1300.767%1%

File(s)

/repo-dir/contents/Source/Code/CBAM.HTTP/HTTP.Implementation.cs

#LineLine coverage
 1/*
 2 * Copyright 2017 Stanislav Muhametsin. All rights Reserved.
 3 *
 4 * Licensed  under the  Apache License,  Version 2.0  (the "License");
 5 * you may not use  this file  except in  compliance with the License.
 6 * You may obtain a copy of the License at
 7 *
 8 *   http://www.apache.org/licenses/LICENSE-2.0
 9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed  under the  License is distributed on an "AS IS" BASIS,
 12 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 13 * implied.
 14 *
 15 * See the License for the specific language governing permissions and
 16 * limitations under the License.
 17 */
 18using System;
 19using System.Collections.Generic;
 20using System.IO;
 21using System.Linq;
 22using System.Text;
 23using System.Threading;
 24using System.Threading.Tasks;
 25using UtilPack;
 26
 27using TRequestHeaderDictionary =
 28#if NET40
 29   CBAM.HTTP.DictionaryWithReadOnlyAPI<System.String, CBAM.HTTP.ListWithReadOnlyAPI<System.String>>
 30#else
 31   System.Collections.Generic.Dictionary<System.String, System.Collections.Generic.List<string>>
 32#endif
 33   ;
 34
 35
 36namespace CBAM.HTTP
 37{
 38   internal sealed class HTTPRequestImpl : HTTPRequest
 39   {
 40      public HTTPRequestImpl()
 41      {
 42         this.Headers = new TRequestHeaderDictionary();
 43      }
 44
 45      public TRequestHeaderDictionary Headers { get; }
 46
 47      public String Method { get; set; }
 48      public String Path { get; set; }
 49
 50      public String Version { get; set; }
 51
 52      public HTTPRequestContent Content { get; set; }
 53
 54
 55   }
 56
 57   internal sealed class HTTPResponseImpl : HTTPResponse
 58   {
 59      public HTTPResponseImpl(
 60         Int32 statusCode,
 61         String statusCodeMessage,
 62         String version,
 63         IDictionary<String, List<String>> headers,
 64         HTTPResponseContent content
 65         )
 66      {
 67         this.Headers = new System.Collections.ObjectModel.ReadOnlyDictionary<String, IReadOnlyList<String>>( headers.To
 68            kvp => kvp.Key,
 69            kvp =>
 70#if NET40
 71            new ListWithReadOnlyAPI<String>
 72#else
 73            new System.Collections.ObjectModel.ReadOnlyCollection<String>
 74#endif
 75            ( kvp.Value )
 76            ) );
 77         this.StatusCode = statusCode;
 78         this.StatusCodeMessage = statusCodeMessage;
 79         this.Version = ArgumentValidator.ValidateNotEmpty( nameof( version ), version );
 80         this.Content = ArgumentValidator.ValidateNotNull( nameof( content ), content );
 81      }
 82
 83      public IReadOnlyDictionary<String, IReadOnlyList<String>> Headers { get; }
 84
 85      public Int32 StatusCode { get; }
 86      public String StatusCodeMessage { get; }
 87
 88      public String Version { get; }
 89
 90      public HTTPResponseContent Content { get; }
 91
 92
 93   }
 94
 95   internal sealed class HTTPRequestContentFromString : HTTPRequestContent
 96   {
 97      public HTTPRequestContentFromString(
 98         String str,
 99         Encoding encoding
 100         )
 101      {
 102         this.StringContent = str ?? String.Empty;
 103         this.Encoding = encoding ?? System.Text.Encoding.UTF8;
 104      }
 105
 106      public Int64? ByteCount => this.Encoding.GetByteCount( this.StringContent );
 107
 108      public ValueTask<Int64> WriteToStream( HTTPWriter writer, Int64? seenByteCount )
 109      {
 110         return writer.WriteToStreamAsync( this.Encoding, this.StringContent, seenByteCount.HasValue ? (Int32?) seenByte
 111      }
 112
 113      public Encoding Encoding { get; set; }
 114
 115      public String StringContent { get; }
 116
 117      public Boolean ContentEndIsKnown => true;
 118   }
 119
 120   internal sealed class HTTPResponseContentFromStream_KnownLength : HTTPResponseContent
 121   {
 122      private const Int32 INITIAL = 0;
 123      private const Int32 READING = 1;
 124
 125      private readonly Stream _stream;
 126      private readonly Byte[] _preReadData;
 127      private readonly BufferAdvanceState _bufferAdvanceState;
 128      private readonly CancellationToken _token;
 129      private readonly Int64 _byteCount;
 130      private Int64 _bytesRemaining;
 131      private Int32 _state;
 132
 133
 2134      public HTTPResponseContentFromStream_KnownLength(
 2135         Stream stream,
 2136         Byte[] buffer,
 2137         BufferAdvanceState bufferAdvanceState,
 2138         Int64 byteCount,
 2139         CancellationToken token
 2140         )
 141      {
 2142         this._stream = ArgumentValidator.ValidateNotNull( nameof( stream ), stream );
 2143         this._preReadData = ArgumentValidator.ValidateNotNull( nameof( buffer ), buffer );
 2144         this._bufferAdvanceState = ArgumentValidator.ValidateNotNull( nameof( BufferAdvanceState ), bufferAdvanceState 
 2145         this._byteCount = byteCount;
 2146         this._bytesRemaining = byteCount;
 2147         this._token = token;
 2148      }
 149
 0150      public Boolean ContentEndIsKnown => this.ByteCount.HasValue;
 151
 2152      public Int64? ByteCount => this._byteCount;
 153
 2154      public Int64? BytesRemaining => this._bytesRemaining < 0 ? default : Interlocked.Read( ref this._bytesRemaining );
 155
 156      public async ValueTask<Int32> ReadToBuffer( Byte[] array, Int32 offset, Int32 count )
 157      {
 2158         array.CheckArrayArguments( offset, count, true );
 159         Int32 bytesRead;
 2160         if ( Interlocked.CompareExchange( ref this._state, READING, INITIAL ) == INITIAL )
 161         {
 162            // TODO support for multi-part form stuff
 163            try
 164            {
 2165               var remaining = this._bytesRemaining;
 166
 2167               if ( remaining == 0 )
 168               {
 0169                  bytesRead = 0;
 0170               }
 171               else
 172               {
 2173                  if ( remaining > 0 )
 174                  {
 2175                     count = (Int32) Math.Min( count, remaining );
 176                  }
 177
 2178                  var aState = this._bufferAdvanceState;
 2179                  var bufferRemaining = aState.BufferTotal - aState.BufferOffset;
 2180                  bytesRead = 0;
 2181                  if ( bufferRemaining > 0 )
 182                  {
 183                     // We have some data in buffer
 2184                     var bufferReadCount = Math.Min( bufferRemaining, count );
 2185                     Array.Copy( this._preReadData, aState.BufferOffset, array, offset, bufferReadCount );
 2186                     aState.Advance( bufferReadCount );
 2187                     if ( remaining > 0 )
 188                     {
 2189                        this._bytesRemaining -= bufferReadCount;
 190                     }
 2191                     count -= bufferReadCount;
 2192                     offset += bufferReadCount;
 2193                     bytesRead = bufferReadCount;
 194
 195                  }
 196
 2197                  if ( count > 0 )
 198                  {
 0199                     var streamRead = await this._stream.ReadAsync( array, offset, count, this._token );
 0200                     bytesRead += streamRead;
 0201                     if ( remaining > 0 )
 202                     {
 0203                        this._bytesRemaining -= streamRead;
 204                     }
 205                  }
 206
 207               }
 2208            }
 209            finally
 210            {
 2211               Interlocked.Exchange( ref this._state, INITIAL );
 212            }
 213         }
 214         else
 215         {
 0216            throw new InvalidOperationException( "Concurrent access" );
 217         }
 218
 2219         return bytesRead;
 2220      }
 221   }
 222
 223   internal sealed class HTTPResponseContentFromStream_Chunked : HTTPResponseContent
 224   {
 225      private static readonly Byte[] CRLF = new[] { (Byte) '\r', (Byte) '\n' };
 226      private static readonly Byte[] TerminatingChunk = new[] { (Byte) '0', (Byte) '\r', (Byte) '\n' }; //, (Byte) '\r',
 227
 228      private const Int32 INITIAL = 0;
 229      private const Int32 READING = 1;
 230
 231      private readonly Stream _stream;
 232      private readonly ResizableArray<Byte> _buffer;
 233      private readonly BufferAdvanceState _advanceState;
 234      private readonly Int32 _streamReadCount;
 235      private readonly CancellationToken _token;
 236
 237
 238      private Int32 _state;
 239      private Int32 _chunkRemaining;
 240
 241      public HTTPResponseContentFromStream_Chunked(
 242         Stream stream,
 243         ResizableArray<Byte> buffer,
 244         BufferAdvanceState advanceState,
 245         Int32 firstChunkCount,
 246         Int32 streamReadCount,
 247         CancellationToken token
 248         )
 249      {
 250         this._state = INITIAL;
 251         this._stream = ArgumentValidator.ValidateNotNull( nameof( stream ), stream );
 252         this._buffer = ArgumentValidator.ValidateNotNull( nameof( buffer ), buffer );
 253         this._advanceState = ArgumentValidator.ValidateNotNull( nameof( advanceState ), advanceState );
 254         this._token = token;
 255         this._streamReadCount = streamReadCount;
 256         this._chunkRemaining = Math.Max( 0, firstChunkCount );
 257      }
 258
 259
 260      public Int64? BytesRemaining => Math.Max( this._chunkRemaining, 0 );
 261
 262      public Int64? ByteCount => null;
 263
 264      public Boolean ContentEndIsKnown => true;
 265
 266      public async ValueTask<Int32> ReadToBuffer( Byte[] array, Int32 offset, Int32 count )
 267      {
 268         array.CheckArrayArguments( offset, count, true );
 269         Int32 retVal;
 270         if ( Interlocked.CompareExchange( ref this._state, READING, INITIAL ) == INITIAL )
 271         {
 272            try
 273            {
 274               if ( this._chunkRemaining < 0 )
 275               {
 276                  // Ended
 277                  retVal = 0;
 278               }
 279               else
 280               {
 281                  // Our whole chunk data has already been read to buffer.
 282                  retVal = Math.Min( this._chunkRemaining, count );
 283                  Array.Copy( this._buffer.Array, this._advanceState.BufferOffset, array, offset, retVal );
 284                  this._advanceState.Advance( retVal );
 285                  this._chunkRemaining -= retVal;
 286
 287                  if ( this._chunkRemaining == 0 )
 288                  {
 289                     // We must read next chunk
 290                     HTTPUtils.EraseReadData( this._advanceState, this._buffer );
 291                     if ( ( this._chunkRemaining = await ReadNextChunk( this._stream, this._buffer, this._advanceState, 
 292                     {
 293                        // Clear data
 294                        HTTPUtils.EraseReadData( this._advanceState, this._buffer );
 295                     }
 296
 297                  }
 298               }
 299            }
 300            finally
 301            {
 302               Interlocked.Exchange( ref this._state, INITIAL );
 303            }
 304         }
 305         else
 306         {
 307            throw new InvalidOperationException( "Concurrent access" );
 308         }
 309
 310         return retVal;
 311      }
 312
 313
 314
 315
 316      // When this method is done, the buffer will have header + chunk (including terminating CRLF) in its contents
 317      // It assumes that chunk headers starts at advanceState.BufferOffset
 318      // Returns the chunk length (>0) or -1 if last chunk. Will be -1 if last chunk. The advanceState.BufferOffset will
 319      public static async Task<Int32> ReadNextChunk(
 320         Stream stream,
 321         ResizableArray<Byte> buffer,
 322         BufferAdvanceState advanceState,
 323         Int32 streamReadCount,
 324         CancellationToken token
 325         )
 326      {
 327         var start = advanceState.BufferOffset;
 328         await stream.ReadUntilMaybeAsync( buffer, advanceState, CRLF, streamReadCount );
 329         var array = buffer.Array;
 330         const Int32 LAST_CHUNK_SIZE = 3;
 331         var isLastChunk = ArrayEqualityComparer<Byte>.RangeEquality( TerminatingChunk, 0, LAST_CHUNK_SIZE, array, start
 332         advanceState.Advance( 2 );
 333         Int32 retVal;
 334         if ( isLastChunk )
 335         {
 336            // TODO trailers!
 337            await stream.ReadUntilMaybeAsync( buffer, advanceState, CRLF, streamReadCount );
 338            retVal = -1;
 339         }
 340         else
 341         {
 342            var idx = start;
 343            retVal = 0;
 344            Int32 curHex;
 345            while ( ( curHex = ExtractASCIIHexValue( array, idx ) ) >= 0 )
 346            {
 347               retVal = 0x10 * retVal + curHex;
 348               ++idx;
 349            }
 350            // TODO extensions!
 351            // Now read content.
 352            streamReadCount = retVal + 2 - ( advanceState.BufferTotal - advanceState.BufferOffset );
 353            if ( streamReadCount > 0 )
 354            {
 355               await stream.ReadSpecificAmountAsync( buffer, advanceState.BufferTotal, streamReadCount, token );
 356               advanceState.ReadMore( streamReadCount );
 357            }
 358         }
 359
 360         return retVal;
 361
 362      }
 363
 364      private static Int32 ExtractASCIIHexValue( Byte[] array, Int32 idx )
 365      {
 366         var cur = array[idx];
 367         Int32 retVal;
 368         if ( cur >= '0' && cur <= '9' )
 369         {
 370            retVal = cur - 0x30;
 371         }
 372         else if ( ( cur >= 'A' && cur <= 'F' ) )
 373         {
 374            retVal = cur - 0x37;
 375         }
 376         else if ( ( cur >= 'a' && cur <= 'f' ) )
 377         {
 378            retVal = cur - 0x57;
 379         }
 380         else
 381         {
 382            retVal = -1;
 383         }
 384         return retVal;
 385      }
 386   }
 387}