Skip to content

Commit f264703

Browse files
authored
Expose stream, seek, and span archive input modes (#373)
* + add universal archive input bridges * + expose stream seek and span archive read modes * + expose archive input modes on concrete descriptors * + test universal archive input modes * # correct archive API XML references * # correct span extension XML reference * * make archive input modes explicitly symmetric * * align archive extension names with input modes * * test explicit span archive input mode * * regenerate the package API reference for the input-mode surface
1 parent 1e0b914 commit f264703

10 files changed

Lines changed: 607 additions & 17 deletions

File tree

Compression.Core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ Use the concrete version you intend to consume; this document does not predict a
204204

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

207-
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).
207+
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).
208208

209209
<!-- API:END -->
210210

Compression.Core/REFERENCE.md

Lines changed: 32 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
namespace Compression.Registry;
2+
3+
/// <summary>
4+
/// Makes the default archive input-mode members available when a descriptor is
5+
/// referenced by its concrete type. Default interface members are otherwise only
6+
/// in the member set of an <see cref="IArchiveFormatOperations"/> reference.
7+
/// </summary>
8+
public static class ArchiveFormatOperationsExtensions {
9+
/// <inheritdoc cref="IArchiveFormatOperations.ListStreaming"/>
10+
public static List<ArchiveEntryInfo> ListStreaming(
11+
this IArchiveFormatOperations operations,
12+
Stream archive,
13+
string? password)
14+
=> operations.ListStreaming(archive, password);
15+
16+
/// <inheritdoc cref="IArchiveFormatOperations.ExtractStreaming"/>
17+
public static void ExtractStreaming(
18+
this IArchiveFormatOperations operations,
19+
Stream archive,
20+
string outputDir,
21+
string? password,
22+
string[]? files)
23+
=> operations.ExtractStreaming(archive, outputDir, password, files);
24+
25+
/// <inheritdoc cref="IArchiveFormatOperations.ListSeekable"/>
26+
public static List<ArchiveEntryInfo> ListSeekable(
27+
this IArchiveFormatOperations operations,
28+
Stream archive,
29+
string? password)
30+
=> operations.ListSeekable(archive, password);
31+
32+
/// <inheritdoc cref="IArchiveFormatOperations.ExtractSeekable"/>
33+
public static void ExtractSeekable(
34+
this IArchiveFormatOperations operations,
35+
Stream archive,
36+
string outputDir,
37+
string? password,
38+
string[]? files)
39+
=> operations.ExtractSeekable(archive, outputDir, password, files);
40+
41+
/// <inheritdoc cref="IArchiveFormatOperations.ListSpan"/>
42+
public static List<ArchiveEntryInfo> ListSpan(
43+
this IArchiveFormatOperations operations,
44+
ReadOnlySpan<byte> archive,
45+
string? password)
46+
=> operations.ListSpan(archive, password);
47+
48+
/// <inheritdoc cref="IArchiveFormatOperations.ExtractSpan"/>
49+
public static void ExtractSpan(
50+
this IArchiveFormatOperations operations,
51+
ReadOnlySpan<byte> archive,
52+
string outputDir,
53+
string? password,
54+
string[]? files)
55+
=> operations.ExtractSpan(archive, outputDir, password, files);
56+
57+
/// <inheritdoc cref="IArchiveFormatOperations.OpenEntryStreaming"/>
58+
public static Stream OpenEntryStreaming(
59+
this IArchiveFormatOperations operations,
60+
Stream archive,
61+
string entryName,
62+
string? password)
63+
=> operations.OpenEntryStreaming(archive, entryName, password);
64+
65+
/// <inheritdoc cref="IArchiveFormatOperations.OpenEntrySeekable"/>
66+
public static Stream OpenEntrySeekable(
67+
this IArchiveFormatOperations operations,
68+
Stream archive,
69+
string entryName,
70+
string? password)
71+
=> operations.OpenEntrySeekable(archive, entryName, password);
72+
73+
/// <inheritdoc cref="IArchiveFormatOperations.OpenEntrySpan"/>
74+
public static Stream OpenEntrySpan(
75+
this IArchiveFormatOperations operations,
76+
ReadOnlySpan<byte> archive,
77+
string entryName,
78+
string? password)
79+
=> operations.OpenEntrySpan(archive, entryName, password);
80+
81+
/// <inheritdoc cref="IArchiveFormatOperations.ExtractEntryToMemoryStreaming"/>
82+
public static byte[] ExtractEntryToMemoryStreaming(
83+
this IArchiveFormatOperations operations,
84+
Stream archive,
85+
string entryName,
86+
string? password)
87+
=> operations.ExtractEntryToMemoryStreaming(archive, entryName, password);
88+
89+
/// <inheritdoc cref="IArchiveFormatOperations.ExtractEntryToMemorySpan"/>
90+
public static byte[] ExtractEntryToMemorySpan(
91+
this IArchiveFormatOperations operations,
92+
ReadOnlySpan<byte> archive,
93+
string entryName,
94+
string? password)
95+
=> operations.ExtractEntryToMemorySpan(archive, entryName, password);
96+
}

