From cd01e170fe87fdffcfa0a25592aa7497de918073 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 16 Apr 2024 20:13:27 -0400 Subject: [PATCH] Port ReadOnlyBitStream from Compression, add tests --- SabreTools.IO.Test/ReadOnlyBitStreamTests.cs | 54 +++++ SabreTools.IO/Streams/ReadOnlyBitStream.cs | 238 +++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 SabreTools.IO.Test/ReadOnlyBitStreamTests.cs create mode 100644 SabreTools.IO/Streams/ReadOnlyBitStream.cs diff --git a/SabreTools.IO.Test/ReadOnlyBitStreamTests.cs b/SabreTools.IO.Test/ReadOnlyBitStreamTests.cs new file mode 100644 index 0000000..5301c7e --- /dev/null +++ b/SabreTools.IO.Test/ReadOnlyBitStreamTests.cs @@ -0,0 +1,54 @@ +using System.IO; +using SabreTools.IO.Streams; +using Xunit; + +namespace SabreTools.IO.Test +{ + public class ReadOnlyBitStreamTests + { + [Fact] + public void DefaultConstructorTest() + { + var stream = new ReadOnlyBitStream(new MemoryStream()); + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + + stream = new ReadOnlyBitStream(new MemoryStream(new byte[16])); + Assert.Equal(16, stream.Length); + Assert.Equal(0, stream.Position); + } + + [Fact] + public void ReadSingleBitTest() + { + byte[] data = [0b01010101]; + var stream = new ReadOnlyBitStream(new MemoryStream(data)); + byte? bit = stream.ReadBit(); + Assert.NotNull(bit); + Assert.Equal((byte)0b00000001, bit); + Assert.Equal(1, stream.Position); + } + + [Fact] + public void ReadBitsLSBTest() + { + byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101]; + var stream = new ReadOnlyBitStream(new MemoryStream(data)); + uint? bits = stream.ReadBitsLSB(4); + Assert.NotNull(bits); + Assert.Equal((byte)0b00000101, bits); + Assert.Equal(1, stream.Position); + } + + [Fact] + public void ReadBitsMSBTest() + { + byte[] data = [0b01010101, 0b01010101, 0b01010101, 0b01010101]; + var stream = new ReadOnlyBitStream(new MemoryStream(data)); + uint? bits = stream.ReadBitsMSB(4); + Assert.NotNull(bits); + Assert.Equal((byte)0b00001010, bits); + Assert.Equal(1, stream.Position); + } + } +} \ No newline at end of file diff --git a/SabreTools.IO/Streams/ReadOnlyBitStream.cs b/SabreTools.IO/Streams/ReadOnlyBitStream.cs new file mode 100644 index 0000000..54e084b --- /dev/null +++ b/SabreTools.IO/Streams/ReadOnlyBitStream.cs @@ -0,0 +1,238 @@ +using System; +using System.IO; + +namespace SabreTools.IO.Streams +{ + /// + /// Wrapper to allow reading bits from a source stream + /// + public class ReadOnlyBitStream + { + /// + public long Position => _source.Position; + + /// + public long Length => _source.Length; + + /// + /// Original stream source + /// + private readonly Stream _source; + + /// + /// Last read byte value from the stream + /// + private byte? _bitBuffer; + + /// + /// Index in the byte of the current bit + /// + private int _bitIndex; + + /// + /// Create a new BitStream from a source Stream + /// + public ReadOnlyBitStream(Stream source) + { + _source = source; + _bitBuffer = null; + _bitIndex = 0; + + // Verify the stream + if (!source.CanRead || !source.CanSeek) + throw new ArgumentException($"{nameof(source)} needs to be readable and seekable"); + } + + /// + /// Discard the current cached byte + /// + public void Discard() + { + _bitBuffer = null; + _bitIndex = 0; + } + + /// + /// Read a single bit, if possible + /// + /// The next bit encoded in a byte, null on error or end of stream + public byte? ReadBit() + { + // If we reached the end of the stream + if (_source.Position >= _source.Length) + return null; + + // If we don't have a value cached + if (_bitBuffer == null) + { + // Read the next byte, if possible + _bitBuffer = ReadSourceByte(); + if (_bitBuffer == null) + return null; + + // Reset the bit index + _bitIndex = 0; + } + + // Get the value by bit-shifting + int value = _bitBuffer.Value & 0x01; + _bitBuffer = (byte?)(_bitBuffer >> 1); + _bitIndex++; + + // Reset the byte if we're at the end + if (_bitIndex >= 8) + Discard(); + + return (byte)value; + } + + /// + /// Read a multiple bits in LSB, if possible + /// + /// The next bits encoded in a UInt32, null on error or end of stream + public uint? ReadBitsLSB(int bits) + { + uint value = 0; + for (int i = 0; i < bits; i++) + { + // Read the next bit + byte? bitValue = ReadBit(); + if (bitValue == null) + return null; + + // Add the bit shifted by the current index + value += (uint)(bitValue.Value << i); + } + + return value; + } + + /// + /// Read a multiple bits in MSB, if possible + /// + /// The next bits encoded in a UInt32, null on error or end of stream + public uint? ReadBitsMSB(int bits) + { + uint value = 0; + for (int i = 0; i < bits; i++) + { + // Read the next bit + byte? bitValue = ReadBit(); + if (bitValue == null) + return null; + + // Add the bit shifted by the current index + value = (value << 1) + bitValue.Value; + } + + return value; + } + + /// + /// Read a byte, if possible + /// + /// The next byte, null on error or end of stream + /// Assumes the stream is byte-aligned + public byte? ReadByte() + { + try + { + Discard(); + return _source.ReadByteValue(); + } + catch + { + return null; + } + } + + /// + /// Read a UInt16, if possible + /// + /// The next UInt16, null on error or end of stream + /// Assumes the stream is byte-aligned + public ushort? ReadUInt16() + { + try + { + Discard(); + return _source.ReadUInt16(); + } + catch + { + return null; + } + } + + /// + /// Read a UInt32, if possible + /// + /// The next UInt32, null on error or end of stream + /// Assumes the stream is byte-aligned + public uint? ReadUInt32() + { + try + { + Discard(); + return _source.ReadUInt32(); + } + catch + { + return null; + } + } + + /// + /// Read a UInt64, if possible + /// + /// The next UInt64, null on error or end of stream + /// Assumes the stream is byte-aligned + public ulong? ReadUInt64() + { + try + { + Discard(); + return _source.ReadUInt64(); + } + catch + { + return null; + } + } + + /// + /// Read bytes, if possible + /// + /// Number of bytes to read + /// The next bytes, null on error or end of stream + /// Assumes the stream is byte-aligned + public byte[]? ReadBytes(int bytes) + { + try + { + Discard(); + return _source.ReadBytes(bytes); + } + catch + { + return null; + } + } + + /// + /// Read a single byte from the underlying stream, if possible + /// + /// The next full byte from the stream, null on error or end of stream + private byte? ReadSourceByte() + { + try + { + return _source.ReadByteValue(); + } + catch + { + return null; + } + } + } +} \ No newline at end of file