mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-14 21:23:38 +00:00
365 lines
15 KiB
C#
365 lines
15 KiB
C#
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>
|
|
void SetPosition(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
|
|
{
|
|
/// <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>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
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 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.
|
|
/// </summary>
|
|
/// <param name="stream">
|
|
/// 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.
|
|
/// </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;
|
|
}
|
|
|
|
throw new NotSupportedException(
|
|
"Cannot set position on this stream stack (no seekable or buffering stream supports the requested position)."
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <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>
|
|
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");
|
|
}
|
|
|
|
/// <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>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a debug message for stream construction.
|
|
/// </summary>
|
|
/// <param name="stream">The stream stack.</param>
|
|
/// <param name="constructing">The type being constructed.</param>
|
|
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}]"
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a debug message for stream disposal.
|
|
/// </summary>
|
|
/// <param name="stream">The stream stack.</param>
|
|
/// <param name="constructing">The type being disposed.</param>
|
|
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}]");
|
|
}
|
|
|
|
/// <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>
|
|
public static void DebugTrace(this IStreamStack stream, string message)
|
|
{
|
|
Debug.WriteLine(
|
|
$"{GetStreamStackString(stream, false)} : [{stream.GetType().Name}]{message}"
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the full stream chain as a string, including instance IDs and positions.
|
|
/// </summary>
|
|
/// <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>
|
|
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
|
|
}
|
|
}
|