2025-07-20 17:35:22 +01:00
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
{
/// <summary>
/// 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.
/// </summary>
int DefaultBufferSize { get ; set ; }
/// <summary>
/// Returns the immediate underlying stream in the stack.
/// </summary>
Stream BaseStream ( ) ;
/// <summary>
/// Gets or sets the size of the buffer if the stream supports buffering; otherwise, returns 0.
/// This property must not throw.
/// </summary>
int BufferSize { get ; set ; }
/// <summary>
/// Gets or sets the current position within the buffer if the stream supports buffering; otherwise, returns 0.
/// This property must not throw.
/// </summary>
int BufferPosition { get ; set ; }
/// <summary>
/// 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.
/// </summary>
/// <param name="position">The absolute position to set within the stream stack.</param>
2025-07-21 23:45:57 +01:00
void SetPosition ( long position ) ;
2025-07-20 17:35:22 +01:00
#if DEBUG_STREAMS
/// <summary>
/// Gets or sets the unique instance identifier for debugging purposes.
/// </summary>
long InstanceId { get ; set ; }
#endif
}
internal static class StackStreamExtensions
{
2025-07-21 23:45:57 +01:00
/// <summary>
/// Gets the logical position of the first buffering stream in the stack, or 0 if none exist.
/// </summary>
/// <param name="stream">The most derived (outermost) stream in the stack.</param>
/// <returns>The position of the first buffering stream, or 0 if not found.</returns>
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 ;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The most derived (outermost) stream in the stack.</param>
/// <param name="count">The number of bytes to rewind within the buffer.</param>
2025-07-20 17:35:22 +01:00
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 ;
2025-07-20 19:16:11 +01:00
buffStream . BufferPosition - = Math . Min ( buffStream . BufferPosition , count ) ;
2025-07-20 17:35:22 +01:00
}
current = current ? . BaseStream ( ) as IStreamStack ;
}
}
2025-07-20 18:11:37 +01:00
2025-07-21 23:45:57 +01:00
/// <summary>
/// Sets the buffer size on the first buffering stream in the stack, or on the outermost stream if none exist.
/// If <paramref name="force"/> is true, sets the buffer size regardless of current value.
/// </summary>
/// <param name="stream">The most derived (outermost) stream in the stack.</param>
/// <param name="bufferSize">The buffer size to set.</param>
/// <param name="force">If true, forces the buffer size to be set even if already set.</param>
2025-07-20 17:35:22 +01:00
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 ;
}
/// <summary>
/// Attempts to set the position in the stream stack. If a buffering stream is present and the position is within its buffer,
2025-07-21 23:45:57 +01:00
/// 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.
2025-07-20 17:35:22 +01:00
/// Seeking is never performed if any intermediate stream in the stack is buffering.
/// Throws if the position cannot be set.
/// </summary>
/// <param name="stream">
2025-07-21 23:45:57 +01:00
/// 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.
2025-07-20 17:35:22 +01:00
/// </param>
/// <param name="position">The absolute position to set.</param>
/// <returns>The position that was set.</returns>
internal static long StackSeek ( this IStreamStack stream , long position )
{
var stack = new List < IStreamStack > ( ) ;
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 ] ;
2025-11-14 01:47:15 +01:00
var targetBufferPosition = position - bufferingStream . GetPosition ( ) + bufferingStream . BufferPosition ;
if ( targetBufferPosition > = 0 & & targetBufferPosition < = bufferingStream . BufferSize )
2025-07-20 17:35:22 +01:00
{
2025-11-14 01:47:15 +01:00
bufferingStream . BufferPosition = ( int ) targetBufferPosition ;
2025-07-20 17:35:22 +01:00
return position ;
}
2025-11-14 01:47:15 +01:00
// If position is not in buffer, reset buffer and proceed as non-buffering
bufferingStream . BufferPosition = 0 ;
2025-07-20 17:35:22 +01:00
// 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 ;
}
2025-07-20 18:11:37 +01:00
throw new NotSupportedException (
"Cannot set position on this stream stack (no seekable or buffering stream supports the requested position)."
) ;
2025-07-20 17:35:22 +01:00
}
/// <summary>
2025-07-21 23:45:57 +01:00
/// 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.
2025-07-20 17:35:22 +01:00
/// </summary>
2025-07-21 23:45:57 +01:00
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The offset in the buffer to start writing data.</param>
/// <param name="count">The maximum number of bytes to read.</param>
/// <param name="buffStream">Returns the buffering stream found in the stack, or null if none exists.</param>
/// <param name="baseReadCount">Returns the number of bytes actually read from the base stream, or -1 if no buffering stream was found.</param>
/// <returns>The number of bytes read into the buffer.</returns>
2025-07-20 18:11:37 +01:00
internal static int Read (
this IStreamStack stream ,
byte [ ] buffer ,
int offset ,
int count ,
out IStreamStack ? buffStream ,
out int baseReadCount
)
2025-07-20 17:35:22 +01:00
{
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" ) ;
}
2025-07-21 23:45:57 +01:00
/// <summary>
/// Gets or creates a unique instance ID for the stream stack for debugging purposes.
/// </summary>
/// <param name="stream">The stream stack.</param>
/// <param name="instanceId">Reference to the instance ID field.</param>
/// <param name="construct">Whether this is being called during construction.</param>
/// <returns>The instance ID.</returns>
2025-07-20 18:11:37 +01:00
public static long GetInstanceId (
this IStreamStack stream ,
ref long instanceId ,
bool construct
)
2025-07-20 17:35:22 +01:00
{
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 ;
}
2025-07-21 23:45:57 +01:00
/// <summary>
/// Writes a debug message for stream construction.
/// </summary>
/// <param name="stream">The stream stack.</param>
/// <param name="constructing">The type being constructed.</param>
2025-07-20 17:35:22 +01:00
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 ) ;
2025-07-20 18:11:37 +01:00
string parentInfo =
frame ! = null
? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()"
: "Unknown" ;
2025-07-20 17:35:22 +01:00
if ( constructing . FullName = = stream . GetType ( ) . FullName ) //don't debug base IStackStream types
2025-07-20 18:11:37 +01:00
Debug . WriteLine (
$"{GetStreamStackString(stream, true)} : Constructed by [{parentInfo}]"
) ;
2025-07-20 17:35:22 +01:00
}
2025-07-21 23:45:57 +01:00
/// <summary>
/// Writes a debug message for stream disposal.
/// </summary>
/// <param name="stream">The stream stack.</param>
/// <param name="constructing">The type being disposed.</param>
2025-07-20 17:35:22 +01:00
public static void DebugDispose ( this IStreamStack stream , Type constructing )
{
var frame = ( new StackTrace ( ) ) . GetFrame ( 3 ) ;
2025-07-20 18:11:37 +01:00
string parentInfo =
frame ! = null
? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()"
: "Unknown" ;
2025-07-20 17:35:22 +01:00
if ( constructing . FullName = = stream . GetType ( ) . FullName ) //don't debug base IStackStream types
Debug . WriteLine ( $"{GetStreamStackString(stream, false)} : Disposed by [{parentInfo}]" ) ;
}
2025-07-21 23:45:57 +01:00
/// <summary>
/// Writes a debug trace message for the stream.
/// </summary>
/// <param name="stream">The stream stack.</param>
/// <param name="message">The debug message to write.</param>
2025-07-20 17:35:22 +01:00
public static void DebugTrace ( this IStreamStack stream , string message )
{
2025-07-20 18:11:37 +01:00
Debug . WriteLine (
$"{GetStreamStackString(stream, false)} : [{stream.GetType().Name}]{message}"
) ;
2025-07-20 17:35:22 +01:00
}
/// <summary>
/// Returns the full stream chain as a string, including instance IDs and positions.
/// </summary>
2025-07-21 23:45:57 +01:00
/// <param name="stream">The stream stack to represent.</param>
/// <param name="construct">Whether this is being called during construction.</param>
/// <returns>A string representation of the entire stream stack.</returns>
2025-07-20 17:35:22 +01:00
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" ) : "" ;
2025-07-20 18:11:37 +01:00
string defBuffSize =
sStack ! = null ? "Dx" + sStack . DefaultBufferSize . ToString ( "x" ) : "" ;
2025-07-20 17:35:22 +01:00
if ( sb . Length > 0 )
sb . Insert ( 0 , "/" ) ;
try
{
2025-07-20 18:11:37 +01:00
sb . Insert (
0 ,
$"{current.GetType().Name}{id}[{cleansePos(current.Position)}:{buffSize}:{defBuffSize}]"
) ;
2025-07-20 17:35:22 +01:00
}
catch
{
if ( current is SharpCompressStream scs )
2025-07-20 18:11:37 +01:00
sb . Insert (
0 ,
$"{current.GetType().Name}{id}[{cleansePos(scs.InternalPosition)}:{buffSize}:{defBuffSize}]"
) ;
2025-07-20 17:35:22 +01:00
else
sb . Insert ( 0 , $"{current.GetType().Name}{id}[:{buffSize}]" ) ;
}
if ( sStack ! = null )
2025-07-21 23:45:57 +01:00
current = sStack . BaseStream ( ) ; //current may not be a IStreamStack, allow one more loop
2025-07-20 17:35:22 +01:00
else
break ;
}
return sb . ToString ( ) ;
}
#endif
}
}