From 3e42008af464fb5f8d615dab7ce920365633a222 Mon Sep 17 00:00:00 2001 From: GGG Date: Mon, 15 Aug 2022 11:33:41 -0300 Subject: [PATCH] Add IBinaryReader and a default implementation. This is required because Stream does not have EndOfStream and its read methods do not have ReadExact in any TFMs lower than .NET 7. --- Tsu.BinaryParser/src/BinaryReader.cs | 288 ++++++++++++++++++++++++++ Tsu.BinaryParser/src/IBinaryReader.cs | 110 ++++++++++ 2 files changed, 398 insertions(+) create mode 100644 Tsu.BinaryParser/src/BinaryReader.cs create mode 100644 Tsu.BinaryParser/src/IBinaryReader.cs diff --git a/Tsu.BinaryParser/src/BinaryReader.cs b/Tsu.BinaryParser/src/BinaryReader.cs new file mode 100644 index 0000000..a306f59 --- /dev/null +++ b/Tsu.BinaryParser/src/BinaryReader.cs @@ -0,0 +1,288 @@ +// Copyright © 2021 GGG KILLER +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +// the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Tsu.BinaryParser; + +/// +/// Our default implementation of . +/// +// This was heavily inspired by .NET's StreamReader. +public sealed class BinaryReader : IBinaryReader +{ + /// + /// The default value for the bufferSize parameter in this class' constructors. + /// + public const int DefaultBufferSize = 4096; + + /// + /// The default value for the leaveOpen parameter in this class' constructors. + /// + public const bool DefaultLeaveOpen = false; + + private readonly bool _leaveOpen; + private Stream? _stream; + private byte[]? _buffer; + private bool _disposedValue; + private int _bufferPos, _bufferLen; + + /// + /// Initializes a new with the + /// and leaveOpen set to . + /// + /// + /// + /// + public BinaryReader(Stream stream) : this(stream, DefaultBufferSize, DefaultLeaveOpen) + { + } + + /// + /// Initializes a new with the provided buffer size and leaveOpen + /// set to . + /// + /// + /// + /// + /// + /// + /// + public BinaryReader(Stream stream, int bufferSize) : this(stream, bufferSize, DefaultLeaveOpen) + { + } + + /// + /// Initializes a new with the provided buffer size and leaveOpen. + /// + /// + /// The stream to read data from. + /// + /// + /// The size of the buffer to buffer data into. + /// + /// + /// Whether to leave the stream open after disposing. + /// + public BinaryReader(Stream stream, int bufferSize, bool leaveOpen) + { + _stream = stream; + _buffer = new byte[bufferSize]; + _leaveOpen = leaveOpen; + } + + /// + public bool EndOfStream + { + get + { + ThrowIfDisposed(); + + if (_bufferLen < _bufferPos) + return false; + + var read = FillBuffer(); + return read == 0; + } + } + + /// + public Option PeekByte() + { + ThrowIfDisposed(); + + if (_bufferPos == _bufferLen && FillBuffer() == 0) + return Option.None(); + + return _buffer[_bufferPos]; + } + + /// + public Option ReadByte() + { + ThrowIfDisposed(); + + if (_bufferPos == _bufferLen && FillBuffer() == 0) + return Option.None(); + + var value = _buffer[_bufferPos]; + _bufferPos++; + return value; + } + + /// + public void Read(byte[] buffer, int bytesToRead) + { + ThrowIfDisposed(); + + var bytesLeft = bytesToRead; + while (bytesLeft > 0) + { + if (_bufferPos == _bufferLen && FillBuffer() == 0) + throw new EndOfStreamException("Unable to read the requested amount of bytes from the stream."); + + var bytesToCopy = Math.Min(_bufferLen - _bufferPos, bytesLeft); + Buffer.BlockCopy(_buffer, _bufferPos, buffer, bytesLeft - bytesToRead, bytesToCopy); + bytesLeft -= bytesToCopy; + _bufferPos += bytesToCopy; + } + } + + /// + public void Read(Span buffer) + { + ThrowIfDisposed(); + + var bytesLeft = buffer.Length; + while (bytesLeft > 0) + { + if (_bufferPos == _bufferLen && FillBuffer() == 0) + throw new EndOfStreamException("Unable to read the requested amount of bytes from the stream."); + + var bytesToCopy = Math.Min(_bufferLen - _bufferPos, bytesLeft); + var internalBuffer = new Span(_buffer, _bufferPos, bytesToCopy); + internalBuffer.CopyTo(buffer.Slice(bytesLeft - buffer.Length, bytesToCopy)); + bytesLeft -= bytesToCopy; + _bufferPos += bytesToCopy; + } + } + + /// + public ValueTask ReadAsync(byte[] buffer, int bytesToRead, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + return Core(buffer, bytesToRead, cancellationToken); + + async ValueTask Core(byte[] buffer, int bytesToRead, CancellationToken cancellationToken) + { + var bytesLeft = bytesToRead; + while (bytesLeft > 0) + { + if (_bufferPos == _bufferLen && await FillBufferAsync(cancellationToken) == 0) + throw new EndOfStreamException("Unable to read the requested amount of bytes from the stream."); + + var bytesToCopy = Math.Min(_bufferLen - _bufferPos, bytesLeft); + Buffer.BlockCopy(_buffer, _bufferPos, buffer, bytesLeft - bytesToRead, bytesToCopy); + bytesLeft -= bytesToCopy; + _bufferPos += bytesToCopy; + } + } + } + + /// + public ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + return Core(buffer, cancellationToken); + + async ValueTask Core(Memory buffer, CancellationToken cancellationToken) + { + var bytesLeft = buffer.Length; + while (bytesLeft > 0) + { + if (_bufferPos == _bufferLen && await FillBufferAsync(cancellationToken) == 0) + throw new EndOfStreamException("Unable to read the requested amount of bytes from the stream."); + + var bytesToCopy = Math.Min(_bufferLen - _bufferPos, bytesLeft); + _buffer.AsSpan(_bufferPos, bytesToCopy) + .CopyTo(buffer.Slice(bytesLeft - buffer.Length, bytesToCopy).Span); + bytesLeft -= bytesToCopy; + _bufferPos += bytesToCopy; + } + } + } + + [MemberNotNull(nameof(_stream), nameof(_buffer))] + private void ThrowIfDisposed() + { + if (_disposedValue) + throw new InvalidOperationException("Object has been disposed."); + InternalDebug.AssertNotNull(_stream); + InternalDebug.AssertNotNull(_buffer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FillBuffer() + { + ThrowIfDisposed(); + + _bufferPos = 0; + _bufferLen = _stream.Read(_buffer, 0, _buffer.Length); + + return _bufferLen; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Task FillBufferAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + + return Core(cancellationToken); + + async Task Core(CancellationToken cancellationToken) + { + _bufferPos = 0; + _bufferLen = await _stream.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + + return _bufferLen; + } + } + + #region IDisposable + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && !_leaveOpen) + _stream!.Dispose(); + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _buffer = null; + _stream = null; + _disposedValue = true; + } + } + + /// + /// Finalizes this object. + /// + ~BinaryReader() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable +} \ No newline at end of file diff --git a/Tsu.BinaryParser/src/IBinaryReader.cs b/Tsu.BinaryParser/src/IBinaryReader.cs new file mode 100644 index 0000000..71dc3e3 --- /dev/null +++ b/Tsu.BinaryParser/src/IBinaryReader.cs @@ -0,0 +1,110 @@ +// Copyright © 2021 GGG KILLER +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom +// the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Tsu.BinaryParser; + +/// +/// A reader for binary data. +/// +public interface IBinaryReader : IDisposable +{ + /// + /// Whether we've hit the end of the stream. + /// + bool EndOfStream { get; } + + /// + /// Attempts to peek a single byte from the stream. + /// + Option PeekByte(); + + /// + /// Attempts to read a single byte from the stream. + /// + Option ReadByte(); + + /// + /// Reads the specified amount of bytes from the stream. + /// + /// + /// The buffer to read the requested amount of bytes into. + /// + /// + /// The amount of bytes + /// + /// + /// Thrown when is larger than the length + /// of . + /// + /// + /// Thrown when the end of the stream is hit. + /// + void Read(byte[] buffer, int bytesToRead); + + /// + /// Reads the amount of bytes the span can hold into the provided buffer. + /// + /// + /// The buffer to read the bytes into. + /// + /// + /// Thrown when the end of the stream is hit. + /// + void Read(Span buffer); + + /// + /// Reads the specified amount of bytes from the stream asynchronously if + /// not enough data has been buffered. + /// + /// + /// The buffer to read the requested amount of bytes into. + /// + /// + /// The amount of bytes + /// + /// + /// The token to cancel the operation. + /// + /// + /// Thrown when is larger than the length + /// of . + /// + /// + /// Thrown when the end of the stream is hit. + /// + ValueTask ReadAsync(byte[] buffer, int bytesToRead, CancellationToken cancellationToken); + + /// + /// Reads the amount of bytes the memory can hold into the provided buffer + /// asynchronously if not enough data has been buffered. + /// + /// + /// The buffer to read the bytes into. + /// + /// + /// The token to cancel the operation. + /// + /// + /// Thrown when the end of the stream is hit. + /// + ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken); +}