Skip to content

EntryReader

EntryReader yields one entry’s original bytes, decompressing when the entry is deflated. It is what you reach for when an asset is too large to hold at once.

const buffer = try gpa.alloc(u8, zpack.stream_buffer_len);
defer gpa.free(buffer);
var entry_reader = try archive.entryReader(entry, buffer);
const r = entry_reader.reader();
var header: [4]u8 = undefined;
try r.readSliceAll(&header);

Obtain one from Archive.entryReader rather than constructing it directly.

Entry method Minimum buffer
deflate zpack.stream_buffer_len
store Any size, including smaller than the entry
pub const stream_buffer_len = flate.max_window_len + 4096;

A deflated entry needs flate.max_window_len (64 KiB) for the decompressor’s window - twice deflate’s 32 KiB back-reference history, so matches resolve without shuffling the buffer - plus 4 KiB to hold compressed bytes read from disk. That is where 69632 comes from. Anything smaller returns error.BufferTooSmall.

If you do not know the method up front, size for stream_buffer_len and it works either way. entry.method tells you, if you want to size down for stored entries.

Internally the buffer is split: the first flate.max_window_len bytes become the decompressor’s window, the remainder becomes the input buffer for the compressed bytes. A stored entry uses the whole thing as one plain read buffer.

pub fn reader(self: *EntryReader) *Io.Reader

Returns a std.Io.Reader over the entry’s original bytes. It reports end of stream at the entry’s last byte rather than the file’s, so a deflate stream can never run on into a neighbouring entry or into the index.

Everything in the std.Io.Reader vocabulary works - readSliceAll, peekGreedy, streamRemaining, toss, and so on.

EntryReader streams raw bytes and verifies nothing. That is the trade for not buffering the whole entry - the hash covers the entry in full, so it cannot be confirmed until the last byte has gone past.

Three options:

Hash as you stream, if you need the check and the bytes:

var hasher = zpack.format.Hasher.init(zpack.format.hash_seed);
var total: u64 = 0;
while (true) {
const chunk = r.peekGreedy(1) catch |err| switch (err) {
error.EndOfStream => break,
else => |e| return e,
};
hasher.update(chunk);
// ...consume chunk...
total += chunk.len;
r.toss(chunk.len);
}
if (total != entry.size or hasher.final() != entry.hash) return error.HashMismatch;

Check the length as well as the digest: an entry that produces fewer bytes than promised could otherwise pass a partial-prefix hash.

Use Archive.read when the asset fits in a buffer. It verifies before returning.

Use Archive.verify once at startup, then stream freely afterwards.

pub const StoredReader = struct {
io: Io,
file: File,
pos: u64,
remaining: u64,
err: ?File.ReadPositionalError,
interface: Io.Reader,
};

The layer beneath EntryReader: it reads the bytes an entry occupies in the archive, stopping at the entry’s last byte even though the file continues past it. Those bytes are still compressed when the entry is deflated.

It implements the std.Io.Reader vtable directly rather than wrapping a File.Reader, so it holds no pointer into itself and can be returned by value.

Reach for it only when you want an entry’s stored bytes - copying a deflate stream between archives without recompressing it, for instance. For contents, use EntryReader.

Reads use readPositional, which is why concurrent reads from one Archive do not race on a shared file cursor. A failed read is recorded in err and surfaces through the interface as error.ReadFailed.

Example 04 streams a 1 MiB asset through a 68 KiB buffer, hashing as it goes:

Terminal window
zig build example-04