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>
/// <returns>The position that was set, which must match the requested value if successful.</returns>
void SetPostion ( long position ) ;
#if DEBUG_STREAMS
/// <summary>
/// Gets or sets the unique instance identifier for debugging purposes.
/// </summary>
long InstanceId { get ; set ; }
#endif
}
internal static class StackStreamExtensions
{
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-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,
/// BufferPosition is set on the outermost buffering stream and all intermediate streams update their internal state via SetPostion.
/// If no buffering stream is present, seeks as close to the root stream as possible and updates all intermediate streams' state via SetPostion.
/// Seeking is never performed if any intermediate stream in the stack is buffering.
/// Throws if the position cannot be set.
/// </summary>
/// <param name="stream">
/// The most derived (outermost) stream in the stack. The method traverses up the stack via Stream() until a stream can satisfy the buffer or seek request.
/// </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 ;
int firstSeekableIndex = - 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 )
{
firstSeekableIndex = stack . Count ;
firstSeekableStream = current ;
}
// If any buffering stream exists, try to set BufferPosition on the outermost one
if ( lastBufferingIndex ! = - 1 )
{
var bufferingStream = stack [ lastBufferingIndex ] ;
if ( position > = 0 & & position < bufferingStream . BufferSize )
{
bufferingStream . BufferPosition = ( int ) position ;
return position ;
}
else
{
// 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 ;
}
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>
/// Read bytes from the stream, use 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 skipping data, while also benefitting from buffering and reduced stream IO reads
/// </summary>
/// <param name="stream"></param>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></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-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 ;
}
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
}
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}]" ) ;
}
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>
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 )
current = sStack . BaseStream ( ) ; //current may not be IStreamStack, allow one more loop
else
break ;
}
return sb . ToString ( ) ;
}
#endif
}
}