From 37c0f5aec575f7dcdc06e76f75e9ca9707bbf0ee Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:32:36 +0900 Subject: [PATCH 1/9] fix: Change LeaveStreamOpen default from true to false As of v0.21, the library is documented to close wrapped streams by default. However, the ReaderOptions.LeaveStreamOpen property defaulted to true, which contradicted the documented behavior and caused file locks when deleting archives after extraction. Changes: - Set LeaveStreamOpen = false as the default (consistent with v0.21 design) - Updated ReaderOptions.cs with comprehensive documentation explaining: * File-based vs caller-provided stream handling * When to use LeaveStreamOpen = true * Usage examples for both scenarios - Updated AGENTS.md Stream Handling Rules section with: * Clear explanation of default disposal behavior * Overload differences (file-based vs stream-based) * Guidance on using ForExternalStream preset This ensures the implementation matches the documented behavior and prevents unexpected file locking issues during archive deletion. Fixes: File deletion failures after archive extraction --- AGENTS.md | 11 +++++--- src/SharpCompress/Readers/ReaderOptions.cs | 31 ++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 32bbb9426..3c960eea1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,10 +126,15 @@ SharpCompress supports multiple archive and compression formats: - See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix ### Stream Handling Rules -- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default -- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal +- **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) + - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically + - Stream-based overloads (e.g., `OpenArchive(Stream stream)`) use `ReaderOptions.ForExternalStream` by default, which sets `LeaveStreamOpen = true` +- **For caller-provided streams**: Use `ReaderOptions` with `LeaveStreamOpen = true` or the `ForExternalStream` preset to prevent automatic disposal + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` +- **For file paths**: SharpCompress manages the stream lifecycle; no manual disposal needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal -- Always dispose of readers, writers, and archives in `using` blocks +- Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs ### Async/Await Patterns diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 6a063611a..ed4961015 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -24,9 +24,36 @@ namespace SharpCompress.Readers; public sealed record ReaderOptions : IReaderOptions { /// - /// SharpCompress will keep the supplied streams open. Default is true. + /// SharpCompress will keep the supplied streams open. + /// Default is false as of v0.21: streams are closed when the archive/reader is disposed. + /// Set to true when passing caller-owned streams that should not be disposed. /// - public bool LeaveStreamOpen { get; init; } = true; + /// + /// + /// Default behavior (LeaveStreamOpen = false): + /// When you open an archive from a file path (e.g., GZipArchive.OpenArchive(filePath)), + /// SharpCompress manages the stream lifetime and closes it on Dispose. + /// + /// + /// Caller-provided streams (LeaveStreamOpen = true): + /// When you pass a stream you created (FileStream, MemoryStream, NetworkStream, etc.), + /// set LeaveStreamOpen = true to prevent SharpCompress from disposing it. + /// Use preset for convenience. + /// + /// + /// Example: + /// + /// // File-based: stream managed by library + /// using var archive = GZipArchive.OpenArchive(filePath); // LeaveStreamOpen = false + /// + /// // Caller-provided stream: caller manages lifetime + /// using var stream = File.OpenRead(filePath); + /// var options = new ReaderOptions { LeaveStreamOpen = true }; + /// using var archive = GZipArchive.OpenArchive(stream, options); + /// + /// + /// + public bool LeaveStreamOpen { get; init; } = false; /// /// Encoding to use for archive entry names. From 23840b7a0ff0f7eb8048249c75fa038ab14490cd Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:15:56 +0900 Subject: [PATCH 2/9] fix(zip): stop overriding LeaveStreamOpen in OpenArchive --- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index db0b84892..aeb2686b0 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -41,7 +41,7 @@ public static IWritableArchive OpenArchive( new SourceStream( fileInfo, i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + readerOptions ?? new ReaderOptions() ) ); } @@ -57,7 +57,7 @@ public static IWritableArchive OpenArchive( new SourceStream( files[0], i => i < files.Count ? files[i] : null, - readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false } + readerOptions ?? new ReaderOptions() ) ); } From ea950457f51b4ac8f076bf4110f15f0e354f963b Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:16:16 +0900 Subject: [PATCH 3/9] fix(docs): clarify stream handling rules and ownership in AGENTS.md --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3c960eea1..0d91e2180 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,11 +128,11 @@ SharpCompress supports multiple archive and compression formats: ### Stream Handling Rules - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically - - Stream-based overloads (e.g., `OpenArchive(Stream stream)`) use `ReaderOptions.ForExternalStream` by default, which sets `LeaveStreamOpen = true` -- **For caller-provided streams**: Use `ReaderOptions` with `LeaveStreamOpen = true` or the `ForExternalStream` preset to prevent automatic disposal - - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - - Or: `var options = ReaderOptions.ForExternalStream;` -- **For file paths**: SharpCompress manages the stream lifecycle; no manual disposal needed beyond the archive/reader itself + - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly + - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` + - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is neededbeyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From 8d193db721319d86c581c2927fa94743fb5dbcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:32:27 +0900 Subject: [PATCH 4/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/Readers/ReaderOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index ed4961015..4f6788c32 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -24,7 +24,7 @@ namespace SharpCompress.Readers; public sealed record ReaderOptions : IReaderOptions { /// - /// SharpCompress will keep the supplied streams open. + /// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed. /// Default is false as of v0.21: streams are closed when the archive/reader is disposed. /// Set to true when passing caller-owned streams that should not be disposed. /// From 1bd7bf2169937f4c87632c2709a15bfb0004944d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:40:13 +0900 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0d91e2180..0284f9e49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,7 +132,7 @@ SharpCompress supports multiple archive and compression formats: - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - Or: `var options = ReaderOptions.ForExternalStream;` - - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is neededbeyond the archive/reader itself + - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From f5e27faefc8e3b237776ab6a8d1b75d0a7e2cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:48:34 +0900 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0284f9e49..ce2bdaccb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -129,10 +129,10 @@ SharpCompress supports multiple archive and compression formats: - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly - - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - - Or: `var options = ReaderOptions.ForExternalStream;` - - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself +- **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` + - Or: `var options = ReaderOptions.ForExternalStream;` +- **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself - Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal - Always dispose of readers, writers, and archives in `using` / `await using` blocks - For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs From 6a7e0b2f10e6c0c19eb9338926ec9a158e1be5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:09:10 +0900 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ce2bdaccb..d8bc7c85e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,8 @@ SharpCompress supports multiple archive and compression formats: ### Stream Handling Rules - **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) - - File-based overloads (e.g., `OpenArchive(string filePath)`) use `ReaderOptions.ForFilePath` automatically + - File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader + - Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly - **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` From a351b56737939888c95d88efb2d2c065b35f1a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B7=E3=81=93=E3=82=8B=E3=81=B5?= <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:26:31 +0900 Subject: [PATCH 8/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 7 ++++--- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 2 +- src/SharpCompress/Readers/ReaderOptions.cs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d8bc7c85e..a47121a02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,11 +126,12 @@ SharpCompress supports multiple archive and compression formats: - See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix ### Stream Handling Rules -- **Disposal**: As of version 0.21, SharpCompress **closes wrapped streams by default** (`LeaveStreamOpen = false`) +- **Disposal semantics**: The default `ReaderOptions.LeaveStreamOpen` value is `false`, but effective stream ownership depends on which API overload you call - File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader - Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics - - Do **not** assume every stream-based overload defaults to `ReaderOptions.ForExternalStream`; some APIs require you to pass stream ownership options explicitly -- **For caller-provided streams**: Pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream + - Several high-level overloads that accept a caller-provided `Stream` use external-stream semantics by default (for example, `ReaderFactory.OpenReader(Stream)` / `ArchiveFactory.OpenArchive(Stream)`), so the caller's stream is typically left open unless you opt into different ownership behavior + - Do **not** assume every stream-based overload behaves identically; some APIs require you to pass stream ownership options explicitly +- **For caller-provided streams**: When the overload accepts `ReaderOptions`, pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream - Example: `var options = new ReaderOptions { LeaveStreamOpen = true };` - Or: `var options = ReaderOptions.ForExternalStream;` - **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index aeb2686b0..9d46d3707 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -57,7 +57,7 @@ public static IWritableArchive OpenArchive( new SourceStream( files[0], i => i < files.Count ? files[i] : null, - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); } diff --git a/src/SharpCompress/Readers/ReaderOptions.cs b/src/SharpCompress/Readers/ReaderOptions.cs index 4f6788c32..cc90224af 100644 --- a/src/SharpCompress/Readers/ReaderOptions.cs +++ b/src/SharpCompress/Readers/ReaderOptions.cs @@ -25,7 +25,7 @@ public sealed record ReaderOptions : IReaderOptions { /// /// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed. - /// Default is false as of v0.21: streams are closed when the archive/reader is disposed. + /// As of v0.21, the library is documented to close streams by default; this option now defaults to false. /// Set to true when passing caller-owned streams that should not be disposed. /// /// From d99b406e09d097ca82e701b1b39bab4860875d4d Mon Sep 17 00:00:00 2001 From: Puk06 <86549420+puk06@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:02:30 +0900 Subject: [PATCH 9/9] fix: change default reader options in OpenArchive method for FileInfo --- src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index 9d46d3707..bdd18669d 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -41,7 +41,7 @@ public static IWritableArchive OpenArchive( new SourceStream( fileInfo, i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo), - readerOptions ?? new ReaderOptions() + readerOptions ?? ReaderOptions.ForFilePath ) ); }