From 9ed27b9a7b90b62d7ea858783f4f1bcb7e67b5b2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 11:18:34 +0100 Subject: [PATCH 1/5] Update dependencies and address threading analyzer findings --- Directory.Packages.props | 8 +- build/packages.lock.json | 18 +-- .../Archives/Tar/TarArchive.Async.cs | 6 +- .../Archives/Zip/ZipArchive.Async.cs | 6 +- .../Compressors/Xz/XZFooter.Async.cs | 2 + src/SharpCompress/packages.lock.json | 110 +++++++++--------- .../SharpCompress.AotSmoke/packages.lock.json | 12 +- .../packages.lock.json | 12 +- tests/SharpCompress.Test/packages.lock.json | 42 ++++--- 9 files changed, 119 insertions(+), 97 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 18d207e9..b9457b1d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ - + @@ -9,16 +9,16 @@ - + - + diff --git a/build/packages.lock.json b/build/packages.lock.json index cf4e18fe..6db93011 100644 --- a/build/packages.lock.json +++ b/build/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Bullseye": { "type": "Direct", - "requested": "[6.2.0, )", - "resolved": "6.2.0", - "contentHash": "YWQxe+pvudGSzW1QqnRUlr9XV0xtlNvMxorVaFPqoMH8qAYNxniMQKGrPSiQkgEy0ckn/TAq98E+DIwTSR7vbQ==" + "requested": "[6.3.0, )", + "resolved": "6.3.0", + "contentHash": "yK14DF1aGvyGxIDFwApU1+YJcnXRmuVhGdz7js7eVTBnLvl/4rg20jb43YDhFvifcx7tRD4iOzF6ykoLdvWNfw==" }, "Glob": { "type": "Direct", @@ -36,9 +36,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -54,9 +54,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index d8991d43..ee3d30c8 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -39,7 +39,9 @@ public partial class TarArchive } else { - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -65,7 +67,9 @@ public partial class TarArchive } else { +#pragma warning disable VSTHRD103 // New writable entries only rewind and wrap their supplied stream; the base async method cannot open them. using var entryStream = entry.OpenEntryStream(); +#pragma warning restore VSTHRD103 await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index 97c412e6..4cae571b 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -101,7 +101,9 @@ public partial class ZipArchive } else { - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -125,7 +127,9 @@ public partial class ZipArchive } else { +#pragma warning disable VSTHRD103 // New writable entries only rewind and wrap their supplied stream; the base async method cannot open them. using var entryStream = entry.OpenEntryStream(); +#pragma warning restore VSTHRD103 await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs index b136335d..7ba8f219 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs @@ -36,7 +36,9 @@ public partial class XZFooter using (var reader = new BinaryReader(stream)) { BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; +#pragma warning disable VSTHRD103 // The footer bytes are already buffered in a MemoryStream. StreamFlags = reader.ReadBytes(2); +#pragma warning restore VSTHRD103 } var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); if (!magBy.AsSpan().SequenceEqual(_magicBytes)) diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index f4c3588e..6afa698b 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -33,9 +33,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -45,19 +45,20 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.ValueTuple": "4.6.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -118,6 +119,11 @@ "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.5.3" } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" } }, ".NETStandard,Version=v2.0": { @@ -152,9 +158,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "NETStandard.Library": { "type": "Direct", @@ -173,19 +179,19 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -282,9 +288,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -294,18 +300,18 @@ }, "System.Text.Encoding.CodePages": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -332,8 +338,8 @@ }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" } }, "net10.0": { @@ -365,9 +371,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -377,9 +383,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -428,9 +434,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -440,9 +446,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", @@ -497,9 +503,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -509,9 +515,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/tests/SharpCompress.AotSmoke/packages.lock.json b/tests/SharpCompress.AotSmoke/packages.lock.json index 882d8334..7fb4c275 100644 --- a/tests/SharpCompress.AotSmoke/packages.lock.json +++ b/tests/SharpCompress.AotSmoke/packages.lock.json @@ -36,9 +36,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -48,9 +48,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", diff --git a/tests/SharpCompress.Performance/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json index eb03c8cd..a3d81fee 100644 --- a/tests/SharpCompress.Performance/packages.lock.json +++ b/tests/SharpCompress.Performance/packages.lock.json @@ -52,9 +52,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -64,9 +64,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "BenchmarkDotNet.Annotations": { "type": "Transitive", diff --git a/tests/SharpCompress.Test/packages.lock.json b/tests/SharpCompress.Test/packages.lock.json index db9a3aa7..90ade434 100644 --- a/tests/SharpCompress.Test/packages.lock.json +++ b/tests/SharpCompress.Test/packages.lock.json @@ -42,9 +42,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -72,9 +72,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.ApplicationInsights": { "type": "Transitive", @@ -236,6 +236,11 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.6.2", + "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" + }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", @@ -311,7 +316,7 @@ "type": "Project", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "[8.0.0, )", - "System.Text.Encoding.CodePages": "[8.0.0, )" + "System.Text.Encoding.CodePages": "[10.0.12, )" } }, "Microsoft.Bcl.AsyncInterfaces": { @@ -325,12 +330,13 @@ }, "System.Text.Encoding.CodePages": { "type": "CentralTransitive", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "rt1G/LdKC6oc3xPyQhMn5vRTp9iTTt1BS9Dx/67TpW9KWgFJ39lyEz0y4CbDksx7iTZEfgG61UsQMoGEXEpF4A==", "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.ValueTuple": "4.6.2" } } }, @@ -373,9 +379,9 @@ }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.12.1, )", + "resolved": "18.12.1", + "contentHash": "sQt7NpnselT1gP1a7bTfhmfHYV0jbavst/8mOoWTMSjXhXzWPssqgBoIwrxNsgJUa7XQzD2raSRMAFfmlepxNw==" }, "PolySharp": { "type": "Direct", @@ -400,9 +406,9 @@ }, "Zomp.SyncMethodGenerator": { "type": "Direct", - "requested": "[2.0.56, )", - "resolved": "2.0.56", - "contentHash": "P1iMA1meu0uX8YcPWhkFnpzV3e+QFDDYMU6hMNyofS+pBlLgeVV7QevBmJmJ/mrxA1jujo3cqNaO1IapdfjOlA==" + "requested": "[2.0.66, )", + "resolved": "2.0.66", + "contentHash": "iYnF185851PEX5vnxrhUj2av30HoxvjUlPd0HVVuSAif7TCgc/Noc5xVf9jiLhMlI8Rt9QrJdef3KB8+opAJQA==" }, "Microsoft.ApplicationInsights": { "type": "Transitive", From 1080969e515109bce1a8d6feb91bd7a3396cd5f0 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 11:36:07 +0100 Subject: [PATCH 2/5] Fix async writable entry streams and cross-framework disposal --- docs/API.md | 2 + .../Archives/GZip/GZipArchive.Async.cs | 16 ++++ .../Archives/IArchiveEntryExtensions.cs | 8 ++ .../IWritableAsyncArchiveExtensions.cs | 4 + .../Archives/Tar/TarArchive.Async.cs | 20 ++++- .../Archives/Tar/TarArchiveEntry.cs | 2 +- .../Archives/Tar/TarWritableArchiveEntry.cs | 11 +++ .../Archives/Zip/ZipArchive.Async.cs | 18 +++- .../Archives/Zip/ZipArchiveEntry.Async.cs | 2 +- .../Archives/Zip/ZipWritableArchiveEntry.cs | 11 +++ .../SevenZip/SevenZipStreamsCompressor.cs | 8 ++ .../BZip2/CBZip2InputStream.Async.cs | 4 +- .../Compressors/Xz/XZFooter.Async.cs | 6 +- .../Readers/AbstractReader.Async.cs | 8 ++ .../Readers/IAsyncReaderExtensions.cs | 12 +++ src/SharpCompress/Utility.Async.cs | 4 + .../Writers/IWriterExtensions.cs | 4 + .../Writers/Zip/ZipWriter.Async.cs | 4 + .../Tar/TarArchiveAsyncTests.cs | 85 +++++++++++++++++++ .../Zip/ZipArchiveAsyncTests.cs | 82 ++++++++++++++++++ 20 files changed, 298 insertions(+), 13 deletions(-) diff --git a/docs/API.md b/docs/API.md index a845a0dc..ce59121b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -502,6 +502,8 @@ archive.SaveTo("output.zip", options); ### Extraction behavior +`ZipArchiveEntry.OpenEntryStreamAsync` and `TarArchiveEntry.OpenEntryStreamAsync` are virtual and support newly added writable entries as well as entries read from an archive. Opening a new writable entry checks cancellation, rewinds its supplied stream, and returns a non-owning wrapper. Disposing that wrapper leaves the supplied stream open; archive disposal follows the entry's `closeStream` setting. + ```csharp var options = new ExtractionOptions { diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 7b2cda51..95f01ae9 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -25,7 +25,11 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { +#if LEGACY_DOTNET using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); +#else + await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); +#endif await SaveToAsync(stream, new GZipWriterOptions(CompressionType.GZip), cancellationToken) .ConfigureAwait(false); } @@ -43,14 +47,22 @@ public partial class GZipArchive { throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); } +#if LEGACY_DOTNET + using var writer = new GZipWriter(stream, options); +#else await using var writer = new GZipWriter(stream, options); +#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) { if (!entry.IsDirectory) { +#if LEGACY_DOTNET using var entryStream = await entry +#else + await using var entryStream = await entry +#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); await writer @@ -65,7 +77,11 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { +#if LEGACY_DOTNET using var entryStream = await entry +#else + await using var entryStream = await entry +#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); await writer diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 31b4596a..617ac124 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -77,9 +77,13 @@ public static partial class IArchiveEntryExtensions var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); +#if LEGACY_DOTNET + using var entryStreamScope = entryStream; +#else await using var entryStreamScope = entryStream .DisposeAsyncScope() .ConfigureAwait(false); +#endif #endif var checkedStream = options is null ? entryStream @@ -164,7 +168,11 @@ public static partial class IArchiveEntryExtensions options, async (x, fm, ct) => { +#if LEGACY_DOTNET using var fs = File.Open(x, fm); +#else + await using var fs = File.Open(x, fm); +#endif await entry .WriteToAsync(fs, options.BufferSize, options, null, ct) .ConfigureAwait(false); diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index dcfee164..c3527f7c 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -83,7 +83,11 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions { +#if LEGACY_DOTNET using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); +#else + await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); +#endif await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index ee3d30c8..9bf95909 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -22,7 +22,11 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { +#if LEGACY_DOTNET using var writer = new TarWriter(stream, options); +#else + await using var writer = new TarWriter(stream, options); +#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -39,7 +43,11 @@ public partial class TarArchive } else { +#if LEGACY_DOTNET using var entryStream = await entry +#else + await using var entryStream = await entry +#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); await writer @@ -67,9 +75,15 @@ public partial class TarArchive } else { -#pragma warning disable VSTHRD103 // New writable entries only rewind and wrap their supplied stream; the base async method cannot open them. - using var entryStream = entry.OpenEntryStream(); -#pragma warning restore VSTHRD103 +#if LEGACY_DOTNET + using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); +#else + await using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); +#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs index c09d4755..88f832f6 100644 --- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs @@ -20,7 +20,7 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); - public async ValueTask OpenEntryStreamAsync( + public virtual async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) => ( diff --git a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs index 84f8ff44..c1a81f93 100644 --- a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -82,6 +84,15 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv return SharpCompressStream.CreateNonDisposing(stream); } + public override ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + // Writable entries have no archive parts; opening only rewinds the supplied stream. + return new(OpenEntryStream()); + } + internal override void Close() { if (closeStream && stream is not null) diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index 4cae571b..79d036cc 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -84,7 +84,11 @@ public partial class ZipArchive CancellationToken cancellationToken = default ) { +#if LEGACY_DOTNET using var writer = new ZipWriter(stream, options); +#else + await using var writer = new ZipWriter(stream, options); +#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -101,7 +105,11 @@ public partial class ZipArchive } else { +#if LEGACY_DOTNET using var entryStream = await entry +#else + await using var entryStream = await entry +#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); await writer @@ -127,9 +135,13 @@ public partial class ZipArchive } else { -#pragma warning disable VSTHRD103 // New writable entries only rewind and wrap their supplied stream; the base async method cannot open them. - using var entryStream = entry.OpenEntryStream(); -#pragma warning restore VSTHRD103 +#if LEGACY_DOTNET + using var entryStream = await entry +#else + await using var entryStream = await entry +#endif + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs index 308727a6..063b457c 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Archives.Zip; public partial class ZipArchiveEntry { - public async ValueTask OpenEntryStreamAsync( + public virtual async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs index b74fea04..d6995e55 100644 --- a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -83,6 +85,15 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry return SharpCompressStream.CreateNonDisposing(stream); } + public override ValueTask OpenEntryStreamAsync( + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + // Writable entries have no archive parts; opening only rewinds the supplied stream. + return new(OpenEntryStream()); + } + internal override void Close() { if (closeStream && !isDisposed && stream is not null) diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs index 1b704e0c..dca6fd19 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs @@ -122,7 +122,11 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc2; long inputSize2; { +#if LEGACY_DOTNET + using var lzma2Stream = new Lzma2EncoderStream( +#else await using var lzma2Stream = new Lzma2EncoderStream( +#endif outCrcStream, encoderProperties.DictionarySize, encoderProperties.NumFastBytes @@ -152,7 +156,11 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc; long inputSize; { +#if LEGACY_DOTNET + using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); +#else await using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); +#endif properties = lzmaStream.Properties; (inputCrc, inputSize) = await CopyWithCrcAsync( diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs index c1053216..ab85c1ef 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs @@ -707,7 +707,7 @@ internal partial class CBZip2InputStream } else { - SetupNoRandPartA(); + await SetupNoRandPartAAsync(cancellationToken).ConfigureAwait(false); } } @@ -792,7 +792,7 @@ internal partial class CBZip2InputStream z ^= (char)((rNToGo == 1) ? 1 : 0); j2 = 0; currentState = RAND_PART_C_STATE; - SetupRandPartC(); + await SetupRandPartCAsync(cancellationToken).ConfigureAwait(false); } else { diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs index 7ba8f219..de7b8fe9 100644 --- a/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/XZFooter.Async.cs @@ -36,9 +36,9 @@ public partial class XZFooter using (var reader = new BinaryReader(stream)) { BackwardSize = (reader.ReadLittleEndianUInt32() + 1) * 4; -#pragma warning disable VSTHRD103 // The footer bytes are already buffered in a MemoryStream. - StreamFlags = reader.ReadBytes(2); -#pragma warning restore VSTHRD103 + StreamFlags = await reader + .ReadBytesAsync(2, cancellationToken: cancellationToken) + .ConfigureAwait(false); } var magBy = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false); if (!magBy.AsSpan().SequenceEqual(_magicBytes)) diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 969c73e6..01811afa 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,7 +104,11 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip +#if LEGACY_DOTNET + using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); +#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); +#endif await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); } @@ -134,7 +138,11 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { +#if LEGACY_DOTNET + using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); +#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); +#endif var sourceStream = WrapWithProgress(s, Entry); await sourceStream .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 5b4e667b..989026be 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -45,7 +45,11 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { +#if LEGACY_DOTNET using var fs = File.Open(x, fm); +#else + await using var fs = File.Open(x, fm); +#endif await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -83,7 +87,11 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { +#if LEGACY_DOTNET using var fs = File.Open(x, fm); +#else + await using var fs = File.Open(x, fm); +#endif await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -108,7 +116,11 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { +#if LEGACY_DOTNET + using var entryStream = await reader +#else await using var entryStream = await reader +#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); var checkedStream = IEntryExtensions.WrapWithChecksumValidation( diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index df323d69..a23a88f3 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -70,7 +70,11 @@ internal static partial class Utility ) { // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync +#if LEGACY_DOTNET using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); +#else + await using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); +#endif await limitedStream .CopyToAsync(destination, bufferSize ?? Constants.BufferSize, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index a178dd5d..8e37a5b9 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -77,7 +77,11 @@ public static class IWriterExtensions { throw new ArgumentException("Source does not exist: " + source.FullName); } +#if LEGACY_DOTNET using var stream = source.OpenRead(); +#else + await using var stream = source.OpenRead(); +#endif await writer .WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs index ec0d9ca4..2956cc4f 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -83,7 +83,11 @@ public partial class ZipWriter #if !SYNC_ONLY cancellationToken.ThrowIfCancellationRequested(); #endif +#if LEGACY_DOTNET + using var output = await WriteToStreamAsync( +#else await using var output = await WriteToStreamAsync( +#endif entryPath, zipWriterEntryOptions, cancellationToken diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index dabc0fa2..ea206009 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Archives.Tar; @@ -229,6 +230,90 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.True(entryStream.IsDisposed); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask Tar_New_Entry_OpenEntryStreamAsync_Rewinds_And_Preserves_Ownership( + bool closeStream + ) + { + var bytes = Encoding.UTF8.GetBytes("test"); + using var source = new TestStream(new MemoryStream(bytes)); + await using (var archive = await TarArchive.CreateAsyncArchive()) + { + IArchiveEntry entry = await archive.AddEntryAsync( + "test.txt", + source, + closeStream: closeStream, + size: source.Length + ); + for (var i = 0; i < 2; i++) + { + source.Position = source.Length; + using (var opened = await entry.OpenEntryStreamAsync(CancellationToken.None)) + { + using var destination = new MemoryStream(); + await opened.CopyToAsync(destination, 81920, CancellationToken.None); + Assert.Equal(bytes, destination.ToArray()); + } + Assert.False(source.IsDisposed); + } + } + Assert.Equal(closeStream, source.IsDisposed); + } + + [Fact] + public async ValueTask Tar_New_Entry_OpenEntryStreamAsync_Cancelled_Does_Not_Rewind() + { + using var source = new MemoryStream(Encoding.UTF8.GetBytes("test")); + await using var archive = await TarArchive.CreateAsyncArchive(); + var entry = await archive.AddEntryAsync("test.txt", source, size: source.Length); + source.Position = source.Length; + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + await Assert.ThrowsAsync(async () => + await entry.OpenEntryStreamAsync(cancellation.Token) + ); + Assert.Equal(source.Length, source.Position); + } + + [Fact] + public async ValueTask Tar_New_Directory_OpenEntryStreamAsync() + { + await using var archive = await TarArchive.CreateAsyncArchive(); + var entry = await archive.AddDirectoryEntryAsync("directory"); + using var opened = await entry.OpenEntryStreamAsync(CancellationToken.None); + Assert.Equal(0, opened.Length); + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask Tar_Save_New_Entry_Async_Only_Streams() + { + using var source = new AsyncOnlyStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + using var destination = new MemoryStream(); + using var output = new AsyncOnlyStream(destination, disposeStream: false); + await using (var archive = await TarArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync("test.txt", source, size: source.Length); + await archive.SaveToAsync( + output, + new TarWriterOptions(CompressionType.None, finalizeArchiveOnClose: true) + { + LeaveStreamOpen = true, + }, + CancellationToken.None + ); + } + destination.Position = 0; + await using var reopened = await TarArchive.OpenAsyncArchive(destination); + var entry = await reopened.EntriesAsync.SingleAsync(); + await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + using var reader = new StreamReader(stream); + Assert.Equal("test", await reader.ReadToEndAsync()); + } +#endif + [Fact] public async ValueTask Tar_Random_Write_Add_Async() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index 420ce481..268e70db 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using SharpCompress; using SharpCompress.Archives; @@ -208,6 +209,87 @@ public class ZipArchiveAsyncTests : ArchiveTests Assert.True(entryStream.IsDisposed); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask Zip_New_Entry_OpenEntryStreamAsync_Rewinds_And_Preserves_Ownership( + bool closeStream + ) + { + var bytes = Encoding.UTF8.GetBytes("test"); + using var source = new TestStream(new MemoryStream(bytes)); + await using (var archive = await ZipArchive.CreateAsyncArchive()) + { + IArchiveEntry entry = await archive.AddEntryAsync( + "test.txt", + source, + closeStream: closeStream, + size: source.Length + ); + for (var i = 0; i < 2; i++) + { + source.Position = source.Length; + using (var opened = await entry.OpenEntryStreamAsync(CancellationToken.None)) + { + using var destination = new MemoryStream(); + await opened.CopyToAsync(destination, 81920, CancellationToken.None); + Assert.Equal(bytes, destination.ToArray()); + } + Assert.False(source.IsDisposed); + } + } + Assert.Equal(closeStream, source.IsDisposed); + } + + [Fact] + public async ValueTask Zip_New_Entry_OpenEntryStreamAsync_Cancelled_Does_Not_Rewind() + { + using var source = new MemoryStream(Encoding.UTF8.GetBytes("test")); + await using var archive = await ZipArchive.CreateAsyncArchive(); + var entry = await archive.AddEntryAsync("test.txt", source, size: source.Length); + source.Position = source.Length; + using var cancellation = new CancellationTokenSource(); + cancellation.Cancel(); + await Assert.ThrowsAsync(async () => + await entry.OpenEntryStreamAsync(cancellation.Token) + ); + Assert.Equal(source.Length, source.Position); + } + + [Fact] + public async ValueTask Zip_New_Directory_OpenEntryStreamAsync() + { + await using var archive = await ZipArchive.CreateAsyncArchive(); + var entry = await archive.AddDirectoryEntryAsync("directory"); + using var opened = await entry.OpenEntryStreamAsync(CancellationToken.None); + Assert.Equal(0, opened.Length); + } + +#if !LEGACY_DOTNET + [Fact] + public async ValueTask Zip_Save_New_Entry_Async_Only_Streams() + { + using var source = new AsyncOnlyStream(new MemoryStream(Encoding.UTF8.GetBytes("test"))); + using var destination = new MemoryStream(); + using var output = new AsyncOnlyStream(destination, disposeStream: false); + await using (var archive = await ZipArchive.CreateAsyncArchive()) + { + await archive.AddEntryAsync("test.txt", source, size: source.Length); + await archive.SaveToAsync( + output, + new ZipWriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true }, + CancellationToken.None + ); + } + destination.Position = 0; + await using var reopened = await ZipArchive.OpenAsyncArchive(destination); + var entry = await reopened.EntriesAsync.SingleAsync(); + await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + using var reader = new StreamReader(stream); + Assert.Equal("test", await reader.ReadToEndAsync()); + } +#endif + [Fact] public async ValueTask Zip_Deflate_Entry_Stream_Async() { From d431c80b38a8f87cc72246d2b26f1a107df92f31 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 12:23:24 +0100 Subject: [PATCH 3/5] Fix WinZip AES authentication retry after cancellation --- docs/API.md | 2 +- docs/FORMATS.md | 2 +- .../Common/Zip/WinzipAesCryptoStream.Async.cs | 32 ++++- .../Common/Zip/WinzipAesCryptoStream.cs | 30 ++++- .../Streams/WinzipAesCryptoStreamTests.cs | 123 ++++++++++++++++++ 5 files changed, 177 insertions(+), 12 deletions(-) diff --git a/docs/API.md b/docs/API.md index e4f14776..b6df93d9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -107,7 +107,7 @@ The stream overloads of `DetectArchive` and `InspectArchive` preserve the suppli Stored (compression method 0) comments are supported up to 16 MiB of unpacked data; larger or unknown-size comments throw `InvalidFormatException`. Comments using other compression methods are skipped without affecting entry extraction. -Truncated encrypted comment data throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads. RAR5 KDF counts above 24 are rejected with `CryptographicException` before deriving encryption keys. +Truncated encrypted comment data throws `InvalidFormatException` on both synchronous and asynchronous reads. RAR5 KDF counts above 24 are rejected with `CryptographicException` before deriving encryption keys. ```csharp using var archive = RarArchive.OpenArchive( diff --git a/docs/FORMATS.md b/docs/FORMATS.md index ab6c88f3..545706a9 100644 --- a/docs/FORMATS.md +++ b/docs/FORMATS.md @@ -45,7 +45,7 @@ - RAR5 encrypted headers and data accept KDF counts from 0 through 24 (the binary logarithm of the PBKDF2 iteration count), matching UnRAR's acceptance limit. Larger counts are rejected before key derivation. - RAR companion filenames use the same exact-match-first, case-insensitive fallback as ZIP, with ambiguous fallback matches rejected. -- Encrypted ciphertext must contain complete AES blocks. Truncation throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads. +- Encrypted ciphertext must contain complete AES blocks. Truncation throws `InvalidFormatException` on both synchronous and asynchronous reads. ### 7Zip Format Notes diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index 2ee0b2ea..e9f31b60 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -44,17 +44,37 @@ internal partial class WinzipAesCryptoStream { return; } - _authRead = true; - var tag = new byte[10]; try { - await _stream - .ReadExactAsync(tag, 0, tag.Length, cancellationToken) - .ConfigureAwait(false); - ValidateAuthentication(tag); + while (_authenticationTagBytesRead < _authenticationTag.Length) + { + var read = await _stream + .ReadAsync( + _authenticationTag, + _authenticationTagBytesRead, + _authenticationTag.Length - _authenticationTagBytesRead, + cancellationToken + ) + .ConfigureAwait(false); + if (read == 0) + { + throw new IncompleteArchiveException( + "Unexpected end of WinZip AES authentication tag." + ); + } + _authenticationTagBytesRead += read; + } + ValidateAuthentication(_authenticationTag); + _authRead = true; + } + catch (OperationCanceledException) + { + // Cancellation is transient; keep the tag prefix so disposal can finish verification. + throw; } catch (Exception exception) { + _authRead = true; _authenticationFailure = ExceptionDispatchInfo.Capture(exception); throw; } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 1ec17808..262e1183 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -15,6 +15,10 @@ internal partial class WinzipAesCryptoStream : Stream private readonly ICryptoTransform _transform; private readonly HMACSHA1 _authentication; private bool _authRead; + + // Retain tag progress across canceled reads, including a subsequent synchronous disposal. + private readonly byte[] _authenticationTag = new byte[10]; + private int _authenticationTagBytesRead; private ExceptionDispatchInfo? _authenticationFailure; private int _nonce = 1; private byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES]; @@ -118,15 +122,33 @@ internal partial class WinzipAesCryptoStream : Stream { return; } - _authRead = true; - var tag = new byte[10]; try { - _stream.ReadExact(tag, 0, tag.Length); - ValidateAuthentication(tag); + while (_authenticationTagBytesRead < _authenticationTag.Length) + { + var read = _stream.Read( + _authenticationTag, + _authenticationTagBytesRead, + _authenticationTag.Length - _authenticationTagBytesRead + ); + if (read == 0) + { + throw new IncompleteArchiveException( + "Unexpected end of WinZip AES authentication tag." + ); + } + _authenticationTagBytesRead += read; + } + ValidateAuthentication(_authenticationTag); + _authRead = true; + } + catch (OperationCanceledException) + { + throw; } catch (Exception exception) { + _authRead = true; _authenticationFailure = ExceptionDispatchInfo.Capture(exception); throw; } diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs index 8b879981..ecbcba77 100644 --- a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -1,5 +1,6 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -14,6 +15,128 @@ namespace SharpCompress.Test.Streams; public class WinzipAesCryptoStreamTests { + public static IEnumerable AuthenticationCancellationCases() + { + foreach (var length in new[] { 0, 33 }) + { + foreach (var tagBytesRead in new[] { 0, 4 }) + { + foreach (var corruption in new[] { "none", "tag", "missing-tag" }) + { + foreach (var completion in new[] { "dispose", "retry", "dispose-async" }) + { +#if LEGACY_DOTNET + if (completion == "dispose-async") + { + continue; + } +#endif + yield return [length, tagBytesRead, corruption, completion]; + } + } + } + } + } + + [Theory] + [MemberData(nameof(AuthenticationCancellationCases))] + public async Task Authentication_Resumes_After_Cancelled_Tag_Read( + int length, + int tagBytesRead, + string corruption, + string completion + ) + { + byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87]; + var data = CreateEncryptionData("password", salt); + var ciphertext = EncryptCtr(new byte[length], data.KeyBytes); +#pragma warning disable CA5350 // WinZip AES mandates HMAC-SHA1-80. + using var authentication = new HMACSHA1(data.IvBytes); +#pragma warning restore CA5350 + var bytes = ciphertext.Concat(authentication.ComputeHash(ciphertext).Take(10)).ToArray(); + if (corruption == "tag") + { + // Corrupt the prefix, which must survive cancellation and be verified on retry. + bytes[length] ^= 1; + } + else if (corruption == "missing-tag") + { + bytes = bytes.Take(bytes.Length - 1).ToArray(); + } + using var cancellation = new CancellationTokenSource(); + using var input = new CancelTagReadStream(bytes, length + tagBytesRead, cancellation); + using var stream = new WinzipAesCryptoStream(input, data, length); + await Assert.ThrowsAnyAsync(async () => + await stream.ReadAsync( + new byte[Math.Max(1, length)], + 0, + Math.Max(1, length), + cancellation.Token + ) + ); + Assert.Equal(length + tagBytesRead, input.Position); + + var error = await Record.ExceptionAsync(async () => + { + if (completion == "dispose") + { + stream.Dispose(); + } +#if !LEGACY_DOTNET + else if (completion == "dispose-async") + { + await stream.DisposeAsync(); + } +#endif + else + { + Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)); + } + }); + if (corruption == "none") + { + Assert.Null(error); + } + else + { + Assert.Equal( + corruption == "tag" + ? typeof(SharpCompress.Common.InvalidFormatException) + : typeof(SharpCompress.Common.IncompleteArchiveException), + error?.GetType() + ); + } + } + + private sealed class CancelTagReadStream( + byte[] bytes, + int cancelPosition, + CancellationTokenSource cancellation + ) : MemoryStream(bytes) + { + private bool cancelled; + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (!cancelled) + { + if (Position == cancelPosition) + { + cancelled = true; + cancellation.Cancel(); + return Task.FromCanceled(cancellation.Token); + } + count = Math.Min(count, (int)(cancelPosition - Position)); + } + return base.ReadAsync(buffer, offset, count, cancellationToken); + } + } + [Fact] public void Dispose_Verifies_Unread_Ciphertext() { From 3bbe0cc93f04f41bc1e353cbb87dc775b4acf68d Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 12:41:38 +0100 Subject: [PATCH 4/5] Use STREAM_DISPOSEASYNC capability guards for stream disposal --- src/SharpCompress/Archives/GZip/GZipArchive.Async.cs | 8 ++++---- src/SharpCompress/Archives/IArchiveEntryExtensions.cs | 4 ++-- .../Archives/IWritableAsyncArchiveExtensions.cs | 2 +- src/SharpCompress/Archives/Tar/TarArchive.Async.cs | 6 +++--- src/SharpCompress/Archives/Zip/ZipArchive.Async.cs | 6 +++--- src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs | 2 +- .../Common/SevenZip/SevenZipStreamsCompressor.cs | 4 ++-- src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs | 2 +- src/SharpCompress/Common/Volume.Async.cs | 2 +- .../Common/Zip/WinzipAesCryptoStream.Async.cs | 2 +- src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs | 2 +- src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs | 4 ++-- .../Compressors/BZip2/CBZip2OutputStream.Async.cs | 4 ++-- .../Compressors/Deflate/DeflateStream.Async.cs | 4 ++-- src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs | 4 ++-- src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs | 4 ++-- src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs | 2 +- src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs | 4 ++-- src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs | 4 ++-- src/SharpCompress/Compressors/PPMd/PpmdStream.cs | 6 +++--- .../Compressors/ZStandard/CompressionStream.Async.cs | 6 +++--- .../Compressors/ZStandard/DecompressionStream.cs | 2 +- src/SharpCompress/IO/AsyncDisposableStream.cs | 4 ++-- src/SharpCompress/IO/ProgressReportingStream.Async.cs | 2 +- src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs | 2 ++ src/SharpCompress/IO/SharpCompressStream.Async.cs | 2 +- src/SharpCompress/Readers/AbstractReader.Async.cs | 4 ++-- src/SharpCompress/Readers/IAsyncReaderExtensions.cs | 6 +++--- src/SharpCompress/SharpCompress.csproj | 4 ++-- src/SharpCompress/Utility.Async.cs | 2 +- src/SharpCompress/Writers/IWriterExtensions.cs | 2 +- src/SharpCompress/Writers/Zip/ZipWriter.Async.cs | 2 +- tests/SharpCompress.Test/SharpCompress.Test.csproj | 3 +++ .../SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs | 2 +- .../Streams/SharpCompressLifetimeTests.cs | 2 +- .../Streams/SharpCompressStreamEdgeAsyncTest.cs | 2 +- .../Streams/SharpCompressStreamErrorAsyncTest.cs | 2 +- .../Streams/SharpCompressStreamPassthroughAsyncTest.cs | 2 +- .../Streams/WinzipAesCryptoStreamTests.cs | 4 ++-- tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs | 2 +- tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs | 2 +- 41 files changed, 70 insertions(+), 65 deletions(-) diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 95f01ae9..3b6ee24d 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -25,7 +25,7 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); #else await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); @@ -47,7 +47,7 @@ public partial class GZipArchive { throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); } -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var writer = new GZipWriter(stream, options); #else await using var writer = new GZipWriter(stream, options); @@ -58,7 +58,7 @@ public partial class GZipArchive { if (!entry.IsDirectory) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry #else await using var entryStream = await entry @@ -77,7 +77,7 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry #else await using var entryStream = await entry diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 617ac124..98ab3115 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -77,7 +77,7 @@ public static partial class IArchiveEntryExtensions var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStreamScope = entryStream; #else await using var entryStreamScope = entryStream @@ -168,7 +168,7 @@ public static partial class IArchiveEntryExtensions options, async (x, fm, ct) => { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var fs = File.Open(x, fm); #else await using var fs = File.Open(x, fm); diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index c3527f7c..7e32f1d2 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -83,7 +83,7 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); #else await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index 9bf95909..a318ab53 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -22,7 +22,7 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var writer = new TarWriter(stream, options); #else await using var writer = new TarWriter(stream, options); @@ -43,7 +43,7 @@ public partial class TarArchive } else { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry #else await using var entryStream = await entry @@ -75,7 +75,7 @@ public partial class TarArchive } else { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index 79d036cc..a65719a0 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -84,7 +84,7 @@ public partial class ZipArchive CancellationToken cancellationToken = default ) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var writer = new ZipWriter(stream, options); #else await using var writer = new ZipWriter(stream, options); @@ -105,7 +105,7 @@ public partial class ZipArchive } else { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry #else await using var entryStream = await entry @@ -135,7 +135,7 @@ public partial class ZipArchive } else { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await entry #else await using var entryStream = await entry diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs index 77cf4476..5aae4c3c 100644 --- a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs @@ -122,7 +122,7 @@ internal partial class MarkHeader { if (!leaveStreamOpen) { -#if LEGACY_DOTNET && !NETSTANDARD2_1 +#if !STREAM_DISPOSEASYNC stream.Dispose(); #else await stream.DisposeAsync().ConfigureAwait(false); diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs index dca6fd19..e769ea23 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs @@ -122,7 +122,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc2; long inputSize2; { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var lzma2Stream = new Lzma2EncoderStream( #else await using var lzma2Stream = new Lzma2EncoderStream( @@ -156,7 +156,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc; long inputSize; { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); #else await using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index c4dc0b72..ced9ea27 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -34,7 +34,7 @@ internal class TarReadOnlySubStream : Stream base.Dispose(disposing); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC public override async System.Threading.Tasks.ValueTask DisposeAsync() { if (_isDisposed) diff --git a/src/SharpCompress/Common/Volume.Async.cs b/src/SharpCompress/Common/Volume.Async.cs index 3a07aece..55d9b686 100644 --- a/src/SharpCompress/Common/Volume.Async.cs +++ b/src/SharpCompress/Common/Volume.Async.cs @@ -7,7 +7,7 @@ public abstract partial class Volume { public virtual async ValueTask DisposeAsync() { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC _actualStream.Dispose(); await Task.CompletedTask.ConfigureAwait(false); #else diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index e9f31b60..e9867989 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -10,7 +10,7 @@ namespace SharpCompress.Common.Zip; internal partial class WinzipAesCryptoStream { -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (_isDisposed) diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 262e1183..06b354af 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -87,7 +87,7 @@ internal partial class WinzipAesCryptoStream : Stream { if (!_authRead) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC // Legacy Stream lacks DisposeAsync, including for async-only inputs. #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits #pragma warning disable CA2012 diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs index 35cf9f1d..c7b33645 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs @@ -115,7 +115,7 @@ public sealed partial class BZip2Stream : IAsyncDisposable CancellationToken cancellationToken = default ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -136,7 +136,7 @@ public sealed partial class BZip2Stream : IAsyncDisposable stream.Dispose(); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs index d56e9244..6db68c8f 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.Async.cs @@ -670,7 +670,7 @@ internal sealed partial class CBZip2OutputStream : IAsyncDisposable await bsStream.FlushAsync(cancellationToken).ConfigureAwait(false); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -696,7 +696,7 @@ internal sealed partial class CBZip2OutputStream : IAsyncDisposable } bsStream = null; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs index ae6343cd..496adca2 100644 --- a/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/DeflateStream.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Compressors.Deflate; public partial class DeflateStream { -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -22,7 +22,7 @@ public partial class DeflateStream } _disposed = true; } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #else await Task.CompletedTask.ConfigureAwait(false); diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs index 4a70c519..db8bfbcc 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.Async.cs @@ -183,7 +183,7 @@ public partial class GZipStream } #endif -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -200,7 +200,7 @@ public partial class GZipStream await BaseStream.DisposeAsync().ConfigureAwait(false); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index adb462b1..b574e74c 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -510,7 +510,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -521,7 +521,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack return; } isDisposed = true; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif if (_stream is null) diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs index 058bd28e..8e433214 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs @@ -21,7 +21,7 @@ public partial class ZlibStream await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (_disposed) diff --git a/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs index 7440ff66..2790b7c5 100644 --- a/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs @@ -200,7 +200,7 @@ internal sealed class Lzma2EncoderStream : Stream private static readonly byte[] _endMarker = [0x00]; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -229,7 +229,7 @@ internal sealed class Lzma2EncoderStream : Stream } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs index 1c87d5e1..0c23a259 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -454,7 +454,7 @@ public partial class LzmaStream } #endif -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -494,7 +494,7 @@ public partial class LzmaStream } } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif GC.SuppressFinalize(this); diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index 744682c4..d6a97a61 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -160,7 +160,7 @@ public class PpmdStream : Stream, IAsyncDisposable } catch { -#if LEGACY_DOTNET && !NETSTANDARD2_1 +#if !STREAM_DISPOSEASYNC instance.Dispose(); #else await instance.DisposeAsync().ConfigureAwait(false); @@ -196,7 +196,7 @@ public class PpmdStream : Stream, IAsyncDisposable base.Dispose(disposing); } -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -215,7 +215,7 @@ public class PpmdStream : Stream, IAsyncDisposable _modelH?.Dispose(); _modelH = null; -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); #endif } diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs index 5f988e4c..d788d152 100644 --- a/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs +++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.Async.cs @@ -8,7 +8,7 @@ namespace SharpCompress.Compressors.ZStandard; public partial class CompressionStream { -#if !LEGACY_DOTNET || NETSTANDARD2_1 +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() #else public async ValueTask DisposeAsync() @@ -16,7 +16,7 @@ public partial class CompressionStream { if (compressor == null) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC Dispose(true); GC.SuppressFinalize(this); await Task.CompletedTask.ConfigureAwait(false); @@ -35,7 +35,7 @@ public partial class CompressionStream ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC Dispose(true); await Task.CompletedTask.ConfigureAwait(false); #else diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs index 20490d91..c4161fd2 100644 --- a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs +++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs @@ -203,7 +203,7 @@ public partial class DecompressionStream : Stream } } -#if LEGACY_DOTNET && !NETSTANDARD2_1 +#if !STREAM_DISPOSEASYNC public virtual ValueTask DisposeAsync() { try diff --git a/src/SharpCompress/IO/AsyncDisposableStream.cs b/src/SharpCompress/IO/AsyncDisposableStream.cs index 4c7fafbe..e528b4ce 100644 --- a/src/SharpCompress/IO/AsyncDisposableStream.cs +++ b/src/SharpCompress/IO/AsyncDisposableStream.cs @@ -22,11 +22,11 @@ namespace SharpCompress.IO; /// /// public abstract class AsyncDisposableStream : Stream -#if NO_STREAM_DISPOSEASYNC +#if !STREAM_DISPOSEASYNC , IAsyncDisposable #endif { -#if NO_STREAM_DISPOSEASYNC +#if !STREAM_DISPOSEASYNC public virtual ValueTask DisposeAsync() { Dispose(); diff --git a/src/SharpCompress/IO/ProgressReportingStream.Async.cs b/src/SharpCompress/IO/ProgressReportingStream.Async.cs index fd10398b..449990f5 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.Async.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.Async.cs @@ -45,7 +45,7 @@ internal sealed partial class ProgressReportingStream } #endif -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (!_leaveOpen) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs index 1bac16de..dd082d39 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -28,7 +28,9 @@ internal sealed partial class SeekableSharpCompressStream ReadOnlyMemory buffer, CancellationToken cancellationToken = default ) => _stream.WriteAsync(buffer, cancellationToken); +#endif +#if STREAM_DISPOSEASYNC public override ValueTask DisposeAsync() { if (_isDisposed) diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index 7b1f376d..2b2057d7 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -251,7 +251,7 @@ public partial class SharpCompressStream } } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (!isDisposed) diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 01811afa..7b75cac2 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,7 +104,7 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); #else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); @@ -138,7 +138,7 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); #else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 989026be..12473db3 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -45,7 +45,7 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var fs = File.Open(x, fm); #else await using var fs = File.Open(x, fm); @@ -87,7 +87,7 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var fs = File.Open(x, fm); #else await using var fs = File.Open(x, fm); @@ -116,7 +116,7 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var entryStream = await reader #else await using var entryStream = await reader diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index c57cb722..d5ba7f80 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -31,8 +31,8 @@ $(DefineConstants);LEGACY_DOTNET - - $(DefineConstants);NO_STREAM_DISPOSEASYNC + + $(DefineConstants);STREAM_DISPOSEASYNC true diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index a23a88f3..7d1546c5 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -70,7 +70,7 @@ internal static partial class Utility ) { // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); #else await using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index 8e37a5b9..d6c94ea0 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -77,7 +77,7 @@ public static class IWriterExtensions { throw new ArgumentException("Source does not exist: " + source.FullName); } -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var stream = source.OpenRead(); #else await using var stream = source.OpenRead(); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs index 2956cc4f..0f3bea53 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -83,7 +83,7 @@ public partial class ZipWriter #if !SYNC_ONLY cancellationToken.ThrowIfCancellationRequested(); #endif -#if LEGACY_DOTNET +#if !STREAM_DISPOSEASYNC using var output = await WriteToStreamAsync( #else await using var output = await WriteToStreamAsync( diff --git a/tests/SharpCompress.Test/SharpCompress.Test.csproj b/tests/SharpCompress.Test/SharpCompress.Test.csproj index 68ff64ae..a8769f29 100644 --- a/tests/SharpCompress.Test/SharpCompress.Test.csproj +++ b/tests/SharpCompress.Test/SharpCompress.Test.csproj @@ -12,6 +12,9 @@ AnyCPU false + + $(DefineConstants);STREAM_DISPOSEASYNC + $(DefineConstants);WINDOWS diff --git a/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs b/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs index dd26ba01..a0569d92 100644 --- a/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs +++ b/tests/SharpCompress.Test/Streams/BufferPoolLifetimeTests.cs @@ -58,7 +58,7 @@ public class BufferPoolLifetimeTests stream.Dispose(); } stream.Dispose(); -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC await stream.DisposeAsync(); #else await Task.CompletedTask; diff --git a/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs index df339154..b38ca2da 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressLifetimeTests.cs @@ -25,7 +25,7 @@ public class SharpCompressLifetimeTests Assert.NotNull(ring.GetValue(stream)); if (asyncDispose) { -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC await Assert.ThrowsAsync(async () => await stream.DisposeAsync()); #else Assert.Throws(() => stream.Dispose()); diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs index 1c3b7d18..c2221d10 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamEdgeAsyncTest.cs @@ -9,7 +9,7 @@ namespace SharpCompress.Test.Streams; public class SharpCompressStreamEdgeAsyncTest { -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask DisposeAsync_WithLeaveStreamOpenTrue_DoesNotDisposeUnderlying() diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs index cf90a566..38020859 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamErrorAsyncTest.cs @@ -49,7 +49,7 @@ public class SharpCompressStreamErrorAsyncTest } } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask DisposeAsync_WithThrowOnDisposeTrue_ThrowsInvalidOperation() { diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs index 521bac6a..862bd442 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamPassthroughAsyncTest.cs @@ -84,7 +84,7 @@ public class SharpCompressStreamPassthroughAsyncTest Assert.Equal(3, ms.Length); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask CreateNonDisposing_DoesNotDisposeUnderlying_Async() { diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs index ecbcba77..861eaf20 100644 --- a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -82,7 +82,7 @@ public class WinzipAesCryptoStreamTests { stream.Dispose(); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC else if (completion == "dispose-async") { await stream.DisposeAsync(); @@ -153,7 +153,7 @@ public class WinzipAesCryptoStreamTests stream.Dispose(); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async Task DisposeAsync_Verifies_Unread_Ciphertext() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index ea206009..afa668ed 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -286,7 +286,7 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Tar_Save_New_Entry_Async_Only_Streams() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index 268e70db..be8c0bc8 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -265,7 +265,7 @@ public class ZipArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if !LEGACY_DOTNET +#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Zip_Save_New_Entry_Async_Only_Streams() { From e58fcb23e5de31221fd048c239b17044900f6daa Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 13 Oct 2026 12:57:14 +0100 Subject: [PATCH 5/5] Preserve async disposal across all supported frameworks --- docs/API.md | 2 + .../Archives/GZip/GZipArchive.Async.cs | 31 ++++++----- .../Archives/IArchiveEntryExtensions.cs | 9 ++-- .../IWritableAsyncArchiveExtensions.cs | 7 +-- .../Archives/Tar/TarArchive.Async.cs | 22 +++----- .../Archives/Zip/ZipArchive.Async.cs | 22 +++----- .../Common/Rar/Headers/MarkHeader.Async.cs | 6 +-- .../SevenZip/SevenZipStreamsCompressor.cs | 8 --- .../Common/Tar/TarReadOnlySubStream.cs | 5 +- src/SharpCompress/Common/Volume.Async.cs | 7 +-- .../Common/Zip/WinzipAesCryptoStream.Async.cs | 4 +- .../Common/Zip/WinzipAesCryptoStream.cs | 3 +- .../Compressors/Deflate/ZlibStream.Async.cs | 9 +++- .../Compressors/Deflate/ZlibStream.cs | 3 ++ .../Compressors/PPMd/PpmdStream.cs | 4 -- .../IO/ProgressReportingStream.Async.cs | 4 +- .../IO/ProgressReportingStream.cs | 2 +- .../IO/SeekableSharpCompressStream.Async.cs | 11 ++-- .../IO/SharpCompressStream.Async.cs | 11 +++- src/SharpCompress/IO/SharpCompressStream.cs | 3 ++ .../Readers/AbstractReader.Async.cs | 8 --- .../Readers/IAsyncReaderExtensions.cs | 18 ++----- src/SharpCompress/Utility.Async.cs | 7 +-- .../Writers/IWriterExtensions.cs | 7 +-- .../Writers/Zip/ZipWriter.Async.cs | 4 -- .../AsyncDisposalCompatibilityTests.cs | 53 +++++++++++++++++++ .../Streams/WinzipAesCryptoStreamTests.cs | 10 ---- .../Tar/TarArchiveAsyncTests.cs | 5 +- .../Zip/ZipArchiveAsyncTests.cs | 5 +- 29 files changed, 141 insertions(+), 149 deletions(-) create mode 100644 tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs diff --git a/docs/API.md b/docs/API.md index b6df93d9..295feeda 100644 --- a/docs/API.md +++ b/docs/API.md @@ -694,6 +694,8 @@ using (var archive = ZipArchive.OpenArchive("archive.zip", options)) ### Async Extract with Cancellation +Async archive and reader/writer operations dispose SharpCompress resources asynchronously on every target, including .NET Framework 4.8 and .NET Standard 2.0. `SharpCompressStream.DisposeAsync()` and `ZlibStream.DisposeAsync()` are available on every target; stream wrappers forward disposal to runtime `IAsyncDisposable` implementations where available, falling back to synchronous disposal only for streams that do not support it. + ```csharp var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromMinutes(5)); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index 3b6ee24d..032e33f3 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -25,10 +25,11 @@ public partial class GZipArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC +#if SYNC_ONLY using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); #else - await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); #endif await SaveToAsync(stream, new GZipWriterOptions(CompressionType.GZip), cancellationToken) .ConfigureAwait(false); @@ -47,24 +48,23 @@ public partial class GZipArchive { throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); } -#if !STREAM_DISPOSEASYNC - using var writer = new GZipWriter(stream, options); -#else await using var writer = new GZipWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) { if (!entry.IsDirectory) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); #else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); +#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -77,13 +77,16 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry +#if SYNC_ONLY + using var entryStream = entry.OpenEntryStream(); #else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); +#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 98ab3115..b4d858f2 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -77,13 +77,9 @@ public static partial class IArchiveEntryExtensions var entryStream = await archiveEntry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#if !STREAM_DISPOSEASYNC - using var entryStreamScope = entryStream; -#else await using var entryStreamScope = entryStream .DisposeAsyncScope() .ConfigureAwait(false); -#endif #endif var checkedStream = options is null ? entryStream @@ -168,10 +164,11 @@ public static partial class IArchiveEntryExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC +#if SYNC_ONLY using var fs = File.Open(x, fm); #else - await using var fs = File.Open(x, fm); + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); #endif await entry .WriteToAsync(fs, options.BufferSize, options, null, ct) diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 7e32f1d2..d74b756b 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -83,11 +83,8 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions { -#if !STREAM_DISPOSEASYNC - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); -#else - await using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); -#endif + var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index a318ab53..2c5e69e0 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -22,11 +22,7 @@ public partial class TarArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC - using var writer = new TarWriter(stream, options); -#else await using var writer = new TarWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -43,13 +39,12 @@ public partial class TarArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -75,15 +70,12 @@ public partial class TarArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); -#else - await using var entryStream = await entry - .OpenEntryStreamAsync(cancellationToken) + await using var entryStreamScope = entryStream + .DisposeAsyncScope() .ConfigureAwait(false); -#endif await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index a65719a0..122112eb 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -84,11 +84,7 @@ public partial class ZipArchive CancellationToken cancellationToken = default ) { -#if !STREAM_DISPOSEASYNC - using var writer = new ZipWriter(stream, options); -#else await using var writer = new ZipWriter(stream, options); -#endif await foreach ( var entry in oldEntries.WithCancellation(cancellationToken).ConfigureAwait(false) ) @@ -105,13 +101,12 @@ public partial class ZipArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -135,13 +130,12 @@ public partial class ZipArchive } else { -#if !STREAM_DISPOSEASYNC - using var entryStream = await entry -#else - await using var entryStream = await entry -#endif + var entryStream = await entry .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); + await using var entryStreamScope = entryStream + .DisposeAsyncScope() + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), diff --git a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs index 5aae4c3c..52b0ad4f 100644 --- a/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/MarkHeader.Async.cs @@ -122,11 +122,7 @@ internal partial class MarkHeader { if (!leaveStreamOpen) { -#if !STREAM_DISPOSEASYNC - stream.Dispose(); -#else - await stream.DisposeAsync().ConfigureAwait(false); -#endif + await stream.DisposeAsyncCompat().ConfigureAwait(false); } throw new InvalidFormatException("Error trying to read rar signature.", e); } diff --git a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs index e769ea23..1b704e0c 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipStreamsCompressor.cs @@ -122,11 +122,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc2; long inputSize2; { -#if !STREAM_DISPOSEASYNC - using var lzma2Stream = new Lzma2EncoderStream( -#else await using var lzma2Stream = new Lzma2EncoderStream( -#endif outCrcStream, encoderProperties.DictionarySize, encoderProperties.NumFastBytes @@ -156,11 +152,7 @@ internal sealed class SevenZipStreamsCompressor(Stream outputStream) uint inputCrc; long inputSize; { -#if !STREAM_DISPOSEASYNC - using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); -#else await using var lzmaStream = LzmaStream.Create(encoderProperties, false, outCrcStream); -#endif properties = lzmaStream.Properties; (inputCrc, inputSize) = await CopyWithCrcAsync( diff --git a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs index ced9ea27..1d0608a2 100644 --- a/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs +++ b/src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs @@ -1,10 +1,11 @@ using System; using System.IO; using System.Threading.Tasks; +using SharpCompress.IO; namespace SharpCompress.Common.Tar; -internal class TarReadOnlySubStream : Stream +internal class TarReadOnlySubStream : AsyncDisposableStream { private readonly Stream _stream; @@ -34,7 +35,6 @@ internal class TarReadOnlySubStream : Stream base.Dispose(disposing); } -#if STREAM_DISPOSEASYNC public override async System.Threading.Tasks.ValueTask DisposeAsync() { if (_isDisposed) @@ -49,7 +49,6 @@ internal class TarReadOnlySubStream : Stream GC.SuppressFinalize(this); await base.DisposeAsync().ConfigureAwait(false); } -#endif private long BytesLeftToRead { get; set; } diff --git a/src/SharpCompress/Common/Volume.Async.cs b/src/SharpCompress/Common/Volume.Async.cs index 55d9b686..05984162 100644 --- a/src/SharpCompress/Common/Volume.Async.cs +++ b/src/SharpCompress/Common/Volume.Async.cs @@ -7,12 +7,7 @@ public abstract partial class Volume { public virtual async ValueTask DisposeAsync() { -#if !STREAM_DISPOSEASYNC - _actualStream.Dispose(); - await Task.CompletedTask.ConfigureAwait(false); -#else - await _actualStream.DisposeAsync().ConfigureAwait(false); -#endif + await _actualStream.DisposeAsyncCompat().ConfigureAwait(false); GC.SuppressFinalize(this); } } diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs index e9867989..242b8515 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -10,7 +10,6 @@ namespace SharpCompress.Common.Zip; internal partial class WinzipAesCryptoStream { -#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (_isDisposed) @@ -31,11 +30,10 @@ internal partial class WinzipAesCryptoStream _authentication.Dispose(); _transform.Dispose(); _cipher.Dispose(); - await _stream.DisposeAsync().ConfigureAwait(false); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } await base.DisposeAsync().ConfigureAwait(false); } -#endif private async ValueTask VerifyAuthenticationAsync(CancellationToken cancellationToken) { diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 06b354af..b96ca1eb 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -3,10 +3,11 @@ using System.Buffers.Binary; using System.IO; using System.Runtime.ExceptionServices; using System.Security.Cryptography; +using SharpCompress.IO; namespace SharpCompress.Common.Zip; -internal partial class WinzipAesCryptoStream : Stream +internal partial class WinzipAesCryptoStream : AsyncDisposableStream { private const int BLOCK_SIZE_IN_BYTES = 16; private readonly Aes _cipher; diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs index 8e433214..1021bbde 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.Async.cs @@ -23,6 +23,9 @@ public partial class ZlibStream #if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() +#else + public virtual async ValueTask DisposeAsync() +#endif { if (_disposed) { @@ -33,9 +36,13 @@ public partial class ZlibStream { await _baseStream.DisposeAsync().ConfigureAwait(false); } +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); - } +#else + base.Dispose(); + GC.SuppressFinalize(this); #endif + } /// /// Read data from the stream. diff --git a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs index 580961d7..97c77538 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibStream.cs @@ -33,6 +33,9 @@ using SharpCompress.Common; namespace SharpCompress.Compressors.Deflate; public partial class ZlibStream : Stream +#if !STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif { private readonly ZlibBaseStream _baseStream; private bool _disposed; diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index d6a97a61..bcf39c90 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -160,11 +160,7 @@ public class PpmdStream : Stream, IAsyncDisposable } catch { -#if !STREAM_DISPOSEASYNC - instance.Dispose(); -#else await instance.DisposeAsync().ConfigureAwait(false); -#endif throw; } } diff --git a/src/SharpCompress/IO/ProgressReportingStream.Async.cs b/src/SharpCompress/IO/ProgressReportingStream.Async.cs index 449990f5..096c0a95 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.Async.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.Async.cs @@ -45,14 +45,12 @@ internal sealed partial class ProgressReportingStream } #endif -#if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() { if (!_leaveOpen) { - await _baseStream.DisposeAsync().ConfigureAwait(false); + await _baseStream.DisposeAsyncCompat().ConfigureAwait(false); } await base.DisposeAsync().ConfigureAwait(false); } -#endif } diff --git a/src/SharpCompress/IO/ProgressReportingStream.cs b/src/SharpCompress/IO/ProgressReportingStream.cs index 8834a546..4eb73bab 100644 --- a/src/SharpCompress/IO/ProgressReportingStream.cs +++ b/src/SharpCompress/IO/ProgressReportingStream.cs @@ -8,7 +8,7 @@ namespace SharpCompress.IO; /// A stream wrapper that reports progress as data is read from the source. /// Used to track compression or extraction progress by wrapping the source stream. /// -internal sealed partial class ProgressReportingStream : Stream +internal sealed partial class ProgressReportingStream : AsyncDisposableStream { private readonly Stream _baseStream; private readonly IProgress _progress; diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs index dd082d39..7bf2b650 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -30,12 +30,12 @@ internal sealed partial class SeekableSharpCompressStream ) => _stream.WriteAsync(buffer, cancellationToken); #endif -#if STREAM_DISPOSEASYNC - public override ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (_isDisposed) { - return base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); + return; } if (ThrowOnDispose) { @@ -46,11 +46,10 @@ internal sealed partial class SeekableSharpCompressStream _isDisposed = true; if (!LeaveStreamOpen) { - _stream.Dispose(); + await _stream.DisposeAsyncCompat().ConfigureAwait(false); } - return base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); } -#endif [Zomp.SyncMethodGenerator.CreateSyncVersion] public override Task WriteAsync( diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index 2b2057d7..47ba651e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -253,6 +253,9 @@ public partial class SharpCompressStream #if STREAM_DISPOSEASYNC public override async ValueTask DisposeAsync() +#else + public virtual async ValueTask DisposeAsync() +#endif { if (!isDisposed) { @@ -267,7 +270,7 @@ public partial class SharpCompressStream { if (!LeaveStreamOpen) { - await stream.DisposeAsync().ConfigureAwait(false); + await stream.DisposeAsyncCompat().ConfigureAwait(false); } } finally @@ -276,7 +279,11 @@ public partial class SharpCompressStream _ringBuffer = null; } } +#if STREAM_DISPOSEASYNC await base.DisposeAsync().ConfigureAwait(false); - } +#else + base.Dispose(); + GC.SuppressFinalize(this); #endif + } } diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 7a5b7777..d59c53c2 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -17,6 +17,9 @@ namespace SharpCompress.IO; /// stream and usage scenario. /// public partial class SharpCompressStream : Stream, IStreamStack +#if !STREAM_DISPOSEASYNC + , IAsyncDisposable +#endif { public virtual Stream BaseStream() => stream; diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 7b75cac2..969c73e6 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -104,11 +104,7 @@ public abstract partial class AbstractReader } } //don't know the size so we have to try to decompress to skip -#if !STREAM_DISPOSEASYNC - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#endif await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false); } @@ -138,11 +134,7 @@ public abstract partial class AbstractReader private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken) { -#if !STREAM_DISPOSEASYNC - using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#else await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false); -#endif var sourceStream = WrapWithProgress(s, Entry); await sourceStream .CopyToAsync(writeStream, Options.BufferSize, cancellationToken) diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 12473db3..c962d116 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -45,11 +45,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC - using var fs = File.Open(x, fm); -#else - await using var fs = File.Open(x, fm); -#endif + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -87,11 +84,8 @@ public static class IAsyncReaderExtensions options, async (x, fm, ct) => { -#if !STREAM_DISPOSEASYNC - using var fs = File.Open(x, fm); -#else - await using var fs = File.Open(x, fm); -#endif + var fs = File.Open(x, fm); + await using var scope = fs.DisposeAsyncScope().ConfigureAwait(false); await CopyEntryToAsync(reader, fs, options, ct).ConfigureAwait(false); }, cancellationToken @@ -116,11 +110,7 @@ public static class IAsyncReaderExtensions CancellationToken cancellationToken ) { -#if !STREAM_DISPOSEASYNC - using var entryStream = await reader -#else await using var entryStream = await reader -#endif .OpenEntryStreamAsync(cancellationToken) .ConfigureAwait(false); var checkedStream = IEntryExtensions.WrapWithChecksumValidation( diff --git a/src/SharpCompress/Utility.Async.cs b/src/SharpCompress/Utility.Async.cs index 7d1546c5..6e6debff 100644 --- a/src/SharpCompress/Utility.Async.cs +++ b/src/SharpCompress/Utility.Async.cs @@ -70,11 +70,8 @@ internal static partial class Utility ) { // Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync -#if !STREAM_DISPOSEASYNC - using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); -#else - await using var limitedStream = new IO.ReadOnlySubStream(source, maxLength); -#endif + var limitedStream = new IO.ReadOnlySubStream(source, maxLength); + await using var scope = limitedStream.DisposeAsyncScope().ConfigureAwait(false); await limitedStream .CopyToAsync(destination, bufferSize ?? Constants.BufferSize, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index d6c94ea0..b1a082f9 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -77,11 +77,8 @@ public static class IWriterExtensions { throw new ArgumentException("Source does not exist: " + source.FullName); } -#if !STREAM_DISPOSEASYNC - using var stream = source.OpenRead(); -#else - await using var stream = source.OpenRead(); -#endif + var stream = source.OpenRead(); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); await writer .WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken) .ConfigureAwait(false); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs index 0f3bea53..ec0d9ca4 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Async.cs @@ -83,11 +83,7 @@ public partial class ZipWriter #if !SYNC_ONLY cancellationToken.ThrowIfCancellationRequested(); #endif -#if !STREAM_DISPOSEASYNC - using var output = await WriteToStreamAsync( -#else await using var output = await WriteToStreamAsync( -#endif entryPath, zipWriterEntryOptions, cancellationToken diff --git a/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs b/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs new file mode 100644 index 00000000..6407eda8 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/AsyncDisposalCompatibilityTests.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class AsyncDisposalCompatibilityTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async ValueTask SharpCompressStream_Forwards_Async_Disposal(bool seekable) + { + var source = new AsyncDisposeOnlyStream(seekable); + var wrapper = SharpCompressStream.Create(source, 128); + await ((IAsyncDisposable)wrapper).DisposeAsync().ConfigureAwait(false); + Assert.True(source.AsyncDisposed); + await ((IAsyncDisposable)wrapper).DisposeAsync().ConfigureAwait(false); + } + + [Fact] + public async ValueTask Stream_Disposal_Scope_Uses_Runtime_Async_Disposable() + { + var source = new AsyncDisposeOnlyStream(true); + Stream stream = source; + await using (stream.DisposeAsyncScope().ConfigureAwait(false)) { } + Assert.True(source.AsyncDisposed); + } + + private sealed class AsyncDisposeOnlyStream(bool seekable) : AsyncOnlyStream(new MemoryStream()) + { + public override bool CanSeek => seekable; + public bool AsyncDisposed { get; private set; } + + public override ValueTask DisposeAsync() + { + AsyncDisposed = true; + return base.DisposeAsync(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !AsyncDisposed) + { + throw new NotSupportedException("Synchronous disposal is not supported."); + } + base.Dispose(disposing); + } + } +} diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs index 861eaf20..463b49f0 100644 --- a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs @@ -25,12 +25,6 @@ public class WinzipAesCryptoStreamTests { foreach (var completion in new[] { "dispose", "retry", "dispose-async" }) { -#if LEGACY_DOTNET - if (completion == "dispose-async") - { - continue; - } -#endif yield return [length, tagBytesRead, corruption, completion]; } } @@ -82,12 +76,10 @@ public class WinzipAesCryptoStreamTests { stream.Dispose(); } -#if STREAM_DISPOSEASYNC else if (completion == "dispose-async") { await stream.DisposeAsync(); } -#endif else { Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)); @@ -153,7 +145,6 @@ public class WinzipAesCryptoStreamTests stream.Dispose(); } -#if STREAM_DISPOSEASYNC [Fact] public async Task DisposeAsync_Verifies_Unread_Ciphertext() { @@ -171,7 +162,6 @@ public class WinzipAesCryptoStreamTests ); await stream.DisposeAsync(); } -#endif [Theory] [InlineData(false, 0)] diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index afa668ed..f31d6a84 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -286,7 +286,6 @@ public class TarArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Tar_Save_New_Entry_Async_Only_Streams() { @@ -308,11 +307,11 @@ public class TarArchiveAsyncTests : ArchiveTests destination.Position = 0; await using var reopened = await TarArchive.OpenAsyncArchive(destination); var entry = await reopened.EntriesAsync.SingleAsync(); - await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); using var reader = new StreamReader(stream); Assert.Equal("test", await reader.ReadToEndAsync()); } -#endif [Fact] public async ValueTask Tar_Random_Write_Add_Async() diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index be8c0bc8..710a4141 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -265,7 +265,6 @@ public class ZipArchiveAsyncTests : ArchiveTests Assert.Equal(0, opened.Length); } -#if STREAM_DISPOSEASYNC [Fact] public async ValueTask Zip_Save_New_Entry_Async_Only_Streams() { @@ -284,11 +283,11 @@ public class ZipArchiveAsyncTests : ArchiveTests destination.Position = 0; await using var reopened = await ZipArchive.OpenAsyncArchive(destination); var entry = await reopened.EntriesAsync.SingleAsync(); - await using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + var stream = await entry.OpenEntryStreamAsync(CancellationToken.None); + await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false); using var reader = new StreamReader(stream); Assert.Equal("test", await reader.ReadToEndAsync()); } -#endif [Fact] public async ValueTask Zip_Deflate_Entry_Stream_Async()