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 } }