File: MS\Internal\IO\Packaging\ByteRangeDownloader.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#if DEBUG
#define TRACE
#endif
//
//
// Description:
// Downloader for downloading resources from the Internet in byte ranges using .NET
//  web requests. This class is used by ByteWrapper (unmanaged code) to make additional
//  web requests other than through WININET
 
using System.Collections;
using System.ComponentModel;              // For Win32Exception
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Cache;                     // For RequestCachePolicy
using System.Runtime.InteropServices;   // For Marshal
using System.Threading;                  // For Mutex
using Microsoft.Win32.SafeHandles;
using MS.Internal.PresentationCore;
 
namespace MS.Internal.IO.Packaging
{
    /// <summary>
    /// Downloader for byte range requests
    /// </summary>
    /// <remarks>
    /// For now, we will only process one batch of requests at a time. We will most likely
    ///  want to spin multiple threads and do multiple batches at a time in the future
    /// </remarks>
    internal class ByteRangeDownloader : IDisposable
    {
         //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructors
 
        /// <summary>
        /// Constructor for ByteRangeDownloader - for UrlMon usage
        /// </summary>
        /// <param name="eventHandle">event to signal when new data is available in tempStream</param>
        /// <param name="requestedUri">uri we should make requests to</param>
        /// <param name="tempFileName">temp file to write to</param>
        internal ByteRangeDownloader(Uri requestedUri, string tempFileName, SafeWaitHandle eventHandle)
            : this(requestedUri, eventHandle)
        {
            ArgumentNullException.ThrowIfNull(tempFileName);
 
            if (tempFileName.Length <= 0)
            {
                throw new ArgumentException(SR.InvalidTempFileName, "tempFileName");
            }
 
            _tempFileStream = File.Open(tempFileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
        }
 
        /// <summary>
        /// Constructor for ByteRangeDownloader - used by NetStream who owns the tempStream
        /// </summary>
        /// <param name="eventHandle">event to signal when new data is available in tempStream</param>
        /// <param name="fileMutex">mutex to synchronize access to tempStream</param>
        /// <param name="requestedUri">uri we should make requests to</param>
        /// <param name="tempStream">stream to write data to</param>
        internal ByteRangeDownloader(Uri requestedUri, Stream tempStream, SafeWaitHandle eventHandle, Mutex fileMutex)
            : this(requestedUri, eventHandle)
        {
            Debug.Assert(fileMutex != null, "FileMutex must be a valid mutex");
            Debug.Assert(tempStream != null, "ByteRangeDownloader requires a stream to write to");
            _tempFileStream = tempStream;
            _fileMutex = fileMutex;
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Interfaces
        //
        //------------------------------------------------------
 
        #region IDisposable
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);  // not strictly necessary, but if we ever have a subclass with a finalizer, this will be more efficient
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (_syncObject)
                {
                    if (!_disposed)
                    {
                        try
                        {
                            // if there is no mutex, then we own the stream and we should close it
                            if (FileMutex == null && _tempFileStream != null)
                            {
                                _tempFileStream.Close();
                            }
                        }
                        finally
                        {
                            _requestedUri = null;
                            _byteRangesInProgress = null;
                            _requestsOnWait = null;
                            _byteRangesAvailable = null;
                            _tempFileStream = null;
                            _eventHandle = null;
                            _proxy = null;
                            _credentials = null;
                            _cachePolicy = null;
                            _disposed = true;
                        }
                    }
                }
            }
        }
 
        #endregion IDisposable
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        /// <summary>
        /// Get the byte ranges that are downloaded
        /// </summary>
        /// <returns>byte ranges that are downloaded; byteRanges is one dimensional
        /// array consisting pairs of offset and length</returns>
        internal int[,] GetDownloadedByteRanges()
        {
            int[,] byteRanges = null;
 
            // The worker thread will never call dispose nor this method; no need lock
            CheckDisposed();
 
            lock (_syncObject)
            {
                CheckErroredOutCondition();
 
                int rangeCount = _byteRangesAvailable.Count / 2;
 
                // The worker thread will update the bytes downloaded; need to lock
                byteRanges = new int[rangeCount, 2];
                for (int i = 0; i < rangeCount; ++i)
                {
                    byteRanges[i, Offset_Index] = (int)_byteRangesAvailable[(i * 2) + Offset_Index];
                    byteRanges[i, Length_Index] = (int)_byteRangesAvailable[(i * 2) + Length_Index];
                }
                _byteRangesAvailable.Clear();
            }
 
            return byteRanges;
        }
 
