Add view stream type

This commit is contained in:
Matt Nadareski
2025-08-23 21:07:57 -04:00
parent 8761629828
commit d0392be2d8
2 changed files with 599 additions and 0 deletions

View File

@@ -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<ArgumentOutOfRangeException>(() => _ = 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<ArgumentOutOfRangeException>(() => _ = 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<ArgumentOutOfRangeException>(() => _ = 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<ArgumentOutOfRangeException>(() => _ = 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<NotImplementedException>(() => 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<NotImplementedException>(() => stream.SetLength(0));
}
[Fact]
public void Write_Array_Throws()
{
byte[] data = new byte[1024];
var stream = new ViewStream(data, 0, 1024);
Assert.Throws<NotImplementedException>(() => 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<NotImplementedException>(() => stream.Write([], 0, 0));
}
#endregion
}
}

View File

@@ -0,0 +1,221 @@
using System;
using System.IO;
namespace SabreTools.IO.Streams
{
/// <summary>
/// Stream representing a view into a source
/// </summary>
public class ViewStream : Stream
{
#region Properties
/// <summary>
/// Filename from the source, if possible
/// </summary>
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;
}
}
/// <inheritdoc/>
public override long Length => _length;
/// <inheritdoc/>
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
/// <summary>
/// Initial position within the underlying data
/// </summary>
protected long _initialPosition;
/// <summary>
/// Usable length in the underlying data
/// </summary>
protected long _length;
/// <summary>
/// Source data
/// </summary>
protected Stream _source;
/// <summary>
/// Lock object for reading from the source
/// </summary>
private readonly object _sourceLock = new();
#endregion
#region Constructors
/// <summary>
/// Construct a new ViewStream from a Stream
/// </summary>
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);
}
/// <summary>
/// Construct a new ViewStream from a byte array
/// </summary>
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
/// <summary>
/// Check if a data segment is valid in the data source
/// </summary>
/// <param name="offset">Position in the source</param>
/// <param name="count">Length of the data to check</param>
/// <returns>True if the positional data is valid, false otherwise</returns>
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
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override bool CanSeek => _source.CanSeek;
/// <inheritdoc/>
public override void Flush() => _source.Flush();
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override void SetLength(long value)
=> throw new NotImplementedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotImplementedException();
#endregion
}
}