File: Signing\Archive\ReadOnlyBufferedStream.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace NuGet.Packaging.Signing
{
    public sealed class ReadOnlyBufferedStream : Stream
    {
        private const int _defaultBufferSize = 4096;

        private readonly byte[] _buffer;
        private readonly bool _leaveOpen;
        private readonly Lazy<long> _length;
        private readonly Stream _stream;

        private long _bufferStartPosition;
        private int _bufferFillLength;
        private bool _isDisposed;
        private long _position;

        public override bool CanRead => _stream.CanRead;
        public override bool CanSeek => _stream.CanSeek;
        public override bool CanTimeout => _stream.CanTimeout;
        public override bool CanWrite => false;
        public override long Length => _length.Value;

        public override long Position
        {
            get
            {
                ThrowIfDisposed();

                return _position;
            }
            set
            {
                ThrowIfDisposed();

                // For stream implementations that support seeking, the Stream contract (per MSDN) is that
                // "[s]eeking to any location beyond the length of the stream is supported."
                // So, Position > Length is legal.
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }

                _position = value;
            }
        }

        public override int ReadTimeout
        {
            get
            {
                ThrowIfDisposed();

                return _stream.ReadTimeout;
            }
            set
            {
                ThrowIfDisposed();

                _stream.ReadTimeout = value;
            }
        }

        public override int WriteTimeout
        {
            get
            {
                ThrowIfDisposed();

                return _stream.WriteTimeout;
            }
            set
            {
                ThrowIfDisposed();

                _stream.WriteTimeout = value;
            }
        }

        public ReadOnlyBufferedStream(Stream stream, bool leaveOpen) :
            this(stream, leaveOpen, _defaultBufferSize)
        {
        }

        public ReadOnlyBufferedStream(Stream stream, bool leaveOpen, int bufferSize)
        {
            if (stream == null)
            {
                throw new ArgumentNullException(nameof(stream));
            }

            if (!stream.CanRead)
            {
                throw new ArgumentException(Strings.StreamMustBeReadable, nameof(stream));
            }

            if (!stream.CanSeek)
            {
                throw new ArgumentException(Strings.StreamMustBeSeekable, nameof(stream));
            }

            if (bufferSize <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(bufferSize));
            }

            _buffer = new byte[bufferSize];
            _stream = stream;
            _leaveOpen = leaveOpen;

            // Repeated calls to the Length property were found to be costly.
            _length = new Lazy<long>(() => _stream.Length);
        }

        public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();

            return base.CopyToAsync(destination, bufferSize, cancellationToken);
        }

        public override void Flush()
        {
            ThrowIfDisposed();

            // This is a read-only stream.
            throw new NotSupportedException();
        }

        public override Task FlushAsync(CancellationToken cancellationToken)
        {
            ThrowIfDisposed();

            // This is a read-only stream.
            throw new NotSupportedException();
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            ThrowIfDisposed();

            if (buffer == null)
            {
                throw new ArgumentNullException(nameof(buffer));
            }

            if (offset < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(offset));
            }

            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count));
            }

            if (count > buffer.Length - offset)
            {
                throw new ArgumentException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.RangeOutOfBoundsForArray,
                        nameof(offset),
                        nameof(count)));
            }

            var bytesRead = 0;
            var destinationOffset = offset;
            var bytesWanted = count;

            while (bytesWanted > 0 && !IsPositionAfterEndOfStream())
            {
                int sourceStart;
                int bytesToCopy;

                if (IsPositionInBuffer())
                {
                    sourceStart = (int)(Position - _bufferStartPosition);
                }
                else
                {
                    FillBuffer();

                    sourceStart = 0;
                }

                bytesToCopy = (int)Math.Min(_bufferStartPosition + _bufferFillLength - Position, bytesWanted);

                Debug.Assert(bytesToCopy >= 0);

                if (bytesToCopy <= 0)
                {
                    break;
                }

                Buffer.BlockCopy(_buffer, sourceStart, buffer, destinationOffset, bytesToCopy);

                destinationOffset += bytesToCopy;
                bytesRead += bytesToCopy;
                bytesWanted -= bytesToCopy;

                Position += bytesToCopy;
            }

            return bytesRead;
        }

        public override int ReadByte()
        {
            ThrowIfDisposed();

            int offset;

            if (IsPositionInBuffer())
            {
                offset = (int)(Position - _bufferStartPosition);
            }
            else if (IsPositionAfterEndOfStream())
            {
                return -1;
            }
            else
            {
                FillBuffer();

                offset = 0;
            }

            var value = _buffer[offset];

            ++Position;

            return value;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            ThrowIfDisposed();

            long newOffset;

            switch (origin)
            {
                case SeekOrigin.Current:
                    newOffset = Position + offset;
                    break;

                case SeekOrigin.Begin:
                    newOffset = offset;
                    break;

                case SeekOrigin.End:
                    newOffset = Length + offset;
                    break;

                default:
                    throw new ArgumentOutOfRangeException(nameof(origin));
            }

            if (newOffset < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(offset));
            }

            Position = _stream.Seek(newOffset, SeekOrigin.Begin);

            return Position;
        }

        public override void SetLength(long value)
        {
            ThrowIfDisposed();

            throw new NotSupportedException();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            ThrowIfDisposed();

            throw new NotSupportedException();
        }

        public override void WriteByte(byte value)
        {
            ThrowIfDisposed();

            throw new NotSupportedException();
        }

        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            ThrowIfDisposed();

            throw new NotSupportedException();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!_isDisposed)
                {
                    if (!_leaveOpen)
                    {
                        _stream.Dispose();
                    }

                    _isDisposed = true;
                }
            }

            base.Dispose(disposing);
        }

        private void FillBuffer()
        {
            _bufferStartPosition = Position;
            _bufferFillLength = 0;

            _stream.Position = _bufferStartPosition;

            var totalBytesRead = 0;
            var bytesRead = 0;
            var offset = 0;
            var count = _buffer.Length;

            // Read(...) does not guarantee that the requested number of bytes will be read, even if there are ample
            // bytes in the source.  From MSDN:
            //
            //     An implementation is free to return fewer bytes than requested even if the end of the stream has
            //     not been reached.
            do
            {
                bytesRead = _stream.Read(_buffer, offset, count);

                offset += bytesRead;
                count -= bytesRead;
                totalBytesRead += bytesRead;
            } while (bytesRead > 0 && count > 0);

            _bufferFillLength = totalBytesRead;
        }

        private bool IsPositionAfterEndOfStream()
        {
            return Position >= Length;
        }

        private bool IsPositionInBuffer()
        {
            return _bufferStartPosition <= Position && Position < _bufferStartPosition + _bufferFillLength;
        }

        private void ThrowIfDisposed()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(nameof(ReadOnlyBufferedStream));
            }
        }
    }
}