using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
namespace SabreTools.IO
{
///
/// Read-only stream wrapper around multiple, consecutive streams
///
public class ReadOnlyCompositeStream : Stream
{
#region Properties
///
public override bool CanRead => true;
///
public override bool CanSeek => true;
///
public override bool CanWrite => false;
///
public override long Length => _length;
///
public override long Position
{
get => _position;
set
{
_position = value;
if (_position < 0)
_position = 0;
else if (_position >= _length)
_position = _length - 1;
}
}
#endregion
#region Instance Variables
///
/// Internal set of streams to read from
///
private readonly List _streams;
///
/// Total length of all internal streams
///
private long _length;
///
/// Overall position in the stream wrapper
///
private long _position;
#endregion
#region Constructors
///
/// Create a new, empty ReadOnlyCompositeStream
///
public ReadOnlyCompositeStream()
{
_streams = [];
_length = 0;
_position = 0;
}
///
/// Create a new ReadOnlyCompositeStream from a single Stream
///
/// Source stream
///
/// Thrown if is either marked
/// as unreadable or non-seekable.
///
public ReadOnlyCompositeStream(Stream stream)
{
_streams = [stream];
_length = 0;
_position = 0;
// Verify the stream and add to the length
if (!stream.CanRead || !stream.CanSeek)
throw new DataException($"{nameof(stream)} needs to be readable and seekable");
_length += stream.Length;
}
///
/// Create a new ReadOnlyCompositeStream from an existing set of Streams
///
/// Set of streams
///
/// Thrown if any stream in is
/// either marked as unreadable or non-seekable.
///
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 DataException($"All members of {nameof(streams)} need to be readable and seekable");
_length += stream.Length;
}
}
///
/// Create a new ReadOnlyCompositeStream from an existing set of Streams
///
/// Set of streams
///
/// Thrown if any stream in is
/// either marked as unreadable or non-seekable.
///
public ReadOnlyCompositeStream(IEnumerable 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;
}
}
#endregion
#region Data
///
/// Add a new stream to the set
///
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;
}
#endregion
#region Stream Implementations
///
public override void Flush()
=> throw new NotImplementedException();
///
public override int Read(byte[] buffer, int offset, int count)
{
// Determine which stream we start reading from
int streamIndex = DetermineStreamIndex(_position, out long streamOffset);
if (streamIndex == -1)
return 0;
// 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;
}
///
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
#region Helpers
///
/// Determine the index of the stream that contains a particular offset
///
/// Output parameter representing the real offset in the stream, -1 on error
/// Index of the stream containing the offset, -1 on error
private int DetermineStreamIndex(long offset, out long realOffset)
{
// If the offset is out of bounds
if (offset < 0 || offset >= _length)
{
realOffset = -1;
return -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)
{
realOffset = offset - (currentLength - _streams[i].Length);
return i;
}
}
// Should never happen
realOffset = -1;
return -1;
}
///
/// Determines if a stream contains a particular segment
///
/// Index into the backing streams set
/// Offset in the stream to check
/// Length of data requested at the offset
/// True if the offset and length are valid, false otherwise
///
/// Thrown if , ,
/// or are invalid.
///
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
}
}