Compression.Registry/IArchiveFormatOperations.cs

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,94 @@ namespace Compression.Registry;
88
/// defragment, shrink, input constraints) are separate opt-in interfaces so callers can
99
/// discover them at the type level.
1010
/// </summary>
11+
/// <remarks>
12+
/// <para>
13+
/// The historical <see cref="List(Stream,string?)"/> / <c>Extract(Stream, ...)</c>
14+
/// methods remain the descriptor's native compatibility surface. The explicit input-mode methods
15+
/// below make the three archive-reading models available uniformly to every archive and
16+
/// pseudo-archive: forward-only stream input, required-seek input, and in-memory
17+
/// <see cref="ReadOnlySpan{T}"/> input.
18+
/// </para>
19+
/// <para>
20+
/// Existing descriptors automatically gain all three modes. A descriptor can override the
21+
/// default interface implementations when it has a genuinely streaming parser, a native
22+
/// random-access reader, or a zero-copy span parser. Until then, forward-only streams are
23+
/// spooled to a temporary seekable file and spans are copied once into an owned memory stream;
24+
/// neither fallback imposes a whole-archive managed-array limit on stream input.
25+
/// </para>
26+
/// </remarks>
1127
public interface IArchiveFormatOperations {
12-
/// <summary>List all entries in the archive.</summary>
28+
/// <summary>List all entries in the archive using the descriptor's native stream path.</summary>
1329
List<ArchiveEntryInfo> List(Stream stream, string? password);
1430

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

34+
/// <summary>
35+
/// Lists entries from a forward-only or seekable stream. This is the libarchive-style
36+
/// streaming entry point: callers do not need to provide seek capability.
37+
/// </summary>
38+
public virtual List<ArchiveEntryInfo> ListStreaming(Stream archive, string? password) {
39+
using var lease = SeekableArchiveInputLease.Open(archive);
40+
return this.ListSeekable(lease.Stream, password);
41+
}
42+
43+
/// <summary>
44+
/// Extracts entries from a forward-only or seekable stream. Descriptors with a native
45+
/// one-pass parser should override this method; the default spools only when necessary.
46+
/// </summary>
47+
public virtual void ExtractStreaming(Stream archive, string outputDir, string? password, string[]? files) {
48+
using var lease = SeekableArchiveInputLease.Open(archive);
49+
this.ExtractSeekable(lease.Stream, outputDir, password, files);
50+
}
51+
52+
/// <summary>
53+
/// Lists entries through the explicit seek-based path. The supplied stream must support
54+
/// seeking; it is rewound before the descriptor's native reader is invoked.
55+
/// </summary>
56+
public virtual List<ArchiveEntryInfo> ListSeekable(Stream archive, string? password) {
57+
ArgumentNullException.ThrowIfNull(archive);
58+
if (!archive.CanRead)
59+
throw new ArgumentException("Archive input must be readable.", nameof(archive));
60+
if (!archive.CanSeek)
61+
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
62+
archive.Position = 0;
63+
return this.List(archive, password);
64+
}
65+
66+
/// <summary>
67+
/// Extracts entries through the explicit seek-based path. The supplied stream must support
68+
/// seeking; it is rewound before the descriptor's native reader is invoked.
69+
/// </summary>
70+
public virtual void ExtractSeekable(Stream archive, string outputDir, string? password, string[]? files) {
71+
ArgumentNullException.ThrowIfNull(archive);
72+
if (!archive.CanRead)
73+
throw new ArgumentException("Archive input must be readable.", nameof(archive));
74+
if (!archive.CanSeek)
75+
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
76+
archive.Position = 0;
77+
this.Extract(archive, outputDir, password, files);
78+
}
79+
80+
/// <summary>
81+
/// Lists entries from an in-memory archive image. The default compatibility bridge copies
82+
/// the span once because a <see cref="Stream"/> cannot safely retain a borrowed span; native
83+
/// span parsers should override this method to remain allocation-free.
84+
/// </summary>
85+
public virtual List<ArchiveEntryInfo> ListSpan(ReadOnlySpan<byte> archive, string? password) {
86+
using var stream = new MemoryStream(archive.ToArray(), writable: false);
87+
return this.ListSeekable(stream, password);
88+
}
89+
90+
/// <summary>
91+
/// Extracts entries from an in-memory archive image. Native span parsers can override this
92+
/// method to avoid the compatibility copy used by the default implementation.
93+
/// </summary>
94+
public virtual void ExtractSpan(ReadOnlySpan<byte> archive, string outputDir, string? password, string[]? files) {
95+
using var stream = new MemoryStream(archive.ToArray(), writable: false);
96+
this.ExtractSeekable(stream, outputDir, password, files);
97+
}
98+
1899
/// <summary>
19100
/// Opens a single entry as a read-only <see cref="Stream"/> bounded to that
20101
/// entry's logical bytes — physically incapable of reading slack space,
@@ -29,7 +110,7 @@ public interface IArchiveFormatOperations {
29110
/// </para>
30111
/// <para>
31112
/// The default implementation intentionally does <b>not</b> materialize a
32-
/// <c>byte[]</c>. It asks <see cref="Extract"/> for the selected entry in an
113+
/// <c>byte[]</c>. It asks <c>Extract(Stream, ...)</c> for the selected entry in an
33114
/// isolated temporary directory, opens the resulting file as a seekable
34115
/// stream, and deletes that tree on dispose. This gives every descriptor a
35116
/// large-file-safe streaming fallback even before it grows a native per-entry
@@ -44,13 +125,61 @@ public virtual Stream OpenEntry(Stream archive, string entryName, string? passwo
44125
return new BoundedEntryStream(extracted, extracted.Length, leaveOpen: false);
45126
}
46127

128+
/// <summary>
129+
/// Opens one entry from a forward-only or seekable archive source. A temporary spool, when
130+
/// required, stays alive until the returned entry stream is disposed.
131+
/// </summary>
132+
public virtual Stream OpenEntryStreaming(Stream archive, string entryName, string? password) {
133+
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
134+
var lease = SeekableArchiveInputLease.Open(archive);
135+
try {
136+
var entry = this.OpenEntrySeekable(lease.Stream, entryName, password);
137+
return new OwnedArchiveEntryStream(entry, lease);
138+
} catch {
139+
lease.Dispose();
140+
throw;
141+
}
142+
}
143+
144+
/// <summary>
145+
/// Opens one entry through the explicit seek-based path. The archive is rewound before the
146+
/// descriptor-specific entry reader is invoked.
147+
/// </summary>
148+
public virtual Stream OpenEntrySeekable(Stream archive, string entryName, string? password) {
149+
ArgumentNullException.ThrowIfNull(archive);
150+
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
151+
if (!archive.CanRead)
152+
throw new ArgumentException("Archive input must be readable.", nameof(archive));
153+
if (!archive.CanSeek)
154+
throw new ArgumentException("Seek-based archive input must support seeking.", nameof(archive));
155+
archive.Position = 0;
156+
return this.OpenEntry(archive, entryName, password);
157+
}
158+
159+
/// <summary>
160+
/// Opens one entry from an in-memory archive image. Because the returned stream may outlive
161+
/// this call, the default bridge owns one copy of the supplied span until that stream is disposed.
162+
/// Native span readers can override this method when they can return independently owned output.
163+
/// </summary>
164+
public virtual Stream OpenEntrySpan(ReadOnlySpan<byte> archive, string entryName, string? password) {
165+
ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
166+
var source = new MemoryStream(archive.ToArray(), writable: false);
167+
try {
168+
var entry = this.OpenEntrySeekable(source, entryName, password);
169+
return new OwnedArchiveEntryStream(entry, source);
170+
} catch {
171+
source.Dispose();
172+
throw;
173+
}
174+
}
175+
47176
/// <summary>
48177
/// Extracts a single entry to a byte array. This is the explicitly buffered
49178
/// convenience API; callers working with large entries should use
50-
/// <see cref="OpenEntry"/> instead.
179+
/// <see cref="OpenEntry(Stream,string,string?)"/> instead.
51180
/// </summary>
52181
/// <remarks>
53-
/// The default routes through <see cref="OpenEntry"/>, so descriptor-specific
182+
/// The default routes through <see cref="OpenEntry(Stream,string,string?)"/>, so descriptor-specific
54183
/// isolation/decoding semantics are preserved. A result past the runtime array
55184
/// limit naturally fails here rather than imposing that limit on the streaming
56185
/// API or filesystem-driver layer.
@@ -64,4 +193,20 @@ public virtual byte[] ExtractEntryToMemory(Stream archive, string entryName, str
64193
entry.CopyTo(memory);
65194
return memory.ToArray();
66195
}
196+
197+
/// <summary>Extracts one entry from a forward-only or seekable archive source into memory.</summary>
198+
public virtual byte[] ExtractEntryToMemoryStreaming(Stream archive, string entryName, string? password) {
199+
using var entry = this.OpenEntryStreaming(archive, entryName, password);
200+
using var memory = new MemoryStream();
201+
entry.CopyTo(memory);
202+
return memory.ToArray();
203+
}
204+
205+
/// <summary>Extracts one entry from an in-memory archive image into a new byte array.</summary>
206+
public virtual byte[] ExtractEntryToMemorySpan(ReadOnlySpan<byte> archive, string entryName, string? password) {
207+
using var entry = this.OpenEntrySpan(archive, entryName, password);
208+
using var memory = new MemoryStream();
209+
entry.CopyTo(memory);
210+
return memory.ToArray();
211+
}
67212
}

0 commit comments

Comments
 (0)