Skip to content
2 changes: 1 addition & 1 deletion Compression.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Use the concrete version you intend to consume; this document does not predict a

<!-- API:BEGIN generated by Hawkynt/RepositoryTemplate/package-readme — edit the XML docs in source, not here -->

Every public and protected member of all 681 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Compression.Core/REFERENCE.md).
Every public and protected member of all 682 types, generated from the built assembly and its XML documentation, is in [REFERENCE.md](https://github.com/Hawkynt/CompressionWorkbench/blob/main/Compression.Core/REFERENCE.md).

<!-- API:END -->

Expand Down
35 changes: 32 additions & 3 deletions Compression.Core/REFERENCE.md

Large diffs are not rendered by default.

96 changes: 96 additions & 0 deletions Compression.Registry/ArchiveFormatOperationsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace Compression.Registry;

/// <summary>
/// Makes the default archive input-mode members available when a descriptor is
/// referenced by its concrete type. Default interface members are otherwise only
/// in the member set of an <see cref="IArchiveFormatOperations"/> reference.
/// </summary>
public static class ArchiveFormatOperationsExtensions {
/// <inheritdoc cref="IArchiveFormatOperations.ListStreaming"/>
public static List<ArchiveEntryInfo> ListStreaming(
this IArchiveFormatOperations operations,
Stream archive,
string? password)
=> operations.ListStreaming(archive, password);

/// <inheritdoc cref="IArchiveFormatOperations.ExtractStreaming"/>
public static void ExtractStreaming(
this IArchiveFormatOperations operations,
Stream archive,
string outputDir,
string? password,
string[]? files)
=> operations.ExtractStreaming(archive, outputDir, password, files);

/// <inheritdoc cref="IArchiveFormatOperations.ListSeekable"/>
public static List<ArchiveEntryInfo> ListSeekable(
this IArchiveFormatOperations operations,
Stream archive,
string? password)
=> operations.ListSeekable(archive, password);

/// <inheritdoc cref="IArchiveFormatOperations.ExtractSeekable"/>
public static void ExtractSeekable(
this IArchiveFormatOperations operations,
Stream archive,
string outputDir,
string? password,
string[]? files)
=> operations.ExtractSeekable(archive, outputDir, password, files);

/// <inheritdoc cref="IArchiveFormatOperations.ListSpan"/>
public static List<ArchiveEntryInfo> ListSpan(
this IArchiveFormatOperations operations,
ReadOnlySpan<byte> archive,
string? password)
=> operations.ListSpan(archive, password);

/// <inheritdoc cref="IArchiveFormatOperations.ExtractSpan"/>
public static void ExtractSpan(
this IArchiveFormatOperations operations,
ReadOnlySpan<byte> archive,
string outputDir,
string? password,
string[]? files)
=> operations.ExtractSpan(archive, outputDir, password, files);

/// <inheritdoc cref="IArchiveFormatOperations.OpenEntryStreaming"/>
public static Stream OpenEntryStreaming(
this IArchiveFormatOperations operations,
Stream archive,
string entryName,
string? password)
=> operations.OpenEntryStreaming(archive, entryName, password);

/// <inheritdoc cref="IArchiveFormatOperations.OpenEntrySeekable"/>
public static Stream OpenEntrySeekable(
this IArchiveFormatOperations operations,
Stream archive,
string entryName,
string? password)
=> operations.OpenEntrySeekable(archive, entryName, password);

/// <inheritdoc cref="IArchiveFormatOperations.OpenEntrySpan"/>
public static Stream OpenEntrySpan(
this IArchiveFormatOperations operations,
ReadOnlySpan<byte> archive,
string entryName,
string? password)
=> operations.OpenEntrySpan(archive, entryName, password);

/// <inheritdoc cref="IArchiveFormatOperations.ExtractEntryToMemoryStreaming"/>
public static byte[] ExtractEntryToMemoryStreaming(
this IArchiveFormatOperations operations,
Stream archive,
string entryName,
string? password)
=> operations.ExtractEntryToMemoryStreaming(archive, entryName, password);

/// <inheritdoc cref="IArchiveFormatOperations.ExtractEntryToMemorySpan"/>
public static byte[] ExtractEntryToMemorySpan(
this IArchiveFormatOperations operations,
ReadOnlySpan<byte> archive,
string entryName,
string? password)
=> operations.ExtractEntryToMemorySpan(archive, entryName, password);
}
155 changes: 150 additions & 5 deletions Compression.Registry/IArchiveFormatOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,94 @@ namespace Compression.Registry;
/// defragment, shrink, input constraints) are separate opt-in interfaces so callers can
/// discover them at the type level.
/// </summary>
/// <remarks>
/// <para>
/// The historical <see cref="List(Stream,string?)"/> / <c>Extract(Stream, ...)</c>
/// methods remain the descriptor's native compatibility surface. The explicit input-mode methods
/// below make the three archive-reading models available uniformly to every archive and
/// pseudo-archive: forward-only stream input, required-seek input, and in-memory
/// <see cref="ReadOnlySpan{T}"/> input.
/// </para>
/// <para>
/// Existing descriptors automatically gain all three modes. A descriptor can override the
/// default interface implementations when it has a genuinely streaming parser, a native
/// random-access reader, or a zero-copy span parser. Until then, forward-only streams are
/// spooled to a temporary seekable file and spans are copied once into an owned memory stream;
/// neither fallback imposes a whole-archive managed-array limit on stream input.
/// </para>
/// </remarks>
public interface IArchiveFormatOperations {
/// <summary>List all entries in the archive.</summary>
/// <summary>List all entries in the archive using the descriptor's native stream path.</summary>
List<ArchiveEntryInfo> List(Stream stream, string? password);

/// <summary>Extract entries from the archive to an output directory.</summary>
/// <summary>Extract entries from the archive to an output directory using the descriptor's native stream path.</summary>
void Extract(Stream stream, string outputDir, string? password, string[]? files);

/// <summary>
/// Lists entries from a forward-only or seekable stream. This is the libarchive-style
/// streaming entry point: callers do not need to provide seek capability.
/// </summary>
public virtual List<ArchiveEntryInfo> ListStreaming(Stream archive, string? password) {
using var lease = SeekableArchiveInputLease.Open(archive);
return this.ListSeekable(lease.Stream, password);
}

/// <summary>
/// Extracts entries from a forward-only or seekable stream. Descriptors with a native
/// one-pass parser should override this method; the default spools only when necessary.
/// </summary>
public virtual void ExtractStreaming(Stream archive, string outputDir, string? password, string[]? files) {
using var lease = SeekableArchiveInputLease.Open(archive);
this.ExtractSeekable(lease.Stream, outputDir, password, files);
}

/// <summary>
/// Lists entries through the explicit seek-based path. The supplied stream must support
/// seeking; it is rewound before the descriptor's native reader is invoked.
/// </summary>
public virtual List<ArchiveEntryInfo> ListSeekable(Stream archive, string? password) {
ArgumentNullException.ThrowIfNull(archive);
if (!archive.CanRead)
throw new ArgumentException("Archive input must be readable.", nameof(archive));
if (!archive.CanSeek)
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
archive.Position = 0;
return this.List(archive, password);
}

/// <summary>
/// Extracts entries through the explicit seek-based path. The supplied stream must support
/// seeking; it is rewound before the descriptor's native reader is invoked.
/// </summary>
public virtual void ExtractSeekable(Stream archive, string outputDir, string? password, string[]? files) {
ArgumentNullException.ThrowIfNull(archive);
if (!archive.CanRead)
throw new ArgumentException("Archive input must be readable.", nameof(archive));
if (!archive.CanSeek)
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
archive.Position = 0;
this.Extract(archive, outputDir, password, files);
}

/// <summary>
/// Lists entries from an in-memory archive image. The default compatibility bridge copies
/// the span once because a <see cref="Stream"/> cannot safely retain a borrowed span; native
/// span parsers should override this method to remain allocation-free.
/// </summary>
public virtual List<ArchiveEntryInfo> ListSpan(ReadOnlySpan<byte> archive, string? password) {
using var stream = new MemoryStream(archive.ToArray(), writable: false);
return this.ListSeekable(stream, password);
}

/// <summary>
/// Extracts entries from an in-memory archive image. Native span parsers can override this
/// method to avoid the compatibility copy used by the default implementation.
/// </summary>
public virtual void ExtractSpan(ReadOnlySpan<byte> archive, string outputDir, string? password, string[]? files) {
using var stream = new MemoryStream(archive.ToArray(), writable: false);
this.ExtractSeekable(stream, outputDir, password, files);
}

/// <summary>
/// Opens a single entry as a read-only <see cref="Stream"/> bounded to that
/// entry's logical bytes — physically incapable of reading slack space,
Expand All @@ -29,7 +110,7 @@ public interface IArchiveFormatOperations {
/// </para>
/// <para>
/// The default implementation intentionally does <b>not</b> materialize a
/// <c>byte[]</c>. It asks <see cref="Extract"/> for the selected entry in an
/// <c>byte[]</c>. It asks <c>Extract(Stream, ...)</c> for the selected entry in an
/// isolated temporary directory, opens the resulting file as a seekable
/// stream, and deletes that tree on dispose. This gives every descriptor a
/// large-file-safe streaming fallback even before it grows a native per-entry
Expand All @@ -44,13 +125,61 @@ public virtual Stream OpenEntry(Stream archive, string entryName, string? passwo
return new BoundedEntryStream(extracted, extracted.Length, leaveOpen: false);
}

/// <summary>
/// Opens one entry from a forward-only or seekable archive source. A temporary spool, when
/// required, stays alive until the returned entry stream is disposed.
/// </summary>
public virtual Stream OpenEntryStreaming(Stream archive, string entryName, string? password) {
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
var lease = SeekableArchiveInputLease.Open(archive);
try {
var entry = this.OpenEntrySeekable(lease.Stream, entryName, password);
return new OwnedArchiveEntryStream(entry, lease);
} catch {
lease.Dispose();
throw;
}
}

/// <summary>
/// Opens one entry through the explicit seek-based path. The archive is rewound before the
/// descriptor-specific entry reader is invoked.
/// </summary>
public virtual Stream OpenEntrySeekable(Stream archive, string entryName, string? password) {
ArgumentNullException.ThrowIfNull(archive);
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
if (!archive.CanRead)
throw new ArgumentException("Archive input must be readable.", nameof(archive));
if (!archive.CanSeek)
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
archive.Position = 0;
return this.OpenEntry(archive, entryName, password);
}

/// <summary>
/// Opens one entry from an in-memory archive image. Because the returned stream may outlive
/// this call, the default bridge owns one copy of the supplied span until that stream is disposed.
/// Native span readers can override this method when they can return independently owned output.
/// </summary>
public virtual Stream OpenEntrySpan(ReadOnlySpan<byte> archive, string entryName, string? password) {
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
var source = new MemoryStream(archive.ToArray(), writable: false);
try {
var entry = this.OpenEntrySeekable(source, entryName, password);
return new OwnedArchiveEntryStream(entry, source);
} catch {
source.Dispose();
throw;
}
}

/// <summary>
/// Extracts a single entry to a byte array. This is the explicitly buffered
/// convenience API; callers working with large entries should use
/// <see cref="OpenEntry"/> instead.
/// <see cref="OpenEntry(Stream,string,string?)"/> instead.
/// </summary>
/// <remarks>
/// The default routes through <see cref="OpenEntry"/>, so descriptor-specific
/// The default routes through <see cref="OpenEntry(Stream,string,string?)"/>, so descriptor-specific
/// isolation/decoding semantics are preserved. A result past the runtime array
/// limit naturally fails here rather than imposing that limit on the streaming
/// API or filesystem-driver layer.
Expand All @@ -64,4 +193,20 @@ public virtual byte[] ExtractEntryToMemory(Stream archive, string entryName, str
entry.CopyTo(memory);
return memory.ToArray();
}

/// <summary>Extracts one entry from a forward-only or seekable archive source into memory.</summary>
public virtual byte[] ExtractEntryToMemoryStreaming(Stream archive, string entryName, string? password) {
using var entry = this.OpenEntryStreaming(archive, entryName, password);
using var memory = new MemoryStream();
entry.CopyTo(memory);
return memory.ToArray();
}

/// <summary>Extracts one entry from an in-memory archive image into a new byte array.</summary>
public virtual byte[] ExtractEntryToMemorySpan(ReadOnlySpan<byte> archive, string entryName, string? password) {
using var entry = this.OpenEntrySpan(archive, entryName, password);
using var memory = new MemoryStream();
entry.CopyTo(memory);
return memory.ToArray();
}
}
Loading
Loading