        /// <summary>
        /// Make byte range request
        /// </summary>
        /// <param name="byteRanges">byte ranges to be downloaded; byteRanges is two dimensional
        /// array consisting pairs of offset and length</param>
        internal void RequestByteRanges(int[,] byteRanges)
        {
            // The worker thread will never call dispose nor this method; no need to lock
            CheckDisposed();
 
            ArgumentNullException.ThrowIfNull(byteRanges);
 
            CheckTwoDimensionalByteRanges(byteRanges);
 
            // When multiple byte ranges are requested through one HttpWebRequest, the current HttpWebResponse
            // returns a stream with headers which has to be manually parsed. At this point, we decided to
            // make only one byte range request at a time
 
            // At this point, none of the callers of this class will make more than one range
            //  So, we will assert if more than one byte range request is made
            Debug.Assert(byteRanges.GetLength(0) == 1, "We don't support a request with multiple byte ranges");
 
            _firstRequestMade = true;
 
            // If there is no request in progress, start the request process; otherwise put it in the wait queue
            lock (_syncObject)
            {
                CheckErroredOutCondition();
 
                // _byteRangeInprogress can be cleared out from the worker thread and this method can be called
                //  from the caller thread; need to lock
                if (_byteRangesInProgress == null)
                {
                    _webRequest = CreateHttpWebRequest(byteRanges);
                    _byteRangesInProgress = byteRanges;
                    _webRequest.BeginGetResponse(ResponseCallback, this);
                }
                else    // cannot make request yet, put the request in the wait queue
                {
                    // Lazy Init
                    if (_requestsOnWait == null)
                    {
                        // there are currently only ever two of these (one pair)
                        // so optimize
                        _requestsOnWait = new ArrayList(2);
                    }
 
                    for (int i = 0; i < byteRanges.GetLength(0); ++i)
                    {
                        // Add requests to the wait queue
                        _requestsOnWait.Add((int) byteRanges[i, Offset_Index]);
                        _requestsOnWait.Add((int) byteRanges[i, Length_Index]);
                    }
                }
            }
        }
 
        /// <summary>
        /// Convert bytes ranges from one dimensional array (pairs of offset and length)
        /// to two dimensional array
        /// </summary>
        /// <param name="inByteRanges">byteRanges in one dimensional array consisting pairs of offset and length</param>
        /// <returns>byteRanges in two dimensional array consisting pairs of offset and length</returns>
        static internal int[,] ConvertByteRanges(int[] inByteRanges)
        {
            CheckOneDimensionalByteRanges(inByteRanges);
 
            int[,] outByteRanges = new int[(inByteRanges.Length / 2),2];
 
            for (int i=0, j=0; i < inByteRanges.Length; ++i, ++j)
            {
                outByteRanges[j,Offset_Index] = inByteRanges[i];
                outByteRanges[j,Length_Index] = inByteRanges[i+1];
 
                ++i;
            }
 
            return outByteRanges;
        }
 
        /// <summary>
        /// Convert bytes ranges from two dimensional array (paris of offiset and length)
        /// to one dimensional array
        /// </summary>
        /// <param name="inByteRanges">byteRanges in two dimensional array consisting pairs of offset and length</param>
        /// <returns>byteRanges in one dimensional array consisting pairs of offset and length</returns>
        static internal int[] ConvertByteRanges(int[,] inByteRanges)
        {
            // Normallly we will check the input from the caller
            //  but in our scenario, the two dimensional array is always generated by ByteRangeDownloader
            //  No need to check
#if DEBUG
            CheckTwoDimensionalByteRanges(inByteRanges);
#endif
 
            int[] outByteRanges = new int[inByteRanges.Length];
 
            for (int i=0, j=0; i < inByteRanges.GetLength(0); ++i, ++j)
            {
                outByteRanges[j] = inByteRanges[i, Offset_Index];
                outByteRanges[++j] = inByteRanges[i, Length_Index];
            }
 
            return outByteRanges;
        }
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Internal Properties
        //
        //------------------------------------------------------
 
        #region Internal Properties
 
        /// <summary>
        /// Proxy for all requests to ByteRangeDownloader
        /// </summary>
        internal IWebProxy Proxy
        {
            set
            {
                CheckDisposed();
                ArgumentNullException.ThrowIfNull(value);
 
                if (!_firstRequestMade)
                {
                    _proxy = value;
                }
                else    // Once first request is made it cannot change
                {
                    throw new InvalidOperationException(SR.RequestAlreadyStarted);
                }
            }
        }
 
