mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-09-22 14:45:58 +00:00
Add ReadOnlyCompositeStream
This commit is contained in:
136
SabreTools.IO.Test/ReadOnlyCompositeStreamTests.cs
Normal file
136
SabreTools.IO.Test/ReadOnlyCompositeStreamTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
253
SabreTools.IO/ReadOnlyCompositeStream.cs
Normal file
253
SabreTools.IO/ReadOnlyCompositeStream.cs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user