using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace SharpCompress.IO
{
public interface IStreamStack
{
///
/// Gets or sets the default buffer size to be applied when buffering is enabled for this stream stack.
/// This value is used by the SetBuffer extension method to configure buffering on the appropriate stream
/// in the stack hierarchy. A value of 0 indicates no default buffer size is set.
///
int DefaultBufferSize { get; set; }
///
/// Returns the immediate underlying stream in the stack.
///
Stream BaseStream();
///
/// Gets or sets the size of the buffer if the stream supports buffering; otherwise, returns 0.
/// This property must not throw.
///
int BufferSize { get; set; }
///
/// Gets or sets the current position within the buffer if the stream supports buffering; otherwise, returns 0.
/// This property must not throw.
///
int BufferPosition { get; set; }
///
/// Updates the internal position state of the stream. This should not perform seeking on the underlying stream,
/// but should update any internal position or buffer state as appropriate for the stream implementation.
///
/// The absolute position to set within the stream stack.
void SetPosition(long position);
#if DEBUG_STREAMS
///
/// Gets or sets the unique instance identifier for debugging purposes.
///
long InstanceId { get; set; }
#endif
}
internal static class StackStreamExtensions
{
///
/// Gets the logical position of the first buffering stream in the stack, or 0 if none exist.
///
/// The most derived (outermost) stream in the stack.
/// The position of the first buffering stream, or 0 if not found.
internal static long GetPosition(this IStreamStack stream)
{
IStreamStack? current = stream;
while (current != null)
{
if (current.BufferSize != 0 && current is Stream st)
{
return st.Position;
}
current = current?.BaseStream() as IStreamStack;
}
return 0;
}
///
/// Rewinds the buffer of the outermost buffering stream in the stack by the specified count, if supported.
/// Only the most derived buffering stream is affected.
///
/// The most derived (outermost) stream in the stack.
/// The number of bytes to rewind within the buffer.
internal static void Rewind(this IStreamStack stream, int count)
{
Stream baseStream = stream.BaseStream();
Stream thisStream = (Stream)stream;
IStreamStack? buffStream = null;
IStreamStack? current = stream;
while (buffStream == null && current != null)
{
if (current.BufferSize != 0)
{
buffStream = current;
buffStream.BufferPosition -= Math.Min(buffStream.BufferPosition, count);
}
current = current?.BaseStream() as IStreamStack;
}
}
///
/// Sets the buffer size on the first buffering stream in the stack, or on the outermost stream if none exist.
/// If is true, sets the buffer size regardless of current value.
///
/// The most derived (outermost) stream in the stack.
/// The buffer size to set.
/// If true, forces the buffer size to be set even if already set.
internal static void SetBuffer(this IStreamStack stream, int bufferSize, bool force)
{
if (bufferSize == 0 || stream == null)
return;
IStreamStack? current = stream;
IStreamStack defaultBuffer = stream;
IStreamStack? buffer = null;
// First pass: find the deepest IStreamStack
while (current != null)
{
defaultBuffer = current;
if (buffer == null && ((current.BufferSize != 0 && bufferSize != 0) || force))
buffer = current;
if (defaultBuffer.DefaultBufferSize != 0)
break;
current = current.BaseStream() as IStreamStack;
}
if (defaultBuffer.DefaultBufferSize == 0)
defaultBuffer.DefaultBufferSize = bufferSize;
(buffer ?? stream).BufferSize = bufferSize;
}
///
/// Attempts to set the position in the stream stack. If a buffering stream is present and the position is within its buffer,
/// BufferPosition is set on the outermost buffering stream and all intermediate streams update their internal state via SetPosition.
/// If no buffering stream is present, seeks as close to the root stream as possible and updates all intermediate streams' state via SetPosition.
/// Seeking is never performed if any intermediate stream in the stack is buffering.
/// Throws if the position cannot be set.
///
///
/// The most derived (outermost) stream in the stack. The method traverses up the stack via BaseStream() until a stream can satisfy the buffer or seek request.
///
/// The absolute position to set.
/// The position that was set.
internal static long StackSeek(this IStreamStack stream, long position)
{
var stack = new List();
Stream? current = stream as Stream;
int lastBufferingIndex = -1;
Stream? firstSeekableStream = null;
// Traverse the stack, collecting info
while (current is IStreamStack stackStream)
{
stack.Add(stackStream);
if (stackStream.BufferSize > 0)
{
lastBufferingIndex = stack.Count - 1;
break;
}
current = stackStream.BaseStream();
}
// Find the first seekable stream (closest to the root)
if (current != null && current.CanSeek)
{
firstSeekableStream = current;
}
// If any buffering stream exists, try to set BufferPosition on the outermost one
if (lastBufferingIndex != -1)
{
var bufferingStream = stack[lastBufferingIndex];
var targetBufferPosition =
position - bufferingStream.GetPosition() + bufferingStream.BufferPosition;
if (targetBufferPosition >= 0 && targetBufferPosition <= bufferingStream.BufferSize)
{
bufferingStream.BufferPosition = (int)targetBufferPosition;
return position;
}
// If position is not in buffer, reset buffer and proceed as non-buffering
bufferingStream.BufferPosition = 0;
// Continue to seek as if no buffer is present
}
// If no buffering, or buffer was reset, seek at the first seekable stream (closest to the root)
if (firstSeekableStream != null)
{
firstSeekableStream.Seek(position, SeekOrigin.Begin);
return firstSeekableStream.Position;
}
throw new NotSupportedException(
"Cannot set position on this stream stack (no seekable or buffering stream supports the requested position)."
);
}
///
/// Reads bytes from the stream, using the position to observe how much was actually consumed and rewind the buffer to ensure further reads are correct.
/// This is required to prevent buffered reads from skipping data, while also benefiting from buffering and reduced stream IO reads.
///
/// The stream to read from.
/// The buffer to read data into.
/// The offset in the buffer to start writing data.
/// The maximum number of bytes to read.
/// Returns the buffering stream found in the stack, or null if none exists.
/// Returns the number of bytes actually read from the base stream, or -1 if no buffering stream was found.
/// The number of bytes read into the buffer.
internal static int Read(
this IStreamStack stream,
byte[] buffer,
int offset,
int count,
out IStreamStack? buffStream,
out int baseReadCount
)
{
Stream baseStream = stream.BaseStream();
Stream thisStream = (Stream)stream;
IStreamStack? current = stream;
buffStream = null;
baseReadCount = -1;
while (buffStream == null && (current = current?.BaseStream() as IStreamStack) != null)
{
if (current.BufferSize != 0)
{
buffStream = current;
}
}
long buffPos = buffStream == null ? -1 : ((Stream)buffStream).Position;
int read = baseStream.Read(buffer, offset, count); //amount read in to buffer
if (buffPos != -1)
{
baseReadCount = (int)(((Stream)buffStream!).Position - buffPos);
}
return read;
}
#if DEBUG_STREAMS
private static long _instanceCounter = 0;
private static string cleansePos(long pos)
{
if (pos < 0)
return "";
return "Px" + pos.ToString("x");
}
///
/// Gets or creates a unique instance ID for the stream stack for debugging purposes.
///
/// The stream stack.
/// Reference to the instance ID field.
/// Whether this is being called during construction.
/// The instance ID.
public static long GetInstanceId(
this IStreamStack stream,
ref long instanceId,
bool construct
)
{
if (instanceId == 0) //will not be equal to 0 when inherited IStackStream types are being used
instanceId = System.Threading.Interlocked.Increment(ref _instanceCounter);
return instanceId;
}
///
/// Writes a debug message for stream construction.
///
/// The stream stack.
/// The type being constructed.
public static void DebugConstruct(this IStreamStack stream, Type constructing)
{
long id = stream.InstanceId;
stream.InstanceId = GetInstanceId(stream, ref id, true);
var frame = (new StackTrace()).GetFrame(3);
string parentInfo =
frame != null
? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()"
: "Unknown";
if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types
Debug.WriteLine(
$"{GetStreamStackString(stream, true)} : Constructed by [{parentInfo}]"
);
}
///
/// Writes a debug message for stream disposal.
///
/// The stream stack.
/// The type being disposed.
public static void DebugDispose(this IStreamStack stream, Type constructing)
{
var frame = (new StackTrace()).GetFrame(3);
string parentInfo =
frame != null
? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()"
: "Unknown";
if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types
Debug.WriteLine($"{GetStreamStackString(stream, false)} : Disposed by [{parentInfo}]");
}
///
/// Writes a debug trace message for the stream.
///
/// The stream stack.
/// The debug message to write.
public static void DebugTrace(this IStreamStack stream, string message)
{
Debug.WriteLine(
$"{GetStreamStackString(stream, false)} : [{stream.GetType().Name}]{message}"
);
}
///
/// Returns the full stream chain as a string, including instance IDs and positions.
///
/// The stream stack to represent.
/// Whether this is being called during construction.
/// A string representation of the entire stream stack.
public static string GetStreamStackString(this IStreamStack stream, bool construct)
{
var sb = new StringBuilder();
Stream? current = stream as Stream;
while (current != null)
{
IStreamStack? sStack = current as IStreamStack;
string id = sStack != null ? "#" + sStack.InstanceId.ToString() : "";
string buffSize = sStack != null ? "Bx" + sStack.BufferSize.ToString("x") : "";
string defBuffSize =
sStack != null ? "Dx" + sStack.DefaultBufferSize.ToString("x") : "";
if (sb.Length > 0)
sb.Insert(0, "/");
try
{
sb.Insert(
0,
$"{current.GetType().Name}{id}[{cleansePos(current.Position)}:{buffSize}:{defBuffSize}]"
);
}
catch
{
if (current is SharpCompressStream scs)
sb.Insert(
0,
$"{current.GetType().Name}{id}[{cleansePos(scs.InternalPosition)}:{buffSize}:{defBuffSize}]"
);
else
sb.Insert(0, $"{current.GetType().Name}{id}[:{buffSize}]");
}
if (sStack != null)
current = sStack.BaseStream(); //current may not be a IStreamStack, allow one more loop
else
break;
}
return sb.ToString();
}
#endif
}
}