Add ReadOnlyCompositeStream

This commit is contained in:
Matt Nadareski
2024-04-16 10:46:41 -04:00
parent 8e3293dd7d
commit 4097a8cc8c
3 changed files with 412 additions and 22 deletions

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace SabreTools.IO.Test
{
public class ReadOnlyCompositeStreamTests
{
[Fact]
public void DefaultConstructorTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyArrayConstructorTest()
{
Stream[] arr = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyEnumerableConstructorTest()
{
// Empty enumerable constructor
List<Stream> list = [new MemoryStream()];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledArrayConstructorTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void FilledEnumerableConstructorTest()
{
List<Stream> list = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(list);
Assert.Equal(2048, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void AddStreamTest()
{
var stream = new ReadOnlyCompositeStream();
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);
stream.AddStream(new MemoryStream(new byte[1024]));
Assert.Equal(1024, stream.Length);
Assert.Equal(0, stream.Position);
}
[Fact]
public void EmptyStreamReadTest()
{
var stream = new ReadOnlyCompositeStream();
byte[] buf = new byte[512];
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buf, 0, 512));
}
[Fact]
public void SingleStreamReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamSingleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[1024]), new MemoryStream(new byte[1024])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void MultipleStreamMultipleContainedReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256]), new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(512, read);
}
[Fact]
public void SingleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[256])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
[Fact]
public void MultipleStreamExtraReadTest()
{
Stream[] arr = [new MemoryStream(new byte[128]), new MemoryStream(new byte[128])];
var stream = new ReadOnlyCompositeStream(arr);
byte[] buf = new byte[512];
int read = stream.Read(buf, 0, 512);
Assert.Equal(256, read);
}
}
}

View File

@@ -1,27 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.IO\SabreTools.IO.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -0,0 +1,253 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SabreTools.IO
{
/// <summary>
/// Read-only stream wrapper around multiple, consecutive streams
/// </summary>
public class ReadOnlyCompositeStream : Stream
{
#region Properties
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanSeek => true;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => _length;
/// <inheritdoc/>
public override long Position
{
get => _position;
set
{
_position = value;
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
}
}
#endregion
#region Internal State
/// <summary>
/// Internal collection of streams to read from
/// </summary>
private readonly List<Stream> _streams;
/// <summary>
/// Total length of all internal streams
/// </summary>
private long _length;
/// <summary>
/// Overall position in the stream wrapper
/// </summary>
private long _position;
#endregion
/// <summary>
/// Create a new, empty ReadOnlyCompositeStream
/// </summary>
public ReadOnlyCompositeStream()
{
_streams = [];
_length = 0;
_position = 0;
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(Stream[] streams)
{
_streams = [.. streams];
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Create a new ReadOnlyCompositeStream from an existing collection of Streams
/// </summary>
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
{
_streams = streams.ToList();
_length = 0;
_position = 0;
// Verify the streams and add to the length
foreach (var stream in streams)
{
if (!stream.CanRead || !stream.CanSeek)
throw new ArgumentException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
/// <summary>
/// Add a new stream to the collection
/// </summary>
public bool AddStream(Stream stream)
{
// Verify the stream
if (!stream.CanRead || !stream.CanSeek)
return false;
// Add the stream to the end
_streams.Add(stream);
_length += stream.Length;
return true;
}
#region Stream Implementations
/// <inheritdoc/>
public override void Flush() => throw new NotImplementedException();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Determine which stream we start reading from
(int streamIndex, long streamOffset) = DetermineStreamIndex(offset);
if (streamIndex == -1)
throw new ArgumentOutOfRangeException(nameof(offset));
// Determine if the stream fully contains the requested segment
bool singleStream = StreamContains(streamIndex, streamOffset, count);
// If we can read from a single stream
if (singleStream)
{
_position += count;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
return _streams[streamIndex].Read(buffer, offset, count);
}
// For all other cases, we read until there's no more
int readBytes = 0, originalCount = count;
while (readBytes < originalCount)
{
// Determine how much can be read from the current stream
long currentBytes = _streams[streamIndex].Length - streamOffset;
int shouldRead = Math.Min((int)currentBytes, count);
// Read from the current stream
_position += shouldRead;
_streams[streamIndex].Seek(streamOffset, SeekOrigin.Begin);
readBytes += _streams[streamIndex].Read(buffer, offset, shouldRead);
// Update the read variables
offset += shouldRead;
count -= shouldRead;
// Move to the next stream
streamIndex++;
streamOffset = 0;
// Validate the next stream exists
if (streamIndex >= _streams.Count)
break;
}
// Return the number of bytes that could be read
return readBytes;
}
/// <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)}");
};
// Handle out-of-bounds seeks
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
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
#region Helpers
/// <summary>
/// Determine the index of the stream that contains a particular offset
/// </summary>
/// <returns>Index of the stream containing the offset and the real offset in the stream, (-1, -1) on error</returns>
private (int index, long realOffset) DetermineStreamIndex(int offset)
{
// If the offset is out of bounds
if (offset < 0 || offset >= _length)
return (-1, -1);
// Seek through until we hit the correct offset
long currentLength = 0;
for (int i = 0; i < _streams.Count; i++)
{
currentLength += _streams[i].Length;
if (currentLength > offset)
{
long realOffset = offset - (currentLength - _streams[i].Length);
return (i, realOffset);
}
}
// Should never happen
return (-1, -1);
}
/// <summary>
/// Determines if a stream contains a particular segment
/// </summary>
private bool StreamContains(int streamIndex, long offset, int length)
{
// Ensure the arguments are valid
if (streamIndex < 0 || streamIndex >= _streams.Count)
throw new ArgumentOutOfRangeException(nameof(streamIndex));
if (offset < 0 || offset >= _streams[streamIndex].Length)
throw new ArgumentOutOfRangeException(nameof(offset));
// Handle the general case
return _streams[streamIndex].Length - offset >= length;
}
#endregion
}
}