diff --git a/SabreTools.IO.Test/Streams/ViewStreamTests.cs b/SabreTools.IO.Test/Streams/ViewStreamTests.cs new file mode 100644 index 0000000..13f9953 --- /dev/null +++ b/SabreTools.IO.Test/Streams/ViewStreamTests.cs @@ -0,0 +1,378 @@ +using System; +using System.IO; +using SabreTools.IO.Streams; +using Xunit; + +namespace SabreTools.IO.Test.Streams +{ + public class ViewStreamTests + { + #region Constructor + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(1024, 0, 1024, 1024)] + [InlineData(1024, 256, 512, 512)] + public void Constructor_Array(int size, long offset, long length, long expectedLength) + { + byte[] data = new byte[size]; + var stream = new ViewStream(data, offset, length); + Assert.Equal(expectedLength, stream.Length); + Assert.Equal(0, stream.Position); + } + + [Theory] + [InlineData(0, -1, 0)] + [InlineData(0, 2048, 0)] + [InlineData(1024, -1, 1024)] + [InlineData(1024, 2048, 1024)] + [InlineData(1024, -1, 512)] + [InlineData(1024, 2048, 512)] + public void Constructor_Array_InvalidOffset(int size, long offset, long length) + { + byte[] data = new byte[size]; + Assert.Throws(() => _ = new ViewStream(data, offset, length)); + } + + [Theory] + [InlineData(0, 0, -1)] + [InlineData(0, 0, 2048)] + [InlineData(1024, 0, -1)] + [InlineData(1024, 0, 2048)] + [InlineData(1024, 256, -1)] + [InlineData(1024, 256, 2048)] + public void Constructor_Array_InvalidLength(int size, long offset, long length) + { + byte[] data = new byte[size]; + Assert.Throws(() => _ = new ViewStream(data, offset, length)); + } + + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(1024, 0, 1024, 1024)] + [InlineData(1024, 256, 512, 512)] + public void Constructor_Stream(int size, long offset, long length, long expectedLength) + { + Stream data = new MemoryStream(new byte[size]); + var stream = new ViewStream(data, offset, length); + Assert.Equal(expectedLength, stream.Length); + Assert.Equal(0, stream.Position); + } + + [Theory] + [InlineData(0, -1, 0)] + [InlineData(0, 2048, 0)] + [InlineData(1024, -1, 1024)] + [InlineData(1024, 2048, 1024)] + [InlineData(1024, -1, 512)] + [InlineData(1024, 2048, 512)] + public void Constructor_Stream_InvalidOffset(int size, long offset, long length) + { + Stream data = new MemoryStream(new byte[size]); + Assert.Throws(() => _ = new ViewStream(data, offset, length)); + } + + [Theory] + [InlineData(0, 0, -1)] + [InlineData(0, 0, 2048)] + [InlineData(1024, 0, -1)] + [InlineData(1024, 0, 2048)] + [InlineData(1024, 256, -1)] + [InlineData(1024, 256, 2048)] + public void Constructor_Stream_InvalidLength(int size, long offset, long length) + { + Stream data = new MemoryStream(new byte[size]); + Assert.Throws(() => _ = new ViewStream(data, offset, length)); + } + + #endregion + + #region Position + + [Theory] + [InlineData(0, 0, 0, -1, 0)] + [InlineData(0, 0, 0, 0, 0)] + [InlineData(0, 0, 0, 256, 0)] + [InlineData(0, 0, 0, 2048, 0)] + [InlineData(1024, 0, 1024, -1, 0)] + [InlineData(1024, 0, 1024, 0, 0)] + [InlineData(1024, 0, 1024, 256, 256)] + [InlineData(1024, 0, 1024, 2048, 1023)] + [InlineData(1024, 256, 512, -1, 0)] + [InlineData(1024, 256, 512, 0, 0)] + [InlineData(1024, 256, 512, 256, 256)] + [InlineData(1024, 256, 512, 2048, 511)] + public void Position_Array(int size, long offset, long length, long position, long expectedPosition) + { + byte[] data = new byte[size]; + var stream = new ViewStream(data, offset, length); + stream.Position = position; + Assert.Equal(expectedPosition, stream.Position); + } + + [Theory] + [InlineData(0, 0, 0, -1, 0)] + [InlineData(0, 0, 0, 0, 0)] + [InlineData(0, 0, 0, 256, 0)] + [InlineData(0, 0, 0, 2048, 0)] + [InlineData(1024, 0, 1024, -1, 0)] + [InlineData(1024, 0, 1024, 0, 0)] + [InlineData(1024, 0, 1024, 256, 256)] + [InlineData(1024, 0, 1024, 2048, 1023)] + [InlineData(1024, 256, 512, -1, 0)] + [InlineData(1024, 256, 512, 0, 0)] + [InlineData(1024, 256, 512, 256, 256)] + [InlineData(1024, 256, 512, 2048, 511)] + public void Position_Stream(int size, long offset, long length, long position, long expectedPosition) + { + Stream data = new MemoryStream(new byte[size]); + var stream = new ViewStream(data, offset, length); + stream.Position = position; + Assert.Equal(expectedPosition, stream.Position); + } + + #endregion + + #region SegmentValid + + [Theory] + [InlineData(0, 0, 0, -1, 0, false)] + [InlineData(0, 0, 0, 2048, 0, false)] + [InlineData(0, 0, 0, 0, 0, true)] + [InlineData(0, 0, 0, 0, -1, false)] + [InlineData(0, 0, 0, 0, 2048, false)] + [InlineData(1024, 0, 1024, -1, 0, false)] + [InlineData(1024, 0, 1024, 2048, 0, false)] + [InlineData(1024, 0, 1024, 0, 0, true)] + [InlineData(1024, 0, 1024, 256, 0, true)] + [InlineData(1024, 0, 1024, 256, 256, true)] + [InlineData(1024, 0, 1024, 0, -1, false)] + [InlineData(1024, 0, 1024, 0, 2048, false)] + [InlineData(1024, 256, 512, -1, 0, false)] + [InlineData(1024, 256, 512, 2048, 0, false)] + [InlineData(1024, 256, 512, 0, 0, true)] + [InlineData(1024, 256, 512, 256, 0, true)] + [InlineData(1024, 256, 512, 256, 256, true)] + [InlineData(1024, 256, 512, 0, -1, false)] + [InlineData(1024, 256, 512, 0, 2048, false)] + public void SegmentValid_Array(int size, long offset, long length, int segmentStart, int segmentLength, bool expected) + { + byte[] data = new byte[size]; + var stream = new ViewStream(data, offset, length); + bool actual = stream.SegmentValid(segmentStart, segmentLength); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(0, 0, 0, -1, 0, false)] + [InlineData(0, 0, 0, 2048, 0, false)] + [InlineData(0, 0, 0, 0, 0, true)] + [InlineData(0, 0, 0, 0, -1, false)] + [InlineData(0, 0, 0, 0, 2048, false)] + [InlineData(1024, 0, 1024, -1, 0, false)] + [InlineData(1024, 0, 1024, 2048, 0, false)] + [InlineData(1024, 0, 1024, 0, 0, true)] + [InlineData(1024, 0, 1024, 256, 0, true)] + [InlineData(1024, 0, 1024, 256, 256, true)] + [InlineData(1024, 0, 1024, 0, -1, false)] + [InlineData(1024, 0, 1024, 0, 2048, false)] + [InlineData(1024, 256, 512, -1, 0, false)] + [InlineData(1024, 256, 512, 2048, 0, false)] + [InlineData(1024, 256, 512, 0, 0, true)] + [InlineData(1024, 256, 512, 256, 0, true)] + [InlineData(1024, 256, 512, 256, 256, true)] + [InlineData(1024, 256, 512, 0, -1, false)] + [InlineData(1024, 256, 512, 0, 2048, false)] + public void SegmentValid_Stream(int size, long offset, long length, int segmentStart, int segmentLength, bool expected) + { + Stream data = new MemoryStream(new byte[size]); + var stream = new ViewStream(data, offset, length); + bool actual = stream.SegmentValid(segmentStart, segmentLength); + Assert.Equal(expected, actual); + } + + #endregion + + #region Read + + [Theory] + [InlineData(0, 0, 0, -1, 0)] + [InlineData(0, 0, 0, 0, 0)] + [InlineData(0, 0, 0, 2048, 0)] + [InlineData(1024, 0, 1024, -1, 0)] + [InlineData(1024, 0, 1024, 0, 0)] + [InlineData(1024, 0, 1024, 256, 256)] + [InlineData(1024, 0, 1024, 1024, 1024)] + [InlineData(1024, 0, 1024, 2048, 0)] + [InlineData(1024, 256, 512, -1, 0)] + [InlineData(1024, 256, 512, 0, 0)] + [InlineData(1024, 256, 512, 256, 256)] + [InlineData(1024, 256, 512, 512, 512)] + [InlineData(1024, 256, 512, 2048, 0)] + public void Read_Array(int size, long offset, long length, int count, int expectedRead) + { + byte[] data = new byte[size]; + var stream = new ViewStream(data, offset, length); + + byte[] buffer = new byte[1024]; + int actual = stream.Read(buffer, 0, count); + Assert.Equal(expectedRead, actual); + } + + [Theory] + [InlineData(0, 0, 0, -1, 0)] + [InlineData(0, 0, 0, 0, 0)] + [InlineData(0, 0, 0, 2048, 0)] + [InlineData(1024, 0, 1024, -1, 0)] + [InlineData(1024, 0, 1024, 0, 0)] + [InlineData(1024, 0, 1024, 256, 256)] + [InlineData(1024, 0, 1024, 1024, 1024)] + [InlineData(1024, 0, 1024, 2048, 0)] + [InlineData(1024, 256, 512, -1, 0)] + [InlineData(1024, 256, 512, 0, 0)] + [InlineData(1024, 256, 512, 256, 256)] + [InlineData(1024, 256, 512, 512, 512)] + [InlineData(1024, 256, 512, 2048, 0)] + public void Read_Stream(int size, long offset, long length, int count, int expectedRead) + { + Stream data = new MemoryStream(new byte[size]); + var stream = new ViewStream(data, offset, length); + + byte[] buffer = new byte[1024]; + int actual = stream.Read(buffer, 0, count); + Assert.Equal(expectedRead, actual); + } + + #endregion + + #region Seek + + [Theory] + [InlineData(0, 0, 0, -1, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, -1, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, -1, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.Begin, 0)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.End, 1022)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.Begin, 0)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.Begin, 256)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.Current, 256)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.Begin, 1023)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.Current, 1023)] + [InlineData(1024, 256, 512, -1, SeekOrigin.Begin, 0)] + [InlineData(1024, 256, 512, -1, SeekOrigin.End, 510)] + [InlineData(1024, 256, 512, -1, SeekOrigin.Current, 0)] + [InlineData(1024, 256, 512, 0, SeekOrigin.Begin, 0)] + [InlineData(1024, 256, 512, 0, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 0, SeekOrigin.Current, 0)] + [InlineData(1024, 256, 512, 256, SeekOrigin.Begin, 256)] + [InlineData(1024, 256, 512, 256, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 256, SeekOrigin.Current, 256)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.Begin, 511)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.Current, 511)] + public void Seek_Array(int size, long offset, long length, long position, SeekOrigin seekOrigin, long expectedPosition) + { + byte[] data = new byte[size]; + var stream = new ViewStream(data, offset, length); + stream.Seek(position, seekOrigin); + Assert.Equal(expectedPosition, stream.Position); + } + + [Theory] + [InlineData(0, 0, 0, -1, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, -1, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, -1, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 0, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 256, SeekOrigin.Current, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.Begin, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.End, 0)] + [InlineData(0, 0, 0, 2048, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.Begin, 0)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.End, 1022)] + [InlineData(1024, 0, 1024, -1, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.Begin, 0)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 0, SeekOrigin.Current, 0)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.Begin, 256)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 256, SeekOrigin.Current, 256)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.Begin, 1023)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.End, 1023)] + [InlineData(1024, 0, 1024, 2048, SeekOrigin.Current, 1023)] + [InlineData(1024, 256, 512, -1, SeekOrigin.Begin, 0)] + [InlineData(1024, 256, 512, -1, SeekOrigin.End, 510)] + [InlineData(1024, 256, 512, -1, SeekOrigin.Current, 0)] + [InlineData(1024, 256, 512, 0, SeekOrigin.Begin, 0)] + [InlineData(1024, 256, 512, 0, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 0, SeekOrigin.Current, 0)] + [InlineData(1024, 256, 512, 256, SeekOrigin.Begin, 256)] + [InlineData(1024, 256, 512, 256, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 256, SeekOrigin.Current, 256)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.Begin, 511)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.End, 511)] + [InlineData(1024, 256, 512, 2048, SeekOrigin.Current, 511)] + public void Seek_Stream(int size, long offset, long length, long position, SeekOrigin seekOrigin, long expectedPosition) + { + Stream data = new MemoryStream(new byte[size]); + var stream = new ViewStream(data, offset, length); + stream.Seek(position, seekOrigin); + Assert.Equal(expectedPosition, stream.Position); + } + + #endregion + + #region Unimplemented + + [Fact] + public void SetLength_Array_Throws() + { + byte[] data = new byte[1024]; + var stream = new ViewStream(data, 0, 1024); + Assert.Throws(() => stream.SetLength(0)); + } + + [Fact] + public void SetLength_Stream_Throws() + { + Stream data = new MemoryStream(new byte[1024]); + var stream = new ViewStream(data, 0, 1024); + Assert.Throws(() => stream.SetLength(0)); + } + + [Fact] + public void Write_Array_Throws() + { + byte[] data = new byte[1024]; + var stream = new ViewStream(data, 0, 1024); + Assert.Throws(() => stream.Write([], 0, 0)); + } + + [Fact] + public void Write_Stream_Throws() + { + Stream data = new MemoryStream(new byte[1024]); + var stream = new ViewStream(data, 0, 1024); + Assert.Throws(() => stream.Write([], 0, 0)); + } + + #endregion + } +} \ No newline at end of file diff --git a/SabreTools.IO/Streams/ViewStream.cs b/SabreTools.IO/Streams/ViewStream.cs new file mode 100644 index 0000000..790f355 --- /dev/null +++ b/SabreTools.IO/Streams/ViewStream.cs @@ -0,0 +1,221 @@ +using System; +using System.IO; + +namespace SabreTools.IO.Streams +{ + /// + /// Stream representing a view into a source + /// + public class ViewStream : Stream + { + #region Properties + + /// + /// Filename from the source, if possible + /// + public string? Filename + { + get + { + // Only file streams can have a filename + if (_source is not FileStream fs) + return null; + + // Return the name + return fs.Name; + } + } + + /// + public override long Length => _length; + + /// + public override long Position + { + get + { + // Handle 0-length sources + if (_length <= 0) + return 0; + + return _source.Position - _initialPosition; + } + set + { + // Handle 0-length sources + if (_length <= 0) + { + _source.Position = 0; + return; + } + + long position = value; + + // Handle out-of-bounds seeks + if (position < 0) + position = 0; + else if (position >= _length) + position = _length - 1; + + _source.Position = _initialPosition + position; + } + } + + #endregion + + #region Instance Variables + + /// + /// Initial position within the underlying data + /// + protected long _initialPosition; + + /// + /// Usable length in the underlying data + /// + protected long _length; + + /// + /// Source data + /// + protected Stream _source; + + /// + /// Lock object for reading from the source + /// + private readonly object _sourceLock = new(); + + #endregion + + #region Constructors + + /// + /// Construct a new ViewStream from a Stream + /// + public ViewStream(Stream data, long offset, long length) + { + if (!data.CanRead) + throw new ArgumentException(nameof(data)); + if (offset < 0 || offset > data.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (length < 0 || offset + length > data.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + _source = data; + _initialPosition = offset; + _length = length; + + _source.Seek(_initialPosition, SeekOrigin.Begin); + } + + /// + /// Construct a new ViewStream from a byte array + /// + public ViewStream(byte[] data, long offset, long length) + { + if (offset < 0 || offset > data.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (length < 0 || offset + length > data.Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + _source = new MemoryStream(data, (int)offset, (int)length); + _initialPosition = 0; + _length = length; + + _source.Seek(_initialPosition, SeekOrigin.Begin); + } + + #endregion + + #region Data + + /// + /// Check if a data segment is valid in the data source + /// + /// Position in the source + /// Length of the data to check + /// True if the positional data is valid, false otherwise + public bool SegmentValid(long offset, long count) + { + if (offset < 0 || offset > Length) + return false; + if (count < 0 || offset + count > Length) + return false; + + return true; + } + + #endregion + + #region Stream Implementations + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + + /// + public override bool CanSeek => _source.CanSeek; + + /// + public override void Flush() => _source.Flush(); + + /// + public override int Read(byte[] buffer, int offset, int count) + { + // Invalid cases always return 0 + if (buffer.Length == 0) + return 0; + if (offset < 0 || offset > buffer.Length) + return 0; + if (count < 0 || offset + count > buffer.Length) + return 0; + if (!SegmentValid(Position, count)) + return 0; + + // Short-circuit 0-byte reads + if (count == 0) + return 0; + + try + { + lock (_sourceLock) + { + return _source.Read(buffer, offset, count); + } + + } + catch + { + // Absorb the error + return 0; + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + // Handle the "seek" + switch (origin) + { + case SeekOrigin.Begin: Position = offset; break; + case SeekOrigin.Current: Position += offset; break; + case SeekOrigin.End: Position = _length + offset - 1; break; + default: throw new ArgumentException($"Invalid value for {nameof(origin)}"); + } + + return Position; + } + + /// + public override void SetLength(long value) + => throw new NotImplementedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) + => throw new NotImplementedException(); + + #endregion + } +} \ No newline at end of file