        /// <summary>
        /// Authentication information for all requests to ByteRangeDownloader
        /// </summary>
        internal ICredentials Credentials
        {
            set
            {
                CheckDisposed();
                _credentials = value;
            }
        }
 
        /// <summary>
        /// Cache Policy for all requests to ByteRangeDownloader
        /// </summary>
        internal RequestCachePolicy CachePolicy
        {
            set
            {
                CheckDisposed();
                if (!_firstRequestMade)
                {
                    _cachePolicy = value;
                }
                else    // Once first request is made it cannot cahnge
                {
                    throw new InvalidOperationException(SR.RequestAlreadyStarted);
                }
            }
        }
 
        /// <summary>
        /// OS synchronization object use to synchronize access to the temp file - if null, no sync is needed
        /// </summary>
        internal Mutex FileMutex
        {
            get
            {
                CheckDisposed();
                return _fileMutex;
            }
        }
 
        /// <summary>
        /// Returns true if any request errored out
        /// </summary>
        internal bool ErroredOut
        {
            get
            {
                CheckDisposed();
 
                lock (_syncObject)
                {
                    return _erroredOut;
                }
            }
        }
 
        #endregion Internal Properties
 
        //------------------------------------------------------
        //
        //  Private Classes
        //
        //------------------------------------------------------
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        /// <summary>
        /// Check if the object is disposed
        /// </summary>
        /// <remarks>this._disposed is not changed by a thread while another thread is calling this function</remarks>
        private void CheckDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(null, SR.ByteRangeDownloaderDisposed);
            }
        }
 
        /// <summary>
        /// Constructor for ByteRangeDownloader
        /// </summary>
        private ByteRangeDownloader(Uri requestedUri, SafeWaitHandle eventHandle)
        {
            ArgumentNullException.ThrowIfNull(requestedUri);
 
            // Ensure uri is correct scheme (http or https) Do case-sensitive comparison since Uri.Scheme contract is to return in lower case only.
            if (!string.Equals(requestedUri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) && !string.Equals(requestedUri.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal))
            {
                throw new ArgumentException(SR.InvalidScheme, "requestedUri");
            }
 
            ArgumentNullException.ThrowIfNull(eventHandle);
 
            if (eventHandle.IsInvalid || eventHandle.IsClosed)
            {
                throw new ArgumentException(SR.InvalidEventHandle, "eventHandle");
            }
 
            _requestedUri = requestedUri;
            _eventHandle = eventHandle;
        }
 
        /// <summary>
        /// Check if it has been errored out from the worker thread and re-throw the exception that was
        /// thrown from the worker thread
        /// </summary>
        /// <remarks>No need to lock in this function since the caller always locks before making this call</remarks>
        private void CheckErroredOutCondition()
        {
            if (_erroredOut)
            {
                throw new InvalidOperationException(SR.ByteRangeDownloaderErroredOut, _erroredOutException);
            }
        }
 
        /// <summary>
        /// Download the requested bytes
        /// </summary>
        private HttpWebRequest CreateHttpWebRequest(int[,] byteRanges)
        {
            HttpWebRequest request;
 
            // Create the request object
            request = (HttpWebRequest)WpfWebRequestHelper.CreateRequest(_requestedUri);
            request.ProtocolVersion = HttpVersion.Version11;
            request.Method = "GET";
 
            // Set the Proxy to Empty one; If we don't set this to empty one, it will try to find one for us
            //  and ends up triggering JScript in another assembly. This will throw PolicyException since the JScript
            //  dll doesn't have execution right. This is a bug in CLR; supposed to be fixed later
            // Future work: Need to keep consistent HTTP stack with the WININET one (e.g. authentication, proxy, cookies)
            //            IWebProxy emptyProxy = GlobalProxySelection.GetEmptyWebProxy();
            //            request.Proxy = emptyProxy;
 
            request.Proxy = _proxy;
 
            request.Credentials = _credentials;
            request.CachePolicy = _cachePolicy;
 
            // Add byte ranges (to header)
            for (int i = 0; i < byteRanges.GetLength(0); ++i)
            {
                request.AddRange(byteRanges[i,Offset_Index],
                                 byteRanges[i,Offset_Index] + byteRanges[i,Length_Index] - 1);
            }
 
            return request;
        }
 
        /// <summary>
        /// Raise Win32 events informing a client that the requested bytes are available or it errored out
        /// </summary>
        /// <param name="throwExceptionOnError">indicates if an exception to be thrown on fail to set event</param>
        /// <remarks></remarks>
        private void RaiseEvent(bool throwExceptionOnError)
        {
            if (_eventHandle != null && !_eventHandle.IsInvalid && !_eventHandle.IsClosed)
            {
                if (MS.Win32.UnsafeNativeMethods.SetEvent(_eventHandle) == 0)
                {
                    if (throwExceptionOnError)
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
            }
        }
 
        /// <summary>
        /// ResponseCallBack
        /// </summary>
        /// <param name="ar">async result</param>
        /// <remarks>static method not necessary</remarks>
        private void ResponseCallback(IAsyncResult ar)
        {
            HttpWebResponse webResponse = null;
 
            lock (_syncObject)
            {
                try
                {
                    if (_disposed)
                    {
                        return;
                    }
 
                    // The caller thread can dispose this class and the worker thread need to check the disposed
                    //  condition; need to lock
                    // If disposed, there is nothing to handle
                    webResponse = (HttpWebResponse)WpfWebRequestHelper.EndGetResponse(_webRequest, ar);
 
                    // If it is not partial content, no need to look further
                    if (webResponse.StatusCode == HttpStatusCode.PartialContent)
                    {
                        //
                        // Check for few conditions
                        //
 
                        // Get the header and make sure that it was indeed the byte range response
                        int beginOffset = _byteRangesInProgress[0, Offset_Index];
                        int endOffset = beginOffset+ _byteRangesInProgress[0,Length_Index] - 1;
 
                        // HttpWebRequest in the current CLR does not allow multiple byte range requests.
                        // At this point, none of the callers of this class will make more than one range at a time
                        //  So, we should not receive any response with more than one range returned
 
                        // When multiple byte ranges requests are support in HttpWebRequest eventually,
                        // there is a question on how to handle multipart response (Content-Type=multipart/byteranges)
 
                        // At this point we only need to handle one byte range response (Content-Range header) only
                        // Request was successful
                        // Note: endOffset could be trimmed offset in the case where the response didn't
                        //  satisfy the entire request
                        if (CheckContentRange(webResponse.Headers, beginOffset, ref endOffset))
                        {
                            // Write out the bytes to the temp file
                            if (WriteByteRange(webResponse, beginOffset, endOffset - beginOffset + 1))
                            {
                                // The range is downloaded successfully; add it to the list
                                _byteRangesAvailable.Add(beginOffset);
                                _byteRangesAvailable.Add(endOffset - beginOffset + 1);
                            }
                            else
                                _erroredOut = true;
                        }
                        else
                        {
                            _erroredOut = true;
                            _erroredOutException = new NotSupportedException(SR.ByteRangeRequestIsNotSupported);
                        }
                    }
                    else
                    {
                        _erroredOut = true;
                    }
                }
                catch (Exception e)  // catch (and re-throw) exceptions so we can inform the other thread
                {
                    _erroredOut = true;
                    _erroredOutException = e;
 
                    throw;
                }
                catch   // catch (and re-throw) all kinds of exceptions so we can inform the other thread
                {
                    // inform other thread of error condition
                    _erroredOut= true;
                    _erroredOutException = null;
 
                    throw;
                }
                finally
                {
                    if (webResponse != null)
                    {
                        webResponse.Close();
                    }
 
                    // bytes requested are downloaded or errored out
                    //  inform the caller that these ranges are available
                    RaiseEvent(!_erroredOut);
                }
 
                // If we haven't errored out already, process the next batch
                if (!_erroredOut)
                {
                    ProcessWaitQueue();
                }
           }
        }
 
        /// <summary>
        /// Shared code for mutex and non-mutex to call
        /// </summary>
        private bool Write(Stream s, int offset, int length)
        {
            int readBytes;
 
            // Process the data chunk at a time (Size of the data that can be processed one time is WriteBufferSize)
            _tempFileStream.Seek(offset, SeekOrigin.Begin);
            while (length > 0)
            {
                // Read in the data into the buffer
                readBytes = s.Read(_buffer, 0, WriteBufferSize);
                if (readBytes == 0)
                    break;
 
                // Write it out
                _tempFileStream.Write(_buffer, 0, readBytes);
                length -= readBytes;
            }
 
            if (length != 0)
                return false;
 
            _tempFileStream.Flush();
            return true;
        }
 
        /// <summary>
        /// Write out the downloaded byte ranges to the temp file
        /// </summary>
        /// <param name="response">Http web response</param>
        /// <param name="offset">Offset of the byte range</param>
        /// <param name="length">Length of the byte range</param>
        /// <returns>True if it is successfully written out</returns>
        private bool WriteByteRange(HttpWebResponse response, int offset, int length)
        {
            bool result = false;
 
            // Get the downloaded stream
            using (Stream s = response.GetResponseStream())
            {
                if (_buffer == null)
                {
                    _buffer = new byte[WriteBufferSize];
                }
 
                // mutex available?
                if (_fileMutex != null)
                {
                    // use it
                    try
                    {
                        // block until temp file is available
                        _fileMutex.WaitOne();
                        lock (PackagingUtilities.IsolatedStorageFileLock)
                        {
                            result = Write(s, offset, length);
                        }
                    }
                    finally
                    {
                        _fileMutex.ReleaseMutex();
                    }
                }
                else
                    result = Write(s, offset, length);
            }
 
            return result;
        }
 
        /// <summary>
        /// Process the requests that are in the wait queue
        /// </summary>
        /// <remarks>This is only called from ResponseCallback which synchronize the call</remarks>
        private void ProcessWaitQueue()
        {
            // There is other requests waiting in the queue; Process those
            if (_requestsOnWait != null && _requestsOnWait.Count > 0)
            {
                // _byteRangesInProgress is already allocated and can be reused
                _byteRangesInProgress[0,Offset_Index] = (int) _requestsOnWait[Offset_Index];
                _byteRangesInProgress[0,Length_Index] = (int) _requestsOnWait[Length_Index];
                _requestsOnWait.RemoveRange(0, 2);
 
                _webRequest = CreateHttpWebRequest(_byteRangesInProgress);
                _webRequest.BeginGetResponse(ResponseCallback, this);
            }
            else
            {
                // If there is nothing more to process in the wait queue, clear out
                //  _byteRangesInProgress so that subsequent byte range requests can be made
                _byteRangesInProgress = null;
            }
        }
 
        /// <summary>
        /// Check if the given byte ranges follows correct format
        /// 1) number of items in the array should be even number (It should be one dimensional array consisting pairs
        ///     of offset and length
        /// 2) offset cannot be less than 0
        /// 3) length cannot be less than equal to 0
        /// </summary>
        /// <param name="byteRanges">Byte ranges to be checked</param>
        /// <returns>True if the byte ranges are in correct format</returns>
        static private void CheckOneDimensionalByteRanges(int[] byteRanges)
        {
            // The byteRanges should never be less; perf optimization
            if (byteRanges.Length < 2 || (byteRanges.Length % 2) != 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidByteRanges, "byteRanges"));
            }
 
            for (int i = 0; i < byteRanges.Length; i++)
            {
                if (byteRanges[i] < 0 || byteRanges[i+1] <= 0)
                {
                    throw new ArgumentException(SR.Format(SR.InvalidByteRanges, "byteRanges"));
                }
                i++;
            }
        }
 
        /// <summary>
        /// Check if the given byte ranges follows correct format
        /// 1) array should consist pairs of offset and length
        /// 2) offset cannot be less than 0
        /// 3) length cannot be less than equal to 0
        /// </summary>
        /// <param name="byteRanges">Byte ranges to be checked</param>
        /// <returns>True if the byte ranges are in correct format</returns>
        static private void CheckTwoDimensionalByteRanges(int[,] byteRanges)
        {
            if (byteRanges.GetLength(0) <= 0 || byteRanges.GetLength(1) != 2)
            {
                throw new ArgumentException(SR.Format(SR.InvalidByteRanges, "byteRanges"));
            }
 
            for (int i = 0; i < byteRanges.GetLength(0); ++i)
            {
                if (byteRanges[i,Offset_Index] < 0 || byteRanges[i,Length_Index] <= 0)
                {
                    throw new ArgumentException(SR.Format(SR.InvalidByteRanges, "byteRanges"));
                }
            }
        }
 
        /// <summary>
        /// Check if the some of byte range request is satisfied by the given response
        /// This method make sure of the following:
        /// 1) It contains Content-Range field
        /// 2) Content-Range follows the format defined in RFC2616
        ///     a. it should be in the form of "bytes 0-499/1234" or "bytes 0-499/*"
        ///     b. it should not be "bytes */*" or "*/1234"
        ///     c. last-byte-pos must not be less than its first byte pos
        /// 3) Ignore the response that does not conform to condition #1 and #2
        /// </summary>
        /// <param name="responseHeaders">collection of headers returned with WebResponse</param>
        /// <param name="beginOffset">first byte pos the requested byte range</param>
        /// <param name="endOffset">last byte pos the requested byte range</param>
        /// <returns>False if the response contains invalid Content-Range field
        ///         or none of bytes requested is included in the response.
        ///         True if the some bytes of the requested bytes are included in the response.</returns>
        static private bool CheckContentRange(WebHeaderCollection responseHeaders, int beginOffset, ref int endOffset)
        {
            String contentRange = responseHeaders[ContentRangeHeader];
 
            // No Content-Range (condition #1)
            if (contentRange == null)
            {
                return false;
            }
 
            contentRange = contentRange.ToUpperInvariant();
 
            // No Content-Range (condition #1)
            if (contentRange.Length == 0
                    || !contentRange.StartsWith(ByteRangeUnit, StringComparison.Ordinal))
            {
                return false;
            }
 
            // ContentRange: BYTES XXX-YYY/ZZZ
            int index = contentRange.IndexOf('-');
 
            if (index == -1)
            {
                return false;
            }
 
            // Get the first byte offset of the range (XXX)
            int firstByteOffset = Int32.Parse(contentRange.AsSpan(ByteRangeUnit.Length,
                                                                        index - ByteRangeUnit.Length),
                                                NumberStyles.None, NumberFormatInfo.InvariantInfo);
 
            ReadOnlySpan<char> contentRangeSpan = contentRange.AsSpan(index + 1);
            // ContentRange: YYY/ZZZ
            index = contentRangeSpan.IndexOf('/');
 
            if (index == -1)
            {
                return false;
            }
 
            // Get the last byte offset of the range (YYY)
            int lastByteOffset = Int32.Parse(contentRangeSpan.Slice(0, index), NumberStyles.None, NumberFormatInfo.InvariantInfo);
 
            // Get the instance length
            // ContentRange: ZZZ
            contentRangeSpan = contentRangeSpan.Slice(index + 1);
            if (!contentRangeSpan.Equals("*", StringComparison.Ordinal))
            {
                // Note: for firstByteOffset and lastByteOffset, we are using Int32.Parse to make sure Int32.Parse to throw
                //  if it is not an integer or the integer is bigger than Int32 since HttpWebRequest.AddRange
                //  only supports Int32
                //  Once HttpWebRequest.AddRange start supporting Int64 we should change it to Int64 and long
                Int32.Parse(contentRangeSpan, NumberStyles.None, NumberFormatInfo.InvariantInfo);
            }
 
            // The response is considered to be successful if
            //  the last byte offset is greater than the first offset of the response
            //  and the response range satisfies some or all of the requested range
            //  However, we don't want to deal with the situation where the first byte of the response is not the beginning
            //  of the requested range
            bool successful = (firstByteOffset <= lastByteOffset
                                    && beginOffset == firstByteOffset);
 
            // Check if the response didn't satisfy the end part of the requested range
            if (successful && lastByteOffset < endOffset)
            {
                endOffset = lastByteOffset;
            }
 
            return successful;
        }
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Properties
        //
        //------------------------------------------------------
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        private bool _firstRequestMade;
        private bool _disposed;
        private Object _syncObject = new Object();
        private bool _erroredOut;
        private Exception _erroredOutException;
 
        private Uri _requestedUri;            // url to be downloaded
        private RequestCachePolicy _cachePolicy;
 
        private IWebProxy _proxy;
        private ICredentials _credentials;
        private CookieContainer _cookieContainer = new CookieContainer(1);
 
        private SafeWaitHandle _eventHandle;    // event handle which needs to be raised to inform the caller that
                                         //  the requested bytes are available
        private Mutex _fileMutex;       // object controlling synchronization on the temp file - if this is null, we own the stream
        private System.IO.Stream _tempFileStream;   // stream to write to
 
        private ArrayList _byteRangesAvailable = new ArrayList(2); // byte ranges that are downloaded
        private ArrayList _requestsOnWait;      // List of byte ranges requested need to be processed
        private int[,] _byteRangesInProgress;
 
        private HttpWebRequest _webRequest;
 
        private byte[] _buffer;             // Buffer used for writing out the downloaded bytes to temp file
 
        private const int WriteBufferSize = 4096; // Buffer size for writing out to the temp file
        private const int TimeOut = 5000;
        private const int Offset_Index = 0;
        private const int Length_Index = 1;
 
        private const String ByteRangeUnit = "BYTES ";
        private const String ContentRangeHeader = "Content-Range";
 
        #endregion Private Fields
    }
}