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. /// The position that was set, which must match the requested value if successful. void SetPostion(long position); #if DEBUG_STREAMS /// /// Gets or sets the unique instance identifier for debugging purposes. /// 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; buffStream.BufferPosition -= Math.Min(buffStream.BufferPosition, count); } current = current?.BaseStream() as IStreamStack; } } 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 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. /// /// /// 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. /// /// 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; 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)." ); } /// /// 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 /// /// /// /// /// /// 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"); } 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; } 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}]" ); } 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}]"); } 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. /// 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 IStreamStack, allow one more loop else break; } return sb.ToString(); } #endif } }