Skip to content

Refactor sync/async implementations by using Adapter pattern#129282

Open
alinpahontu2912 wants to merge 4 commits into
dotnet:mainfrom
alinpahontu2912:tar_deduplicate
Open

Refactor sync/async implementations by using Adapter pattern#129282
alinpahontu2912 wants to merge 4 commits into
dotnet:mainfrom
alinpahontu2912:tar_deduplicate

Conversation

@alinpahontu2912

Copy link
Copy Markdown
Member

Fixes #127378

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors System.Formats.Tar to deduplicate synchronous and asynchronous read/write implementations by introducing a static-abstract IReadWriteAdapter and routing both sync/async public APIs through shared generic “CoreAsync” methods.

Changes:

  • Introduces IReadWriteAdapter with SyncReadWriteAdapter / AsyncReadWriteAdapter implementations to unify stream operations.
  • Reworks TarReader / TarWriter to funnel sync and async APIs through shared generic *CoreAsync<TAdapter> implementations.
  • Updates shared helpers and header read/write logic (TarHelpers, TarHeader.Read, TarHeader.Write) to use the adapter-based core methods.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs Routes sync/async write APIs through a unified adapter-based core implementation.
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReadWriteAdapter.cs Adds the sync/async adapter abstraction (static-abstract interface + implementations).
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs Routes sync/async read APIs through a unified adapter-based core implementation.
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs Refactors stream-advance/copy helpers into generic core methods using adapters.
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs Unifies header write logic behind adapter-based core methods.
src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj Includes the new TarReadWriteAdapter.cs in compilation.

Comment on lines +22 to 27
internal static async ValueTask<TarHeader?> TryGetNextHeaderCoreAsync<TAdapter>(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken)
where TAdapter : IReadWriteAdapter
{
// The four supported formats have a header that fits in the default record size
Span<byte> buffer = stackalloc byte[TarHelpers.RecordSize];

archiveStream.ReadExactly(buffer);

TarHeader? header = TryReadAttributes(initialFormat, buffer, archiveStream);
if (header != null && processDataBlock)
{
header.ProcessDataBlock(archiveStream, copyData);
}

return header;
}

// Asynchronously attempts read all the fields of the next header.
// Throws if end of stream is reached or if any data type conversion fails.
// Returns true if all the attributes were read successfully, false otherwise.
internal static async ValueTask<TarHeader?> TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, bool processDataBlock, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

// The four supported formats have a header that fits in the default record size
byte[] rented = ArrayPool<byte>.Shared.Rent(minimumLength: TarHelpers.RecordSize);
Memory<byte> buffer = rented.AsMemory(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger
Comment thread src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs Outdated
Comment on lines +27 to +42
public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
stream.ReadAsync(buffer, cancellationToken);

public static ValueTask ReadExactlyAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
stream.ReadExactlyAsync(buffer, cancellationToken);

public static ValueTask WriteAsync(Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken) =>
stream.WriteAsync(buffer, cancellationToken);

public static ValueTask CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) =>
new ValueTask(source.CopyToAsync(destination, cancellationToken));

public static ValueTask AdvanceToEndAsync(SubReadStream stream, CancellationToken cancellationToken) =>
stream.AdvanceToEndAsync(cancellationToken);

public static ValueTask DisposeAsync(Stream stream) => stream.DisposeAsync();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add awaits to help runtime async? Or are they not necessary?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is work in progress to make forwards like this work well in runtime async.

Note that await changes observable behaviors: It wraps exceptions in the Task and it saves/restores async context.

@rzikm rzikm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seem to be some private/internal methods that are not converted (i commented on them), if we call them only from a single place, I think we should remove them

// SyncReadWriteAdapter and asserts the returned ValueTask is already completed;
// the async entry point passes AsyncReadWriteAdapter. Mirrors the IReadWriteAdapter
// pattern used by SslStream / SmtpClient (see src/libraries/Common/src/System/Net/ReadWriteAdapter.cs).
internal interface IReadWriteAdapter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to duplicate the adapter? can't we reuse/enhance the existing adapter we use in SslStream?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is possible, probably need to update the csproj file

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea to add a reference the the netwroking project here though ? I think the adapters are also slightly different, since tar doesn't need the waitasync, readatleast and ssl doesn't need advancetoend/readexactly from what I can see, so maybe it's not worth combining them ? Or maybe we should create something super generic in a common project and reuse that adapter everywhere?

Comment on lines 53 to 70
internal static void AdvanceStream(Stream archiveStream, long bytesToDiscard)
{
if (archiveStream.CanSeek)
{
archiveStream.Position += bytesToDiscard;
}
else if (bytesToDiscard > 0)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(minimumLength: (int)Math.Min(MaxBufferLength, bytesToDiscard));
while (bytesToDiscard > 0)
{
int currentLengthToRead = (int)Math.Min(MaxBufferLength, bytesToDiscard);
archiveStream.ReadExactly(buffer.AsSpan(0, currentLengthToRead));
bytesToDiscard -= currentLengthToRead;
}
ArrayPool<byte>.Shared.Return(buffer);
}
ValueTask vt = AdvanceStreamCoreAsync<SyncReadWriteAdapter>(archiveStream, bytesToDiscard, CancellationToken.None);
Debug.Assert(vt.IsCompleted, "Synchronous AdvanceStream completed asynchronously.");
vt.GetAwaiter().GetResult();
}

// Asynchronously helps advance the stream a total number of bytes larger than int.MaxValue.
internal static async ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken)
internal static ValueTask AdvanceStreamAsync(Stream archiveStream, long bytesToDiscard, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled(cancellationToken);
}

return AdvanceStreamCoreAsync<AsyncReadWriteAdapter>(archiveStream, bytesToDiscard, cancellationToken);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need those two as separate helpers?

@alinpahontu2912 alinpahontu2912 Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't touched gnuspsarsestream, which is where these two are used. the biggest code chunk in there, parsesparsemap, uses the isasync bool. Should I refactor it too?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes,

Comment thread src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarReader.cs Outdated
Comment thread src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarWriter.cs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deduplicate TarReader/Writer sync/async paths

7 participants