@@ -212,10 +212,31 @@ protected override IReader CreateReaderForSolidExtraction() =>
212212 public override long TotalSize =>
213213 _database ? . _packSizes . Aggregate ( 0L , ( total , packSize ) => total + packSize ) ?? 0 ;
214214
215- private sealed class SevenZipReader : AbstractReader < SevenZipEntry , SevenZipVolume >
215+ internal sealed class SevenZipReader : AbstractReader < SevenZipEntry , SevenZipVolume >
216216 {
217217 private readonly SevenZipArchive _archive ;
218218 private SevenZipEntry ? _currentEntry ;
219+ private Stream ? _currentFolderStream ;
220+ private CFolder ? _currentFolder ;
221+
222+ /// <summary>
223+ /// Enables internal diagnostics for tests.
224+ /// When disabled (default), diagnostics properties return null to avoid exposing internal state.
225+ /// </summary>
226+ internal bool DiagnosticsEnabled { get ; set ; }
227+
228+ /// <summary>
229+ /// Current folder instance used to decide whether the solid folder stream should be reused.
230+ /// Only available when <see cref="DiagnosticsEnabled"/> is true.
231+ /// </summary>
232+ internal object ? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null ;
233+
234+ /// <summary>
235+ /// Current shared folder stream instance.
236+ /// Only available when <see cref="DiagnosticsEnabled"/> is true.
237+ /// </summary>
238+ internal Stream ? DiagnosticsCurrentFolderStream =>
239+ DiagnosticsEnabled ? _currentFolderStream : null ;
219240
220241 internal SevenZipReader ( ReaderOptions readerOptions , SevenZipArchive archive )
221242 : base ( readerOptions , ArchiveType . SevenZip ) => this . _archive = archive ;
@@ -231,9 +252,10 @@ protected override IEnumerable<SevenZipEntry> GetEntries(Stream stream)
231252 _currentEntry = dir ;
232253 yield return dir ;
233254 }
234- // For non-directory entries, yield them without creating shared streams
235- // Each call to GetEntryStream() will create a fresh decompression stream
236- // to avoid state corruption issues with async operations
255+ // For solid archives (entries in the same folder share a compressed stream),
256+ // we must iterate entries sequentially and maintain the folder stream state
257+ // across entries in the same folder to avoid recreating the decompression
258+ // stream for each file, which breaks contiguous streaming.
237259 foreach ( var entry in entries . Where ( x => ! x . IsDirectory ) )
238260 {
239261 _currentEntry = entry ;
@@ -243,19 +265,53 @@ protected override IEnumerable<SevenZipEntry> GetEntries(Stream stream)
243265
244266 protected override EntryStream GetEntryStream ( )
245267 {
246- // Create a fresh decompression stream for each file (no state sharing).
247- // However, the LZMA decoder has bugs in its async implementation that cause
248- // state corruption even on fresh streams. The SyncOnlyStream wrapper
249- // works around these bugs by forcing async operations to use sync equivalents.
250- //
251- // TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
252- // so this wrapper is no longer necessary.
253268 var entry = _currentEntry . NotNull ( "currentEntry is not null" ) ;
254269 if ( entry . IsDirectory )
255270 {
256271 return CreateEntryStream ( Stream . Null ) ;
257272 }
258- return CreateEntryStream ( new SyncOnlyStream ( entry . FilePart . GetCompressedStream ( ) ) ) ;
273+
274+ var filePart = ( SevenZipFilePart ) entry . FilePart ;
275+ if ( ! filePart . Header . HasStream )
276+ {
277+ // Entries with no underlying stream (e.g., empty files or anti-items)
278+ // should return an empty stream, matching previous behavior.
279+ return CreateEntryStream ( Stream . Null ) ;
280+ }
281+
282+ var folder = filePart . Folder ;
283+ // Check if we're starting a new folder - dispose old folder stream if needed
284+ if ( folder != _currentFolder )
285+ {
286+ _currentFolderStream ? . Dispose ( ) ;
287+ _currentFolderStream = null ;
288+ _currentFolder = folder ;
289+ }
290+
291+ // Create the folder stream once per folder
292+ if ( _currentFolderStream is null )
293+ {
294+ _currentFolderStream = _archive . _database ! . GetFolderStream (
295+ _archive . Volumes . Single ( ) . Stream ,
296+ folder ! ,
297+ _archive . _database . PasswordProvider
298+ ) ;
299+ }
300+
301+ // Wrap with SyncOnlyStream to work around LZMA async bugs
302+ // Return a ReadOnlySubStream that reads from the shared folder stream
303+ return CreateEntryStream (
304+ new SyncOnlyStream (
305+ new ReadOnlySubStream ( _currentFolderStream , entry . Size , leaveOpen : true )
306+ )
307+ ) ;
308+ }
309+
310+ public override void Dispose ( )
311+ {
312+ _currentFolderStream ? . Dispose ( ) ;
313+ _currentFolderStream = null ;
314+ base . Dispose ( ) ;
259315 }
260316 }
261317
0 commit comments