Nope! It will do a single allocation of the remaining file size!
unless the file size can’t be gotten, but you did it so clearly it does work for your file
If you follow the function calls:
pub fn allocRemaining(r: *Reader, gpa: Allocator, limit: Limit) LimitedAllocError![]u8 {
var buffer: ArrayList(u8) = .empty;
defer buffer.deinit(gpa);
try appendRemaining(r, gpa, &buffer, limit);
return buffer.toOwnedSlice(gpa);
}
pub fn appendRemaining(
r: *Reader,
gpa: Allocator,
list: *ArrayList(u8),
limit: Limit,
) LimitedAllocError!void {
return appendRemainingAligned(r, gpa, .of(u8), list, limit);
}
pub fn appendRemainingAligned(
r: *Reader,
gpa: Allocator,
comptime alignment: std.mem.Alignment,
list: *std.array_list.Aligned(u8, alignment),
limit: Limit,
) LimitedAllocError!void {
// THIS IS IMPORTANT
var a = std.Io.Writer.Allocating.fromArrayListAligned(gpa, alignment, list);
defer list.* = a.toArrayListAligned(alignment);
var remaining = limit;
while (remaining != .nothing) {
const n = stream(r, &a.writer, remaining) catch |err| switch (err) {
error.EndOfStream => return,
error.WriteFailed => return error.OutOfMemory,
error.ReadFailed => return error.ReadFailed,
};
remaining = remaining.subtract(n).?;
}
return error.StreamTooLong;
}
pub fn stream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize {
const buffer = limit.slice(r.buffer[r.seek..r.end]);
if (buffer.len > 0) {
@branchHint(.likely);
const n = try w.write(buffer);
r.seek += n;
return n;
}
const n = try r.vtable.stream(r, w, limit);
assert(n <= @intFromEnum(limit));
return n;
}
You can see that it just calls stream with the reader and a Writer.Allocating (made from an array list).
Jumping to the File reader implementation:
fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader));
return streamMode(r, w, limit, r.mode);
}
pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Reader.StreamError!usize {
switch (mode) {
.positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
error.Unimplemented => {
r.mode = r.mode.toSimple();
return 0;
},
else => |e| return e,
},
.positional_simple => {
const dest = limit.slice(try w.writableSliceGreedy(1));
var data: [1][]u8 = .{dest};
const n = try readVecPositional(r, &data);
w.advance(n);
return n;
},
.streaming_simple => {
const dest = limit.slice(try w.writableSliceGreedy(1));
var data: [1][]u8 = .{dest};
const n = try readVecStreaming(r, &data);
w.advance(n);
return n;
},
.failure => return error.ReadFailed,
}
}
In positional and streaming modes, it calls sendFile on the writer. This gives the writer access to the File.Reader implementation!!!
Jumping to the Writer.Allocating implementation:
// for completeness, `Writer.sendFile`
pub fn sendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize {
return w.vtable.sendFile(w, file_reader, limit);
}
// now Allocating's implementation
fn sendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize {
if (File.Handle == void) return error.Unimplemented;
if (limit == .nothing) return 0;
const a: *Allocating = @fieldParentPtr("writer", w);
// gets the *remaining* size of the file, or default if error
const pos = file_reader.logicalPos();
const additional = if (file_reader.getSize()) |size| size - pos else |_| std.atomic.cache_line;
if (additional == 0) return error.EndOfStream;
// allocates the minimum between the limit and remaining size
a.ensureUnusedCapacity(limit.minInt64(additional)) catch return error.WriteFailed;
// reads directly into the allocated slice, accounting for limit
const dest = limit.slice(a.writer.buffer[a.writer.end..]);
const n = try file_reader.interface.readSliceShort(dest);
if (n == 0) return error.EndOfStream;
a.writer.end += n;
return n;
}