From ea44ec098b93e1e72d3a6c6ec12b25bc6d112ca1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 28 Jul 2026 11:26:51 +0100 Subject: [PATCH] first pass of sync generator --- AGENTS.md | 29 ++++++ .../Compressors/Filters/Filter.Async.cs | 2 + .../Compressors/Filters/Filter.cs | 90 ------------------- .../Compressors/Xz/Filters/ArmFilter.Async.cs | 1 + .../Compressors/Xz/Filters/ArmFilter.cs | 9 -- .../Xz/Filters/ArmThumbFilter.Async.cs | 1 + .../Compressors/Xz/Filters/ArmThumbFilter.cs | 9 -- .../Xz/Filters/IA64Filter.Async.cs | 1 + .../Compressors/Xz/Filters/IA64Filter.cs | 9 -- .../Xz/Filters/Lzma2Filter.Async.cs | 1 + .../Compressors/Xz/Filters/Lzma2Filter.cs | 3 - .../Xz/Filters/PowerPCFilter.Async.cs | 1 + .../Compressors/Xz/Filters/PowerPCFilter.cs | 9 -- .../Xz/Filters/SparcFilter.Async.cs | 1 + .../Compressors/Xz/Filters/SparcFilter.cs | 9 -- .../Compressors/Xz/Filters/X86Filter.Async.cs | 1 + .../Compressors/Xz/Filters/X86Filter.cs | 9 -- 17 files changed, 38 insertions(+), 147 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eaa3e326..9166add2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,6 +154,35 @@ SharpCompress supports multiple archive and compression formats: - `OpenEntryStreamAsync` - Open entry stream asynchronously - Always provide `CancellationToken` parameter in async methods +### Generating sync methods from async ones +Most of the library still keeps a hand-written sync twin beside each async method, usually split as +`Foo.cs` + `Foo.Async.cs`. Where the two bodies are the same logic, the sync one is generated +instead, using [Zomp.SyncMethodGenerator](https://github.com/zompinc/sync-method-generator) (already +referenced for every project via `Directory.Packages.props`). + +- Put `[Zomp.SyncMethodGenerator.CreateSyncVersion]` on the **async** method — that body is then the + single source of truth — and delete the hand-written sync twin. The generator drops the `Async` + suffix, `CancellationToken` and `IProgress` (unless `PreserveCancellationToken` / + `PreserveProgress`), maps `Task`/`ValueTask` to `void`/`T` and `Memory` to `Span`, and + rewrites `FooAsync(...)` calls to `Foo(...)`. Modifiers, including `override`/`virtual`, are kept. +- **Attribute individual methods, never the whole type.** A type-level attribute also generates the + members that must not exist (see below), so you would need more `[SkipSyncVersion]` than + `[CreateSyncVersion]`. +- Attribute a method **only when the generated signature already exists by hand.** Generating a + member that did not exist before is a behaviour change, not a deduplication — in particular, a + generated `Read(Span)` replaces `Stream`'s default rent-and-copy shim. +- Deliberately **not** generated: `Dispose`/`DisposeAsync` (on a `Stream` the generated `Dispose()` + cannot override the non-virtual `Stream.Dispose()`), `Flush`/`FlushAsync`, `CopyTo`/`CopyToAsync`, + `ReadByte`/`WriteByte` (generated without `override`, so they would hide `Stream`'s), and the + `Memory`/`ReadOnlyMemory` overloads that have no sync twin. +- Keep one XML doc comment, on the async method, phrased tense-neutrally ("Extract entry to the + specified stream.") — it is emitted onto both copies. +- Use `#if SYNC_ONLY` / `#if !SYNC_ONLY` only for a localised I/O idiom that genuinely differs; if it + would cover more than a small part of the method, keep two hand-written methods instead. +- To see what was generated, build with `-p:EmitCompilerGeneratedFiles=true` and look in + `obj///generated/Zomp.SyncMethodGenerator/`. A sync stack frame may therefore name a + method that has no file in the repo — debug it via the async source. + ### Archive APIs vs Reader/Writer APIs - **Archive API**: Use for random access with seekable streams (e.g., `ZipArchive`, `TarArchive`) - **Reader API**: Use for forward-only reading on non-seekable streams (e.g., `ZipReader`, `TarReader`) diff --git a/src/SharpCompress/Compressors/Filters/Filter.Async.cs b/src/SharpCompress/Compressors/Filters/Filter.Async.cs index 2fd03d37..29dd2f4a 100644 --- a/src/SharpCompress/Compressors/Filters/Filter.Async.cs +++ b/src/SharpCompress/Compressors/Filters/Filter.Async.cs @@ -7,6 +7,7 @@ namespace SharpCompress.Compressors.Filters; internal abstract partial class Filter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, @@ -200,6 +201,7 @@ internal abstract partial class Filter } #endif + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task WriteAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Filters/Filter.cs b/src/SharpCompress/Compressors/Filters/Filter.cs index 8841d618..3f0d2c47 100644 --- a/src/SharpCompress/Compressors/Filters/Filter.cs +++ b/src/SharpCompress/Compressors/Filters/Filter.cs @@ -50,99 +50,9 @@ internal abstract partial class Filter : Stream set => throw new NotSupportedException(); } - public override int Read(byte[] buffer, int offset, int count) - { - var size = 0; - - if (_transformed > 0) - { - var copySize = _transformed; - if (copySize > count) - { - copySize = count; - } - Buffer.BlockCopy(_tail, 0, buffer, offset, copySize); - _transformed -= copySize; - _read -= copySize; - offset += copySize; - count -= copySize; - size += copySize; - Buffer.BlockCopy(_tail, copySize, _tail, 0, _read); - } - if (count == 0) - { - return size; - } - - var inSize = _read; - if (inSize > count) - { - inSize = count; - } - Buffer.BlockCopy(_tail, 0, buffer, offset, inSize); - _read -= inSize; - Buffer.BlockCopy(_tail, inSize, _tail, 0, _read); - while (!_endReached && inSize < count) - { - var baseRead = _baseStream.Read(buffer, offset + inSize, count - inSize); - inSize += baseRead; - if (baseRead == 0) - { - _endReached = true; - } - } - while (!_endReached && _read < _tail.Length) - { - var baseRead = _baseStream.Read(_tail, _read, _tail.Length - _read); - _read += baseRead; - if (baseRead == 0) - { - _endReached = true; - } - } - - if (inSize > _tail.Length) - { - _transformed = Transform(buffer, offset, inSize); - offset += _transformed; - count -= _transformed; - size += _transformed; - inSize -= _transformed; - _transformed = 0; - } - - if (count == 0) - { - return size; - } - - Buffer.BlockCopy(buffer, offset, _window, 0, inSize); - Buffer.BlockCopy(_tail, 0, _window, inSize, _read); - if (inSize + _read > _tail.Length) - { - _transformed = Transform(_window, 0, inSize + _read); - } - else - { - _transformed = inSize + _read; - } - Buffer.BlockCopy(_window, 0, buffer, offset, inSize); - Buffer.BlockCopy(_window, inSize, _tail, 0, _read); - size += inSize; - _transformed -= inSize; - - return size; - } - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) - { - Transform(buffer, offset, count); - _baseStream.Write(buffer, offset, count); - } - protected abstract int Transform(byte[] buffer, int offset, int count); } diff --git a/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.Async.cs index 321535aa..acb6ec29 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class ArmFilter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.cs b/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.cs index 30590408..fb4e8d86 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/ArmFilter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -45,13 +44,5 @@ public partial class ArmFilter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.ARMConverter(buffer, _ip); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; } diff --git a/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.Async.cs index 3632716b..b3709ac4 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class ArmThumbFilter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.cs b/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.cs index 97d20867..44f3d679 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/ArmThumbFilter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -45,13 +44,5 @@ public partial class ArmThumbFilter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.ARMTConverter(buffer, _ip); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; } diff --git a/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.Async.cs index cb1b4535..a3308505 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class IA64Filter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.cs b/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.cs index b514f64e..4db58a2f 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/IA64Filter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -45,13 +44,5 @@ public partial class IA64Filter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.IA64Converter(buffer, _ip); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; } diff --git a/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.Async.cs index f8d04250..cd5a267b 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.Async.cs @@ -7,6 +7,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class Lzma2Filter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.cs b/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.cs index fef8017b..a3ddaa2f 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/Lzma2Filter.cs @@ -52,8 +52,5 @@ public partial class Lzma2Filter : BlockFilter public override void SetBaseStream(Stream stream) => BaseStream = LzmaStream.Create(new[] { _dictionarySize }, stream); - public override int Read(byte[] buffer, int offset, int count) => - BaseStream.Read(buffer, offset, count); - public override int ReadByte() => BaseStream.ReadByte(); } diff --git a/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.Async.cs index 95c04b0b..40978ee8 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class PowerPCFilter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.cs b/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.cs index d0c6e5f5..2e8cec3e 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/PowerPCFilter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -45,13 +44,5 @@ public partial class PowerPCFilter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.PowerPCConverter(buffer, _ip); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; } diff --git a/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.Async.cs index d4b06191..747439c2 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class SparcFilter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.cs b/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.cs index 01c1cf36..708fa440 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/SparcFilter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -45,13 +44,5 @@ public partial class SparcFilter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.SPARCConverter(buffer, _ip); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; } diff --git a/src/SharpCompress/Compressors/Xz/Filters/X86Filter.Async.cs b/src/SharpCompress/Compressors/Xz/Filters/X86Filter.Async.cs index 6a8ff6f4..c8538c61 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/X86Filter.Async.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/X86Filter.Async.cs @@ -8,6 +8,7 @@ namespace SharpCompress.Compressors.Xz.Filters; public partial class X86Filter { + [Zomp.SyncMethodGenerator.CreateSyncVersion] public override async Task ReadAsync( byte[] buffer, int offset, diff --git a/src/SharpCompress/Compressors/Xz/Filters/X86Filter.cs b/src/SharpCompress/Compressors/Xz/Filters/X86Filter.cs index 37d959bc..8d48394d 100644 --- a/src/SharpCompress/Compressors/Xz/Filters/X86Filter.cs +++ b/src/SharpCompress/Compressors/Xz/Filters/X86Filter.cs @@ -6,7 +6,6 @@ using System.IO; using SharpCompress.Common; -using SharpCompress.Compressors.Filters; namespace SharpCompress.Compressors.Xz.Filters; @@ -47,13 +46,5 @@ public partial class X86Filter : BlockFilter public override void ValidateFilter() { } - public override int Read(byte[] buffer, int offset, int count) - { - var bytesRead = BaseStream.Read(buffer, offset, count); - BranchExecFilter.X86Converter(buffer, _ip, ref _state); - _ip += (uint)bytesRead; - return bytesRead; - } - public override void SetBaseStream(Stream stream) => BaseStream = stream; }