diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2025-06-27 19:33:03 -0700 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2025-07-07 22:43:51 -0700 |
| commit | 9f27d770a1832cf3017a8b2f7281b6faf0347a51 (patch) | |
| tree | ef5207f926478533af2e4f84ff7fdc62e6f61a6f /lib/std | |
| parent | fc2c1883b36a6ba8c7303d12b57147656dc7dd70 (diff) | |
| download | zig-9f27d770a1832cf3017a8b2f7281b6faf0347a51.tar.gz zig-9f27d770a1832cf3017a8b2f7281b6faf0347a51.zip | |
std.io: deprecated Reader/Writer; introduce new API
Diffstat (limited to 'lib/std')
53 files changed, 4267 insertions, 469 deletions
diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig index 2a9159aeac..f1a6ec5041 100644 --- a/lib/std/array_list.zig +++ b/lib/std/array_list.zig @@ -338,11 +338,14 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?mem.Alignment) ty @memcpy(self.items[old_len..][0..items.len], items); } - pub const Writer = if (T != u8) - @compileError("The Writer interface is only defined for ArrayList(u8) " ++ - "but the given type is ArrayList(" ++ @typeName(T) ++ ")") - else - std.io.Writer(*Self, Allocator.Error, appendWrite); + pub fn print(self: *Self, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { + const gpa = self.allocator; + var unmanaged = self.moveToUnmanaged(); + defer self.* = unmanaged.toManaged(gpa); + try unmanaged.print(gpa, fmt, args); + } + + pub const Writer = if (T != u8) void else std.io.GenericWriter(*Self, Allocator.Error, appendWrite); /// Initializes a Writer which will append to the list. pub fn writer(self: *Self) Writer { @@ -350,14 +353,14 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?mem.Alignment) ty } /// Same as `append` except it returns the number of bytes written, which is always the same - /// as `m.len`. The purpose of this function existing is to match `std.io.Writer` API. + /// as `m.len`. The purpose of this function existing is to match `std.io.GenericWriter` API. /// Invalidates element pointers if additional memory is needed. fn appendWrite(self: *Self, m: []const u8) Allocator.Error!usize { try self.appendSlice(m); return m.len; } - pub const FixedWriter = std.io.Writer(*Self, Allocator.Error, appendWriteFixed); + pub const FixedWriter = std.io.GenericWriter(*Self, Allocator.Error, appendWriteFixed); /// Initializes a Writer which will append to the list but will return /// `error.OutOfMemory` rather than increasing capacity. @@ -365,7 +368,7 @@ pub fn ArrayListAligned(comptime T: type, comptime alignment: ?mem.Alignment) ty return .{ .context = self }; } - /// The purpose of this function existing is to match `std.io.Writer` API. + /// The purpose of this function existing is to match `std.io.GenericWriter` API. fn appendWriteFixed(self: *Self, m: []const u8) error{OutOfMemory}!usize { const available_capacity = self.capacity - self.items.len; if (m.len > available_capacity) @@ -933,40 +936,56 @@ pub fn ArrayListAlignedUnmanaged(comptime T: type, comptime alignment: ?mem.Alig @memcpy(self.items[old_len..][0..items.len], items); } + pub fn print(self: *Self, gpa: Allocator, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { + comptime assert(T == u8); + try self.ensureUnusedCapacity(gpa, fmt.len); + var aw: std.io.Writer.Allocating = .fromArrayList(gpa, self); + defer self.* = aw.toArrayList(); + return aw.interface.print(fmt, args) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + }; + } + + pub fn printAssumeCapacity(self: *Self, comptime fmt: []const u8, args: anytype) void { + comptime assert(T == u8); + var w: std.io.Writer = .fixed(self.unusedCapacitySlice()); + w.print(fmt, args) catch unreachable; + self.items.len += w.end; + } + + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. pub const WriterContext = struct { self: *Self, allocator: Allocator, }; + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. pub const Writer = if (T != u8) @compileError("The Writer interface is only defined for ArrayList(u8) " ++ "but the given type is ArrayList(" ++ @typeName(T) ++ ")") else - std.io.Writer(WriterContext, Allocator.Error, appendWrite); + std.io.GenericWriter(WriterContext, Allocator.Error, appendWrite); - /// Initializes a Writer which will append to the list. + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. pub fn writer(self: *Self, gpa: Allocator) Writer { return .{ .context = .{ .self = self, .allocator = gpa } }; } - /// Same as `append` except it returns the number of bytes written, - /// which is always the same as `m.len`. The purpose of this function - /// existing is to match `std.io.Writer` API. - /// Invalidates element pointers if additional memory is needed. + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. fn appendWrite(context: WriterContext, m: []const u8) Allocator.Error!usize { try context.self.appendSlice(context.allocator, m); return m.len; } - pub const FixedWriter = std.io.Writer(*Self, Allocator.Error, appendWriteFixed); + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. + pub const FixedWriter = std.io.GenericWriter(*Self, Allocator.Error, appendWriteFixed); - /// Initializes a Writer which will append to the list but will return - /// `error.OutOfMemory` rather than increasing capacity. + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. pub fn fixedWriter(self: *Self) FixedWriter { return .{ .context = self }; } - /// The purpose of this function existing is to match `std.io.Writer` API. + /// Deprecated in favor of `print` or `std.io.Writer.Allocating`. fn appendWriteFixed(self: *Self, m: []const u8) error{OutOfMemory}!usize { const available_capacity = self.capacity - self.items.len; if (m.len > available_capacity) diff --git a/lib/std/base64.zig b/lib/std/base64.zig index e88b723439..a84f4a0b4f 100644 --- a/lib/std/base64.zig +++ b/lib/std/base64.zig @@ -108,7 +108,7 @@ pub const Base64Encoder = struct { } } - // dest must be compatible with std.io.Writer's writeAll interface + // dest must be compatible with std.io.GenericWriter's writeAll interface pub fn encodeWriter(encoder: *const Base64Encoder, dest: anytype, source: []const u8) !void { var chunker = window(u8, source, 3, 3); while (chunker.next()) |chunk| { @@ -118,8 +118,8 @@ pub const Base64Encoder = struct { } } - // destWriter must be compatible with std.io.Writer's writeAll interface - // sourceReader must be compatible with std.io.Reader's read interface + // destWriter must be compatible with std.io.GenericWriter's writeAll interface + // sourceReader must be compatible with `std.io.GenericReader` read interface pub fn encodeFromReaderToWriter(encoder: *const Base64Encoder, destWriter: anytype, sourceReader: anytype) !void { while (true) { var tempSource: [3]u8 = undefined; diff --git a/lib/std/bounded_array.zig b/lib/std/bounded_array.zig index 1a4407e687..7864dfb775 100644 --- a/lib/std/bounded_array.zig +++ b/lib/std/bounded_array.zig @@ -277,7 +277,7 @@ pub fn BoundedArrayAligned( @compileError("The Writer interface is only defined for BoundedArray(u8, ...) " ++ "but the given type is BoundedArray(" ++ @typeName(T) ++ ", ...)") else - std.io.Writer(*Self, error{Overflow}, appendWrite); + std.io.GenericWriter(*Self, error{Overflow}, appendWrite); /// Initializes a writer which will write into the array. pub fn writer(self: *Self) Writer { @@ -285,7 +285,7 @@ pub fn BoundedArrayAligned( } /// Same as `appendSlice` except it returns the number of bytes written, which is always the same - /// as `m.len`. The purpose of this function existing is to match `std.io.Writer` API. + /// as `m.len`. The purpose of this function existing is to match `std.io.GenericWriter` API. fn appendWrite(self: *Self, m: []const u8) error{Overflow}!usize { try self.appendSlice(m); return m.len; diff --git a/lib/std/compress.zig b/lib/std/compress.zig index 7cc4a80d33..e07c3a4126 100644 --- a/lib/std/compress.zig +++ b/lib/std/compress.zig @@ -16,7 +16,7 @@ pub fn HashedReader(ReaderType: type, HasherType: type) type { hasher: HasherType, pub const Error = ReaderType.Error; - pub const Reader = std.io.Reader(*@This(), Error, read); + pub const Reader = std.io.GenericReader(*@This(), Error, read); pub fn read(self: *@This(), buf: []u8) Error!usize { const amt = try self.child_reader.read(buf); @@ -43,7 +43,7 @@ pub fn HashedWriter(WriterType: type, HasherType: type) type { hasher: HasherType, pub const Error = WriterType.Error; - pub const Writer = std.io.Writer(*@This(), Error, write); + pub const Writer = std.io.GenericWriter(*@This(), Error, write); pub fn write(self: *@This(), buf: []const u8) Error!usize { const amt = try self.child_writer.write(buf); diff --git a/lib/std/compress/flate/deflate.zig b/lib/std/compress/flate/deflate.zig index e953ecb354..fd93236000 100644 --- a/lib/std/compress/flate/deflate.zig +++ b/lib/std/compress/flate/deflate.zig @@ -355,7 +355,7 @@ fn Deflate(comptime container: Container, comptime WriterType: type, comptime Bl // Writer interface - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); pub const Error = BlockWriterType.Error; /// Write `input` of uncompressed data. @@ -512,7 +512,7 @@ fn SimpleCompressor( // Writer interface - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); pub const Error = BlockWriterType.Error; // Write `input` of uncompressed data. diff --git a/lib/std/compress/flate/inflate.zig b/lib/std/compress/flate/inflate.zig index bb4d158aca..2fcf3cafd4 100644 --- a/lib/std/compress/flate/inflate.zig +++ b/lib/std/compress/flate/inflate.zig @@ -341,7 +341,7 @@ pub fn Inflate(comptime container: Container, comptime LookaheadType: type, comp // Reader interface - pub const Reader = std.io.Reader(*Self, Error, read); + pub const Reader = std.io.GenericReader(*Self, Error, read); /// Returns the number of bytes read. It may be less than buffer.len. /// If the number of bytes read is 0, it means end of stream. diff --git a/lib/std/compress/lzma.zig b/lib/std/compress/lzma.zig index aa35c3ffa9..5d072f5381 100644 --- a/lib/std/compress/lzma.zig +++ b/lib/std/compress/lzma.zig @@ -30,7 +30,7 @@ pub fn Decompress(comptime ReaderType: type) type { Allocator.Error || error{ CorruptInput, EndOfStream, Overflow }; - pub const Reader = std.io.Reader(*Self, Error, read); + pub const Reader = std.io.GenericReader(*Self, Error, read); allocator: Allocator, in_reader: ReaderType, diff --git a/lib/std/compress/xz.zig b/lib/std/compress/xz.zig index 4514c8857f..445d103098 100644 --- a/lib/std/compress/xz.zig +++ b/lib/std/compress/xz.zig @@ -34,7 +34,7 @@ pub fn Decompress(comptime ReaderType: type) type { const Self = @This(); pub const Error = ReaderType.Error || block.Decoder(ReaderType).Error; - pub const Reader = std.io.Reader(*Self, Error, read); + pub const Reader = std.io.GenericReader(*Self, Error, read); allocator: Allocator, block_decoder: block.Decoder(ReaderType), diff --git a/lib/std/compress/xz/block.zig b/lib/std/compress/xz/block.zig index a3c289dfb8..6253341f36 100644 --- a/lib/std/compress/xz/block.zig +++ b/lib/std/compress/xz/block.zig @@ -27,7 +27,7 @@ pub fn Decoder(comptime ReaderType: type) type { ReaderType.Error || DecodeError || Allocator.Error; - pub const Reader = std.io.Reader(*Self, Error, read); + pub const Reader = std.io.GenericReader(*Self, Error, read); allocator: Allocator, inner_reader: ReaderType, diff --git a/lib/std/compress/zstandard.zig b/lib/std/compress/zstandard.zig index 7b41e1fe3e..df45e9686d 100644 --- a/lib/std/compress/zstandard.zig +++ b/lib/std/compress/zstandard.zig @@ -50,7 +50,7 @@ pub fn Decompressor(comptime ReaderType: type) type { OutOfMemory, }; - pub const Reader = std.io.Reader(*Self, Error, read); + pub const Reader = std.io.GenericReader(*Self, Error, read); pub fn init(source: ReaderType, options: DecompressorOptions) Self { return .{ diff --git a/lib/std/compress/zstandard/readers.zig b/lib/std/compress/zstandard/readers.zig index d7bf90ed80..7b15784187 100644 --- a/lib/std/compress/zstandard/readers.zig +++ b/lib/std/compress/zstandard/readers.zig @@ -4,7 +4,7 @@ pub const ReversedByteReader = struct { remaining_bytes: usize, bytes: []const u8, - const Reader = std.io.Reader(*ReversedByteReader, error{}, readFn); + const Reader = std.io.GenericReader(*ReversedByteReader, error{}, readFn); pub fn init(bytes: []const u8) ReversedByteReader { return .{ diff --git a/lib/std/crypto/aegis.zig b/lib/std/crypto/aegis.zig index 2f565ce490..ea4176d13d 100644 --- a/lib/std/crypto/aegis.zig +++ b/lib/std/crypto/aegis.zig @@ -803,7 +803,7 @@ fn AegisMac(comptime T: type) type { } pub const Error = error{}; - pub const Writer = std.io.Writer(*Mac, Error, write); + pub const Writer = std.io.GenericWriter(*Mac, Error, write); fn write(self: *Mac, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/blake2.zig b/lib/std/crypto/blake2.zig index 1a285080b5..cd40978cf3 100644 --- a/lib/std/crypto/blake2.zig +++ b/lib/std/crypto/blake2.zig @@ -187,7 +187,7 @@ pub fn Blake2s(comptime out_bits: usize) type { } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/blake3.zig b/lib/std/crypto/blake3.zig index 585c338417..eecb794c9d 100644 --- a/lib/std/crypto/blake3.zig +++ b/lib/std/crypto/blake3.zig @@ -476,7 +476,7 @@ pub const Blake3 = struct { } pub const Error = error{}; - pub const Writer = std.io.Writer(*Blake3, Error, write); + pub const Writer = std.io.GenericWriter(*Blake3, Error, write); fn write(self: *Blake3, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig b/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig index f580c54546..b6c0ab20d4 100644 --- a/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig +++ b/lib/std/crypto/codecs/asn1/der/ArrayListReverse.zig @@ -45,7 +45,7 @@ pub fn prependSlice(self: *ArrayListReverse, data: []const u8) Error!void { self.data.ptr = begin; } -pub const Writer = std.io.Writer(*ArrayListReverse, Error, prependSliceSize); +pub const Writer = std.io.GenericWriter(*ArrayListReverse, Error, prependSliceSize); /// Warning: This writer writes backwards. `fn print` will NOT work as expected. pub fn writer(self: *ArrayListReverse) Writer { return .{ .context = self }; diff --git a/lib/std/crypto/sha1.zig b/lib/std/crypto/sha1.zig index 2968517b68..55b5312bd3 100644 --- a/lib/std/crypto/sha1.zig +++ b/lib/std/crypto/sha1.zig @@ -269,7 +269,7 @@ pub const Sha1 = struct { } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/sha2.zig b/lib/std/crypto/sha2.zig index f098f68ef4..20a883d49f 100644 --- a/lib/std/crypto/sha2.zig +++ b/lib/std/crypto/sha2.zig @@ -376,7 +376,7 @@ fn Sha2x32(comptime iv: Iv32, digest_bits: comptime_int) type { } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/sha3.zig b/lib/std/crypto/sha3.zig index a001538c1d..78c3ff5527 100644 --- a/lib/std/crypto/sha3.zig +++ b/lib/std/crypto/sha3.zig @@ -82,7 +82,7 @@ pub fn Keccak(comptime f: u11, comptime output_bits: u11, comptime default_delim } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); @@ -193,7 +193,7 @@ fn ShakeLike(comptime security_level: u11, comptime default_delim: u8, comptime } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); @@ -286,7 +286,7 @@ fn CShakeLike(comptime security_level: u11, comptime default_delim: u8, comptime } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); @@ -392,7 +392,7 @@ fn KMacLike(comptime security_level: u11, comptime default_delim: u8, comptime r } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); @@ -484,7 +484,7 @@ fn TupleHashLike(comptime security_level: u11, comptime default_delim: u8, compt } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/crypto/siphash.zig b/lib/std/crypto/siphash.zig index 5d1ac4f874..4334a6912d 100644 --- a/lib/std/crypto/siphash.zig +++ b/lib/std/crypto/siphash.zig @@ -240,7 +240,7 @@ fn SipHash(comptime T: type, comptime c_rounds: usize, comptime d_rounds: usize) } pub const Error = error{}; - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); fn write(self: *Self, bytes: []const u8) Error!usize { self.update(bytes); diff --git a/lib/std/debug/Pdb.zig b/lib/std/debug/Pdb.zig index 3b1adb3c56..f8e135b037 100644 --- a/lib/std/debug/Pdb.zig +++ b/lib/std/debug/Pdb.zig @@ -562,7 +562,7 @@ const MsfStream = struct { return block * self.block_size + offset; } - pub fn reader(self: *MsfStream) std.io.Reader(*MsfStream, Error, read) { + pub fn reader(self: *MsfStream) std.io.GenericReader(*MsfStream, Error, read) { return .{ .context = self }; } }; diff --git a/lib/std/fifo.zig b/lib/std/fifo.zig index fa5357cd6c..e18b5edb01 100644 --- a/lib/std/fifo.zig +++ b/lib/std/fifo.zig @@ -38,8 +38,8 @@ pub fn LinearFifo( count: usize, const Self = @This(); - pub const Reader = std.io.Reader(*Self, error{}, readFn); - pub const Writer = std.io.Writer(*Self, error{OutOfMemory}, appendWrite); + pub const Reader = std.io.GenericReader(*Self, error{}, readFn); + pub const Writer = std.io.GenericWriter(*Self, error{OutOfMemory}, appendWrite); // Type of Self argument for slice operations. // If buffer is inline (Static) then we need to ensure we haven't @@ -231,7 +231,7 @@ pub fn LinearFifo( } /// Same as `read` except it returns an error union - /// The purpose of this function existing is to match `std.io.Reader` API. + /// The purpose of this function existing is to match `std.io.GenericReader` API. fn readFn(self: *Self, dest: []u8) error{}!usize { return self.read(dest); } @@ -320,7 +320,7 @@ pub fn LinearFifo( } /// Same as `write` except it returns the number of bytes written, which is always the same - /// as `bytes.len`. The purpose of this function existing is to match `std.io.Writer` API. + /// as `bytes.len`. The purpose of this function existing is to match `std.io.GenericWriter` API. fn appendWrite(self: *Self, bytes: []const u8) error{OutOfMemory}!usize { try self.write(bytes); return bytes.len; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 30b98cddf0..35b84769e4 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -1581,13 +1581,13 @@ fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix } } -pub const Reader = io.Reader(File, ReadError, read); +pub const Reader = io.GenericReader(File, ReadError, read); pub fn reader(file: File) Reader { return .{ .context = file }; } -pub const Writer = io.Writer(File, WriteError, write); +pub const Writer = io.GenericWriter(File, WriteError, write); pub fn writer(file: File) Writer { return .{ .context = file }; diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index d36cd10aee..4dea4415a4 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -311,7 +311,7 @@ pub const Connection = struct { EndOfStream, }; - pub const Reader = std.io.Reader(*Connection, ReadError, read); + pub const Reader = std.io.GenericReader(*Connection, ReadError, read); pub fn reader(conn: *Connection) Reader { return Reader{ .context = conn }; @@ -374,7 +374,7 @@ pub const Connection = struct { UnexpectedWriteFailure, }; - pub const Writer = std.io.Writer(*Connection, WriteError, write); + pub const Writer = std.io.GenericWriter(*Connection, WriteError, write); pub fn writer(conn: *Connection) Writer { return Writer{ .context = conn }; @@ -934,7 +934,7 @@ pub const Request = struct { const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; - const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); + const TransferReader = std.io.GenericReader(*Request, TransferReadError, transferRead); fn transferReader(req: *Request) TransferReader { return .{ .context = req }; @@ -1094,7 +1094,7 @@ pub const Request = struct { pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers }; - pub const Reader = std.io.Reader(*Request, ReadError, read); + pub const Reader = std.io.GenericReader(*Request, ReadError, read); pub fn reader(req: *Request) Reader { return .{ .context = req }; @@ -1134,7 +1134,7 @@ pub const Request = struct { pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; - pub const Writer = std.io.Writer(*Request, WriteError, write); + pub const Writer = std.io.GenericWriter(*Request, WriteError, write); pub fn writer(req: *Request) Writer { return .{ .context = req }; diff --git a/lib/std/http/protocol.zig b/lib/std/http/protocol.zig index fc00a68ec3..797ed989ad 100644 --- a/lib/std/http/protocol.zig +++ b/lib/std/http/protocol.zig @@ -344,7 +344,7 @@ const MockBufferedConnection = struct { } pub const ReadError = std.io.FixedBufferStream([]const u8).ReadError || error{EndOfStream}; - pub const Reader = std.io.Reader(*MockBufferedConnection, ReadError, read); + pub const Reader = std.io.GenericReader(*MockBufferedConnection, ReadError, read); pub fn reader(conn: *MockBufferedConnection) Reader { return Reader{ .context = conn }; @@ -359,7 +359,7 @@ const MockBufferedConnection = struct { } pub const WriteError = std.io.FixedBufferStream([]const u8).WriteError; - pub const Writer = std.io.Writer(*MockBufferedConnection, WriteError, write); + pub const Writer = std.io.GenericWriter(*MockBufferedConnection, WriteError, write); pub fn writer(conn: *MockBufferedConnection) Writer { return Writer{ .context = conn }; diff --git a/lib/std/io.zig b/lib/std/io.zig index 597b8d5ec1..77643bb716 100644 --- a/lib/std/io.zig +++ b/lib/std/io.zig @@ -14,6 +14,69 @@ const File = std.fs.File; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; +pub const Limit = enum(usize) { + nothing = 0, + unlimited = std.math.maxInt(usize), + _, + + /// `std.math.maxInt(usize)` is interpreted to mean `.unlimited`. + pub fn limited(n: usize) Limit { + return @enumFromInt(n); + } + + pub fn countVec(data: []const []const u8) Limit { + var total: usize = 0; + for (data) |d| total += d.len; + return .limited(total); + } + + pub fn min(a: Limit, b: Limit) Limit { + return @enumFromInt(@min(@intFromEnum(a), @intFromEnum(b))); + } + + pub fn minInt(l: Limit, n: usize) usize { + return @min(n, @intFromEnum(l)); + } + + pub fn slice(l: Limit, s: []u8) []u8 { + return s[0..l.minInt(s.len)]; + } + + pub fn sliceConst(l: Limit, s: []const u8) []const u8 { + return s[0..l.minInt(s.len)]; + } + + pub fn toInt(l: Limit) ?usize { + return switch (l) { + else => @intFromEnum(l), + .unlimited => null, + }; + } + + /// Reduces a slice to account for the limit, leaving room for one extra + /// byte above the limit, allowing for the use case of differentiating + /// between end-of-stream and reaching the limit. + pub fn slice1(l: Limit, non_empty_buffer: []u8) []u8 { + assert(non_empty_buffer.len >= 1); + return non_empty_buffer[0..@min(@intFromEnum(l) +| 1, non_empty_buffer.len)]; + } + + pub fn nonzero(l: Limit) bool { + return @intFromEnum(l) > 0; + } + + /// Return a new limit reduced by `amount` or return `null` indicating + /// limit would be exceeded. + pub fn subtract(l: Limit, amount: usize) ?Limit { + if (l == .unlimited) return .unlimited; + if (amount > @intFromEnum(l)) return null; + return @enumFromInt(@intFromEnum(l) - amount); + } +}; + +pub const Reader = @import("io/Reader.zig"); +pub const Writer = @import("io/Writer.zig"); + fn getStdOutHandle() posix.fd_t { if (is_windows) { return windows.peb().ProcessParameters.hStdOutput; @@ -62,6 +125,7 @@ pub fn getStdIn() File { return .{ .handle = getStdInHandle() }; } +/// Deprecated in favor of `Reader`. pub fn GenericReader( comptime Context: type, comptime ReadError: type, @@ -289,6 +353,7 @@ pub fn GenericReader( }; } +/// Deprecated in favor of `Writer`. pub fn GenericWriter( comptime Context: type, comptime WriteError: type, @@ -350,15 +415,10 @@ pub fn GenericWriter( }; } -/// Deprecated; consider switching to `AnyReader` or use `GenericReader` -/// to use previous API. -pub const Reader = GenericReader; -/// Deprecated; consider switching to `AnyWriter` or use `GenericWriter` -/// to use previous API. -pub const Writer = GenericWriter; - -pub const AnyReader = @import("io/Reader.zig"); -pub const AnyWriter = @import("io/Writer.zig"); +/// Deprecated in favor of `Reader`. +pub const AnyReader = @import("io/DeprecatedReader.zig"); +/// Deprecated in favor of `Writer`. +pub const AnyWriter = @import("io/DeprecatedWriter.zig"); pub const SeekableStream = @import("io/seekable_stream.zig").SeekableStream; @@ -819,8 +879,8 @@ pub fn PollFiles(comptime StreamEnum: type) type { } test { - _ = AnyReader; - _ = AnyWriter; + _ = Reader; + _ = Writer; _ = @import("io/bit_reader.zig"); _ = @import("io/bit_writer.zig"); _ = @import("io/buffered_atomic_file.zig"); diff --git a/lib/std/io/DeprecatedReader.zig b/lib/std/io/DeprecatedReader.zig new file mode 100644 index 0000000000..3f2429c3ae --- /dev/null +++ b/lib/std/io/DeprecatedReader.zig @@ -0,0 +1,386 @@ +context: *const anyopaque, +readFn: *const fn (context: *const anyopaque, buffer: []u8) anyerror!usize, + +pub const Error = anyerror; + +/// Returns the number of bytes read. It may be less than buffer.len. +/// If the number of bytes read is 0, it means end of stream. +/// End of stream is not an error condition. +pub fn read(self: Self, buffer: []u8) anyerror!usize { + return self.readFn(self.context, buffer); +} + +/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it +/// means the stream reached the end. Reaching the end of a stream is not an error +/// condition. +pub fn readAll(self: Self, buffer: []u8) anyerror!usize { + return readAtLeast(self, buffer, buffer.len); +} + +/// Returns the number of bytes read, calling the underlying read +/// function the minimal number of times until the buffer has at least +/// `len` bytes filled. If the number read is less than `len` it means +/// the stream reached the end. Reaching the end of the stream is not +/// an error condition. +pub fn readAtLeast(self: Self, buffer: []u8, len: usize) anyerror!usize { + assert(len <= buffer.len); + var index: usize = 0; + while (index < len) { + const amt = try self.read(buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; +} + +/// If the number read would be smaller than `buf.len`, `error.EndOfStream` is returned instead. +pub fn readNoEof(self: Self, buf: []u8) anyerror!void { + const amt_read = try self.readAll(buf); + if (amt_read < buf.len) return error.EndOfStream; +} + +/// Appends to the `std.ArrayList` contents by reading from the stream +/// until end of stream is found. +/// If the number of bytes appended would exceed `max_append_size`, +/// `error.StreamTooLong` is returned +/// and the `std.ArrayList` has exactly `max_append_size` bytes appended. +pub fn readAllArrayList( + self: Self, + array_list: *std.ArrayList(u8), + max_append_size: usize, +) anyerror!void { + return self.readAllArrayListAligned(null, array_list, max_append_size); +} + +pub fn readAllArrayListAligned( + self: Self, + comptime alignment: ?Alignment, + array_list: *std.ArrayListAligned(u8, alignment), + max_append_size: usize, +) anyerror!void { + try array_list.ensureTotalCapacity(@min(max_append_size, 4096)); + const original_len = array_list.items.len; + var start_index: usize = original_len; + while (true) { + array_list.expandToCapacity(); + const dest_slice = array_list.items[start_index..]; + const bytes_read = try self.readAll(dest_slice); + start_index += bytes_read; + + if (start_index - original_len > max_append_size) { + array_list.shrinkAndFree(original_len + max_append_size); + return error.StreamTooLong; + } + + if (bytes_read != dest_slice.len) { + array_list.shrinkAndFree(start_index); + return; + } + + // This will trigger ArrayList to expand superlinearly at whatever its growth rate is. + try array_list.ensureTotalCapacity(start_index + 1); + } +} + +/// Allocates enough memory to hold all the contents of the stream. If the allocated +/// memory would be greater than `max_size`, returns `error.StreamTooLong`. +/// Caller owns returned memory. +/// If this function returns an error, the contents from the stream read so far are lost. +pub fn readAllAlloc(self: Self, allocator: mem.Allocator, max_size: usize) anyerror![]u8 { + var array_list = std.ArrayList(u8).init(allocator); + defer array_list.deinit(); + try self.readAllArrayList(&array_list, max_size); + return try array_list.toOwnedSlice(); +} + +/// Deprecated: use `streamUntilDelimiter` with ArrayList's writer instead. +/// Replaces the `std.ArrayList` contents by reading from the stream until `delimiter` is found. +/// Does not include the delimiter in the result. +/// If the `std.ArrayList` length would exceed `max_size`, `error.StreamTooLong` is returned and the +/// `std.ArrayList` is populated with `max_size` bytes from the stream. +pub fn readUntilDelimiterArrayList( + self: Self, + array_list: *std.ArrayList(u8), + delimiter: u8, + max_size: usize, +) anyerror!void { + array_list.shrinkRetainingCapacity(0); + try self.streamUntilDelimiter(array_list.writer(), delimiter, max_size); +} + +/// Deprecated: use `streamUntilDelimiter` with ArrayList's writer instead. +/// Allocates enough memory to read until `delimiter`. If the allocated +/// memory would be greater than `max_size`, returns `error.StreamTooLong`. +/// Caller owns returned memory. +/// If this function returns an error, the contents from the stream read so far are lost. +pub fn readUntilDelimiterAlloc( + self: Self, + allocator: mem.Allocator, + delimiter: u8, + max_size: usize, +) anyerror![]u8 { + var array_list = std.ArrayList(u8).init(allocator); + defer array_list.deinit(); + try self.streamUntilDelimiter(array_list.writer(), delimiter, max_size); + return try array_list.toOwnedSlice(); +} + +/// Deprecated: use `streamUntilDelimiter` with FixedBufferStream's writer instead. +/// Reads from the stream until specified byte is found. If the buffer is not +/// large enough to hold the entire contents, `error.StreamTooLong` is returned. +/// If end-of-stream is found, `error.EndOfStream` is returned. +/// Returns a slice of the stream data, with ptr equal to `buf.ptr`. The +/// delimiter byte is written to the output buffer but is not included +/// in the returned slice. +pub fn readUntilDelimiter(self: Self, buf: []u8, delimiter: u8) anyerror![]u8 { + var fbs = std.io.fixedBufferStream(buf); + try self.streamUntilDelimiter(fbs.writer(), delimiter, fbs.buffer.len); + const output = fbs.getWritten(); + buf[output.len] = delimiter; // emulating old behaviour + return output; +} + +/// Deprecated: use `streamUntilDelimiter` with ArrayList's (or any other's) writer instead. +/// Allocates enough memory to read until `delimiter` or end-of-stream. +/// If the allocated memory would be greater than `max_size`, returns +/// `error.StreamTooLong`. If end-of-stream is found, returns the rest +/// of the stream. If this function is called again after that, returns +/// null. +/// Caller owns returned memory. +/// If this function returns an error, the contents from the stream read so far are lost. +pub fn readUntilDelimiterOrEofAlloc( + self: Self, + allocator: mem.Allocator, + delimiter: u8, + max_size: usize, +) anyerror!?[]u8 { + var array_list = std.ArrayList(u8).init(allocator); + defer array_list.deinit(); + self.streamUntilDelimiter(array_list.writer(), delimiter, max_size) catch |err| switch (err) { + error.EndOfStream => if (array_list.items.len == 0) { + return null; + }, + else => |e| return e, + }; + return try array_list.toOwnedSlice(); +} + +/// Deprecated: use `streamUntilDelimiter` with FixedBufferStream's writer instead. +/// Reads from the stream until specified byte is found. If the buffer is not +/// large enough to hold the entire contents, `error.StreamTooLong` is returned. +/// If end-of-stream is found, returns the rest of the stream. If this +/// function is called again after that, returns null. +/// Returns a slice of the stream data, with ptr equal to `buf.ptr`. The +/// delimiter byte is written to the output buffer but is not included +/// in the returned slice. +pub fn readUntilDelimiterOrEof(self: Self, buf: []u8, delimiter: u8) anyerror!?[]u8 { + var fbs = std.io.fixedBufferStream(buf); + self.streamUntilDelimiter(fbs.writer(), delimiter, fbs.buffer.len) catch |err| switch (err) { + error.EndOfStream => if (fbs.getWritten().len == 0) { + return null; + }, + + else => |e| return e, + }; + const output = fbs.getWritten(); + buf[output.len] = delimiter; // emulating old behaviour + return output; +} + +/// Appends to the `writer` contents by reading from the stream until `delimiter` is found. +/// Does not write the delimiter itself. +/// If `optional_max_size` is not null and amount of written bytes exceeds `optional_max_size`, +/// returns `error.StreamTooLong` and finishes appending. +/// If `optional_max_size` is null, appending is unbounded. +pub fn streamUntilDelimiter( + self: Self, + writer: anytype, + delimiter: u8, + optional_max_size: ?usize, +) anyerror!void { + if (optional_max_size) |max_size| { + for (0..max_size) |_| { + const byte: u8 = try self.readByte(); + if (byte == delimiter) return; + try writer.writeByte(byte); + } + return error.StreamTooLong; + } else { + while (true) { + const byte: u8 = try self.readByte(); + if (byte == delimiter) return; + try writer.writeByte(byte); + } + // Can not throw `error.StreamTooLong` since there are no boundary. + } +} + +/// Reads from the stream until specified byte is found, discarding all data, +/// including the delimiter. +/// If end-of-stream is found, this function succeeds. +pub fn skipUntilDelimiterOrEof(self: Self, delimiter: u8) anyerror!void { + while (true) { + const byte = self.readByte() catch |err| switch (err) { + error.EndOfStream => return, + else => |e| return e, + }; + if (byte == delimiter) return; + } +} + +/// Reads 1 byte from the stream or returns `error.EndOfStream`. +pub fn readByte(self: Self) anyerror!u8 { + var result: [1]u8 = undefined; + const amt_read = try self.read(result[0..]); + if (amt_read < 1) return error.EndOfStream; + return result[0]; +} + +/// Same as `readByte` except the returned byte is signed. +pub fn readByteSigned(self: Self) anyerror!i8 { + return @as(i8, @bitCast(try self.readByte())); +} + +/// Reads exactly `num_bytes` bytes and returns as an array. +/// `num_bytes` must be comptime-known +pub fn readBytesNoEof(self: Self, comptime num_bytes: usize) anyerror![num_bytes]u8 { + var bytes: [num_bytes]u8 = undefined; + try self.readNoEof(&bytes); + return bytes; +} + +/// Reads bytes until `bounded.len` is equal to `num_bytes`, +/// or the stream ends. +/// +/// * it is assumed that `num_bytes` will not exceed `bounded.capacity()` +pub fn readIntoBoundedBytes( + self: Self, + comptime num_bytes: usize, + bounded: *std.BoundedArray(u8, num_bytes), +) anyerror!void { + while (bounded.len < num_bytes) { + // get at most the number of bytes free in the bounded array + const bytes_read = try self.read(bounded.unusedCapacitySlice()); + if (bytes_read == 0) return; + + // bytes_read will never be larger than @TypeOf(bounded.len) + // due to `self.read` being bounded by `bounded.unusedCapacitySlice()` + bounded.len += @as(@TypeOf(bounded.len), @intCast(bytes_read)); + } +} + +/// Reads at most `num_bytes` and returns as a bounded array. +pub fn readBoundedBytes(self: Self, comptime num_bytes: usize) anyerror!std.BoundedArray(u8, num_bytes) { + var result = std.BoundedArray(u8, num_bytes){}; + try self.readIntoBoundedBytes(num_bytes, &result); + return result; +} + +pub inline fn readInt(self: Self, comptime T: type, endian: std.builtin.Endian) anyerror!T { + const bytes = try self.readBytesNoEof(@divExact(@typeInfo(T).int.bits, 8)); + return mem.readInt(T, &bytes, endian); +} + +pub fn readVarInt( + self: Self, + comptime ReturnType: type, + endian: std.builtin.Endian, + size: usize, +) anyerror!ReturnType { + assert(size <= @sizeOf(ReturnType)); + var bytes_buf: [@sizeOf(ReturnType)]u8 = undefined; + const bytes = bytes_buf[0..size]; + try self.readNoEof(bytes); + return mem.readVarInt(ReturnType, bytes, endian); +} + +/// Optional parameters for `skipBytes` +pub const SkipBytesOptions = struct { + buf_size: usize = 512, +}; + +// `num_bytes` is a `u64` to match `off_t` +/// Reads `num_bytes` bytes from the stream and discards them +pub fn skipBytes(self: Self, num_bytes: u64, comptime options: SkipBytesOptions) anyerror!void { + var buf: [options.buf_size]u8 = undefined; + var remaining = num_bytes; + + while (remaining > 0) { + const amt = @min(remaining, options.buf_size); + try self.readNoEof(buf[0..amt]); + remaining -= amt; + } +} + +/// Reads `slice.len` bytes from the stream and returns if they are the same as the passed slice +pub fn isBytes(self: Self, slice: []const u8) anyerror!bool { + var i: usize = 0; + var matches = true; + while (i < slice.len) : (i += 1) { + if (slice[i] != try self.readByte()) { + matches = false; + } + } + return matches; +} + +pub fn readStruct(self: Self, comptime T: type) anyerror!T { + // Only extern and packed structs have defined in-memory layout. + comptime assert(@typeInfo(T).@"struct".layout != .auto); + var res: [1]T = undefined; + try self.readNoEof(mem.sliceAsBytes(res[0..])); + return res[0]; +} + +pub fn readStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian) anyerror!T { + var res = try self.readStruct(T); + if (native_endian != endian) { + mem.byteSwapAllFields(T, &res); + } + return res; +} + +/// Reads an integer with the same size as the given enum's tag type. If the integer matches +/// an enum tag, casts the integer to the enum tag and returns it. Otherwise, returns an `error.InvalidValue`. +/// TODO optimization taking advantage of most fields being in order +pub fn readEnum(self: Self, comptime Enum: type, endian: std.builtin.Endian) anyerror!Enum { + const E = error{ + /// An integer was read, but it did not match any of the tags in the supplied enum. + InvalidValue, + }; + const type_info = @typeInfo(Enum).@"enum"; + const tag = try self.readInt(type_info.tag_type, endian); + + inline for (std.meta.fields(Enum)) |field| { + if (tag == field.value) { + return @field(Enum, field.name); + } + } + + return E.InvalidValue; +} + +/// Reads the stream until the end, ignoring all the data. +/// Returns the number of bytes discarded. +pub fn discard(self: Self) anyerror!u64 { + var trash: [4096]u8 = undefined; + var index: u64 = 0; + while (true) { + const n = try self.read(&trash); + if (n == 0) return index; + index += n; + } +} + +const std = @import("../std.zig"); +const Self = @This(); +const math = std.math; +const assert = std.debug.assert; +const mem = std.mem; +const testing = std.testing; +const native_endian = @import("builtin").target.cpu.arch.endian(); +const Alignment = std.mem.Alignment; + +test { + _ = @import("Reader/test.zig"); +} diff --git a/lib/std/io/DeprecatedWriter.zig b/lib/std/io/DeprecatedWriter.zig new file mode 100644 index 0000000000..26d4f88def --- /dev/null +++ b/lib/std/io/DeprecatedWriter.zig @@ -0,0 +1,83 @@ +const std = @import("../std.zig"); +const assert = std.debug.assert; +const mem = std.mem; +const native_endian = @import("builtin").target.cpu.arch.endian(); + +context: *const anyopaque, +writeFn: *const fn (context: *const anyopaque, bytes: []const u8) anyerror!usize, + +const Self = @This(); +pub const Error = anyerror; + +pub fn write(self: Self, bytes: []const u8) anyerror!usize { + return self.writeFn(self.context, bytes); +} + +pub fn writeAll(self: Self, bytes: []const u8) anyerror!void { + var index: usize = 0; + while (index != bytes.len) { + index += try self.write(bytes[index..]); + } +} + +pub fn print(self: Self, comptime format: []const u8, args: anytype) anyerror!void { + return std.fmt.format(self, format, args); +} + +pub fn writeByte(self: Self, byte: u8) anyerror!void { + const array = [1]u8{byte}; + return self.writeAll(&array); +} + +pub fn writeByteNTimes(self: Self, byte: u8, n: usize) anyerror!void { + var bytes: [256]u8 = undefined; + @memset(bytes[0..], byte); + + var remaining: usize = n; + while (remaining > 0) { + const to_write = @min(remaining, bytes.len); + try self.writeAll(bytes[0..to_write]); + remaining -= to_write; + } +} + +pub fn writeBytesNTimes(self: Self, bytes: []const u8, n: usize) anyerror!void { + var i: usize = 0; + while (i < n) : (i += 1) { + try self.writeAll(bytes); + } +} + +pub inline fn writeInt(self: Self, comptime T: type, value: T, endian: std.builtin.Endian) anyerror!void { + var bytes: [@divExact(@typeInfo(T).int.bits, 8)]u8 = undefined; + mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian); + return self.writeAll(&bytes); +} + +pub fn writeStruct(self: Self, value: anytype) anyerror!void { + // Only extern and packed structs have defined in-memory layout. + comptime assert(@typeInfo(@TypeOf(value)).@"struct".layout != .auto); + return self.writeAll(mem.asBytes(&value)); +} + +pub fn writeStructEndian(self: Self, value: anytype, endian: std.builtin.Endian) anyerror!void { + // TODO: make sure this value is not a reference type + if (native_endian == endian) { + return self.writeStruct(value); + } else { + var copy = value; + mem.byteSwapAllFields(@TypeOf(value), ©); + return self.writeStruct(copy); + } +} + +pub fn writeFile(self: Self, file: std.fs.File) anyerror!void { + // TODO: figure out how to adjust std lib abstractions so that this ends up + // doing sendfile or maybe even copy_file_range under the right conditions. + var buf: [4000]u8 = undefined; + while (true) { + const n = try file.readAll(&buf); + try self.writeAll(buf[0..n]); + if (n < buf.len) return; + } +} diff --git a/lib/std/io/Reader.zig b/lib/std/io/Reader.zig index 3f2429c3ae..d26c18955b 100644 --- a/lib/std/io/Reader.zig +++ b/lib/std/io/Reader.zig @@ -1,386 +1,1471 @@ -context: *const anyopaque, -readFn: *const fn (context: *const anyopaque, buffer: []u8) anyerror!usize, +const Reader = @This(); -pub const Error = anyerror; +const builtin = @import("builtin"); +const native_endian = builtin.target.cpu.arch.endian(); -/// Returns the number of bytes read. It may be less than buffer.len. -/// If the number of bytes read is 0, it means end of stream. -/// End of stream is not an error condition. -pub fn read(self: Self, buffer: []u8) anyerror!usize { - return self.readFn(self.context, buffer); +const std = @import("../std.zig"); +const Writer = std.io.Writer; +const assert = std.debug.assert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; +const Limit = std.io.Limit; + +pub const Limited = @import("Reader/Limited.zig"); + +vtable: *const VTable, +buffer: []u8, +/// Number of bytes which have been consumed from `buffer`. +seek: usize, +/// In `buffer` before this are buffered bytes, after this is `undefined`. +end: usize, + +pub const VTable = struct { + /// Writes bytes from the internally tracked logical position to `w`. + /// + /// Returns the number of bytes written, which will be at minimum `0` and + /// at most `limit`. The number returned, including zero, does not indicate + /// end of stream. `limit` is guaranteed to be at least as large as the + /// buffer capacity of `w`. + /// + /// The reader's internal logical seek position moves forward in accordance + /// with the number of bytes returned from this function. + /// + /// Implementations are encouraged to utilize mandatory minimum buffer + /// sizes combined with short reads (returning a value less than `limit`) + /// in order to minimize complexity. + /// + /// This function is always called when `buffer` is empty. + stream: *const fn (r: *Reader, w: *Writer, limit: Limit) StreamError!usize, + + /// Consumes bytes from the internally tracked stream position without + /// providing access to them. + /// + /// Returns the number of bytes discarded, which will be at minimum `0` and + /// at most `limit`. The number of bytes returned, including zero, does not + /// indicate end of stream. + /// + /// The reader's internal logical seek position moves forward in accordance + /// with the number of bytes returned from this function. + /// + /// Implementations are encouraged to utilize mandatory minimum buffer + /// sizes combined with short reads (returning a value less than `limit`) + /// in order to minimize complexity. + /// + /// The default implementation is is based on calling `stream`, borrowing + /// `buffer` to construct a temporary `Writer` and ignoring the written + /// data. + discard: *const fn (r: *Reader, limit: Limit) Error!usize = defaultDiscard, +}; + +pub const StreamError = error{ + /// See the `Reader` implementation for detailed diagnostics. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, + /// End of stream indicated from the `Reader`. This error cannot originate + /// from the `Writer`. + EndOfStream, +}; + +pub const Error = error{ + /// See the `Reader` implementation for detailed diagnostics. + ReadFailed, + EndOfStream, +}; + +pub const StreamRemainingError = error{ + /// See the `Reader` implementation for detailed diagnostics. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, +}; + +pub const ShortError = error{ + /// See the `Reader` implementation for detailed diagnostics. + ReadFailed, +}; + +pub const failing: Reader = .{ + .vtable = &.{ + .read = failingStream, + .discard = failingDiscard, + }, + .buffer = &.{}, + .seek = 0, + .end = 0, +}; + +/// This is generally safe to `@constCast` because it has an empty buffer, so +/// there is not really a way to accidentally attempt mutation of these fields. +const ending_state: Reader = .fixed(&.{}); +pub const ending: *Reader = @constCast(&ending_state); + +pub fn limited(r: *Reader, limit: Limit, buffer: []u8) Limited { + return Limited.init(r, limit, buffer); } -/// Returns the number of bytes read. If the number read is smaller than `buffer.len`, it -/// means the stream reached the end. Reaching the end of a stream is not an error -/// condition. -pub fn readAll(self: Self, buffer: []u8) anyerror!usize { - return readAtLeast(self, buffer, buffer.len); +/// Constructs a `Reader` such that it will read from `buffer` and then end. +pub fn fixed(buffer: []const u8) Reader { + return .{ + .vtable = &.{ + .stream = endingStream, + .discard = endingDiscard, + }, + // This cast is safe because all potential writes to it will instead + // return `error.EndOfStream`. + .buffer = @constCast(buffer), + .end = buffer.len, + .seek = 0, + }; } -/// Returns the number of bytes read, calling the underlying read -/// function the minimal number of times until the buffer has at least -/// `len` bytes filled. If the number read is less than `len` it means -/// the stream reached the end. Reaching the end of the stream is not -/// an error condition. -pub fn readAtLeast(self: Self, buffer: []u8, len: usize) anyerror!usize { - assert(len <= buffer.len); - var index: usize = 0; - while (index < len) { - const amt = try self.read(buffer[index..]); - if (amt == 0) break; - index += amt; +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 before = w.count; + const n = try r.vtable.stream(r, w, limit); + assert(n <= @intFromEnum(limit)); + assert(w.count == before + n); + return n; +} + +pub fn discard(r: *Reader, limit: Limit) Error!usize { + const buffered_len = r.end - r.seek; + const remaining: Limit = if (limit.toInt()) |n| l: { + if (buffered_len >= n) { + r.seek += n; + return n; + } + break :l .limited(n - buffered_len); + } else .unlimited; + r.seek = 0; + r.end = 0; + const n = try r.vtable.discard(r, remaining); + assert(n <= @intFromEnum(remaining)); + return buffered_len + n; +} + +pub fn defaultDiscard(r: *Reader, limit: Limit) Error!usize { + assert(r.seek == 0); + assert(r.end == 0); + var w: Writer = .discarding(r.buffer); + const n = r.stream(&w, limit) catch |err| switch (err) { + error.WriteFailed => unreachable, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => return error.EndOfStream, + }; + if (n > @intFromEnum(limit)) { + const over_amt = n - @intFromEnum(limit); + r.seek = w.end - over_amt; + r.end = w.end; + assert(r.end <= w.buffer.len); // limit may be exceeded only by an amount within buffer capacity. + return @intFromEnum(limit); } - return index; -} - -/// If the number read would be smaller than `buf.len`, `error.EndOfStream` is returned instead. -pub fn readNoEof(self: Self, buf: []u8) anyerror!void { - const amt_read = try self.readAll(buf); - if (amt_read < buf.len) return error.EndOfStream; -} - -/// Appends to the `std.ArrayList` contents by reading from the stream -/// until end of stream is found. -/// If the number of bytes appended would exceed `max_append_size`, -/// `error.StreamTooLong` is returned -/// and the `std.ArrayList` has exactly `max_append_size` bytes appended. -pub fn readAllArrayList( - self: Self, - array_list: *std.ArrayList(u8), - max_append_size: usize, -) anyerror!void { - return self.readAllArrayListAligned(null, array_list, max_append_size); -} - -pub fn readAllArrayListAligned( - self: Self, - comptime alignment: ?Alignment, - array_list: *std.ArrayListAligned(u8, alignment), - max_append_size: usize, -) anyerror!void { - try array_list.ensureTotalCapacity(@min(max_append_size, 4096)); - const original_len = array_list.items.len; - var start_index: usize = original_len; + return n; +} + +/// "Pump" exactly `n` bytes from the reader to the writer. +pub fn streamExact(r: *Reader, w: *Writer, n: usize) StreamError!void { + var remaining = n; + while (remaining != 0) remaining -= try r.stream(w, .limited(remaining)); +} + +/// "Pump" data from the reader to the writer, handling `error.EndOfStream` as +/// a success case. +/// +/// Returns total number of bytes written to `w`. +pub fn streamRemaining(r: *Reader, w: *Writer) StreamRemainingError!usize { + var offset: usize = 0; while (true) { - array_list.expandToCapacity(); - const dest_slice = array_list.items[start_index..]; - const bytes_read = try self.readAll(dest_slice); - start_index += bytes_read; + offset += r.stream(w, .unlimited) catch |err| switch (err) { + error.EndOfStream => return offset, + else => |e| return e, + }; + } +} + +/// Consumes the stream until the end, ignoring all the data, returning the +/// number of bytes discarded. +pub fn discardRemaining(r: *Reader) ShortError!usize { + var offset: usize = r.end; + r.seek = 0; + r.end = 0; + while (true) { + offset += r.vtable.discard(r, .unlimited) catch |err| switch (err) { + error.EndOfStream => return offset, + else => |e| return e, + }; + } +} + +pub const LimitedAllocError = Allocator.Error || ShortError || error{StreamTooLong}; + +/// Transfers all bytes from the current position to the end of the stream, up +/// to `limit`, returning them as a caller-owned allocated slice. +/// +/// If `limit` would be exceeded, `error.StreamTooLong` is returned instead. In +/// such case, the next byte that would be read will be the first one to exceed +/// `limit`, and all preceeding bytes have been discarded. +/// +/// Asserts `buffer` has nonzero capacity. +/// +/// See also: +/// * `appendRemaining` +pub fn allocRemaining(r: *Reader, gpa: Allocator, limit: Limit) LimitedAllocError![]u8 { + var buffer: ArrayList(u8) = .empty; + defer buffer.deinit(gpa); + try appendRemaining(r, gpa, null, &buffer, limit); + return buffer.toOwnedSlice(gpa); +} - if (start_index - original_len > max_append_size) { - array_list.shrinkAndFree(original_len + max_append_size); +/// Transfers all bytes from the current position to the end of the stream, up +/// to `limit`, appending them to `list`. +/// +/// If `limit` would be exceeded, `error.StreamTooLong` is returned instead. In +/// such case, the next byte that would be read will be the first one to exceed +/// `limit`, and all preceeding bytes have been appended to `list`. +/// +/// Asserts `buffer` has nonzero capacity. +/// +/// See also: +/// * `allocRemaining` +pub fn appendRemaining( + r: *Reader, + gpa: Allocator, + comptime alignment: ?std.mem.Alignment, + list: *std.ArrayListAlignedUnmanaged(u8, alignment), + limit: Limit, +) LimitedAllocError!void { + const buffer = r.buffer; + const buffer_contents = buffer[r.seek..r.end]; + const copy_len = limit.minInt(buffer_contents.len); + try list.ensureUnusedCapacity(gpa, copy_len); + @memcpy(list.unusedCapacitySlice()[0..copy_len], buffer[0..copy_len]); + list.items.len += copy_len; + r.seek += copy_len; + if (copy_len == buffer_contents.len) { + r.seek = 0; + r.end = 0; + } + var remaining = limit.subtract(copy_len).?; + while (true) { + try list.ensureUnusedCapacity(gpa, 1); + const dest = remaining.slice(list.unusedCapacitySlice()); + const additional_buffer: []u8 = if (@intFromEnum(remaining) == dest.len) buffer else &.{}; + const n = readVec(r, &.{ dest, additional_buffer }) catch |err| switch (err) { + error.EndOfStream => break, + error.ReadFailed => return error.ReadFailed, + }; + if (n >= dest.len) { + r.end = n - dest.len; + list.items.len += dest.len; + if (n == dest.len) return; return error.StreamTooLong; } + list.items.len += n; + remaining = remaining.subtract(n).?; + } +} + +/// Writes bytes from the internally tracked stream position to `data`. +/// +/// Returns the number of bytes written, which will be at minimum `0` and +/// at most the sum of each data slice length. The number of bytes read, +/// including zero, does not indicate end of stream. +/// +/// The reader's internal logical seek position moves forward in accordance +/// with the number of bytes returned from this function. +pub fn readVec(r: *Reader, data: []const []u8) Error!usize { + return readVecLimit(r, data, .unlimited); +} - if (bytes_read != dest_slice.len) { - array_list.shrinkAndFree(start_index); - return; +/// Equivalent to `readVec` but reads at most `limit` bytes. +/// +/// This ultimately will lower to a call to `stream`, but it must ensure +/// that the buffer used has at least as much capacity, in case that function +/// depends on a minimum buffer capacity. It also ensures that if the `stream` +/// implementation calls `Writer.writableVector`, it will get this data slice +/// along with the buffer at the end. +pub fn readVecLimit(r: *Reader, data: []const []u8, limit: Limit) Error!usize { + comptime assert(@intFromEnum(Limit.unlimited) == std.math.maxInt(usize)); + var remaining = @intFromEnum(limit); + for (data, 0..) |buf, i| { + const buffer_contents = r.buffer[r.seek..r.end]; + const copy_len = @min(buffer_contents.len, buf.len, remaining); + @memcpy(buf[0..copy_len], buffer_contents[0..copy_len]); + r.seek += copy_len; + remaining -= copy_len; + if (remaining == 0) break; + if (buf.len - copy_len == 0) continue; + + // All of `buffer` has been copied to `data`. We now set up a structure + // that enables the `Writer.writableVector` API, while also ensuring + // API that directly operates on the `Writable.buffer` has its minimum + // buffer capacity requirements met. + r.seek = 0; + r.end = 0; + const first = buf[copy_len..]; + const middle = data[i + 1 ..]; + var wrapper: Writer.VectorWrapper = .{ + .it = .{ + .first = first, + .middle = middle, + .last = r.buffer, + }, + .writer = .{ + .buffer = if (first.len >= r.buffer.len) first else r.buffer, + .vtable = &Writer.VectorWrapper.vtable, + }, + }; + var n = r.vtable.stream(r, &wrapper.writer, .limited(remaining)) catch |err| switch (err) { + error.WriteFailed => { + if (wrapper.writer.buffer.ptr == first.ptr) { + remaining -= wrapper.writer.end; + } else { + r.end = wrapper.writer.end; + } + break; + }, + else => |e| return e, + }; + if (wrapper.writer.buffer.ptr != first.ptr) { + r.end = n; + break; + } + if (n < first.len) { + remaining -= n; + break; } + remaining -= first.len; + n -= first.len; + for (middle) |mid| { + if (n < mid.len) { + remaining -= n; + break; + } + remaining -= mid.len; + n -= mid.len; + } + r.end = n; + break; + } + return @intFromEnum(limit) - remaining; +} + +pub fn buffered(r: *Reader) []u8 { + return r.buffer[r.seek..r.end]; +} + +pub fn bufferedLen(r: *const Reader) usize { + return r.end - r.seek; +} + +pub fn hashed(r: *Reader, hasher: anytype) Hashed(@TypeOf(hasher)) { + return .{ .in = r, .hasher = hasher }; +} - // This will trigger ArrayList to expand superlinearly at whatever its growth rate is. - try array_list.ensureTotalCapacity(start_index + 1); +pub fn readVecAll(r: *Reader, data: [][]u8) Error!void { + var index: usize = 0; + var truncate: usize = 0; + while (index < data.len) { + { + const untruncated = data[index]; + data[index] = untruncated[truncate..]; + defer data[index] = untruncated; + truncate += try r.readVec(data[index..]); + } + while (index < data.len and truncate >= data[index].len) { + truncate -= data[index].len; + index += 1; + } } } -/// Allocates enough memory to hold all the contents of the stream. If the allocated -/// memory would be greater than `max_size`, returns `error.StreamTooLong`. -/// Caller owns returned memory. -/// If this function returns an error, the contents from the stream read so far are lost. -pub fn readAllAlloc(self: Self, allocator: mem.Allocator, max_size: usize) anyerror![]u8 { - var array_list = std.ArrayList(u8).init(allocator); - defer array_list.deinit(); - try self.readAllArrayList(&array_list, max_size); - return try array_list.toOwnedSlice(); -} - -/// Deprecated: use `streamUntilDelimiter` with ArrayList's writer instead. -/// Replaces the `std.ArrayList` contents by reading from the stream until `delimiter` is found. -/// Does not include the delimiter in the result. -/// If the `std.ArrayList` length would exceed `max_size`, `error.StreamTooLong` is returned and the -/// `std.ArrayList` is populated with `max_size` bytes from the stream. -pub fn readUntilDelimiterArrayList( - self: Self, - array_list: *std.ArrayList(u8), - delimiter: u8, - max_size: usize, -) anyerror!void { - array_list.shrinkRetainingCapacity(0); - try self.streamUntilDelimiter(array_list.writer(), delimiter, max_size); -} - -/// Deprecated: use `streamUntilDelimiter` with ArrayList's writer instead. -/// Allocates enough memory to read until `delimiter`. If the allocated -/// memory would be greater than `max_size`, returns `error.StreamTooLong`. -/// Caller owns returned memory. -/// If this function returns an error, the contents from the stream read so far are lost. -pub fn readUntilDelimiterAlloc( - self: Self, - allocator: mem.Allocator, - delimiter: u8, - max_size: usize, -) anyerror![]u8 { - var array_list = std.ArrayList(u8).init(allocator); - defer array_list.deinit(); - try self.streamUntilDelimiter(array_list.writer(), delimiter, max_size); - return try array_list.toOwnedSlice(); -} - -/// Deprecated: use `streamUntilDelimiter` with FixedBufferStream's writer instead. -/// Reads from the stream until specified byte is found. If the buffer is not -/// large enough to hold the entire contents, `error.StreamTooLong` is returned. -/// If end-of-stream is found, `error.EndOfStream` is returned. -/// Returns a slice of the stream data, with ptr equal to `buf.ptr`. The -/// delimiter byte is written to the output buffer but is not included -/// in the returned slice. -pub fn readUntilDelimiter(self: Self, buf: []u8, delimiter: u8) anyerror![]u8 { - var fbs = std.io.fixedBufferStream(buf); - try self.streamUntilDelimiter(fbs.writer(), delimiter, fbs.buffer.len); - const output = fbs.getWritten(); - buf[output.len] = delimiter; // emulating old behaviour - return output; -} - -/// Deprecated: use `streamUntilDelimiter` with ArrayList's (or any other's) writer instead. -/// Allocates enough memory to read until `delimiter` or end-of-stream. -/// If the allocated memory would be greater than `max_size`, returns -/// `error.StreamTooLong`. If end-of-stream is found, returns the rest -/// of the stream. If this function is called again after that, returns -/// null. -/// Caller owns returned memory. -/// If this function returns an error, the contents from the stream read so far are lost. -pub fn readUntilDelimiterOrEofAlloc( - self: Self, - allocator: mem.Allocator, - delimiter: u8, - max_size: usize, -) anyerror!?[]u8 { - var array_list = std.ArrayList(u8).init(allocator); - defer array_list.deinit(); - self.streamUntilDelimiter(array_list.writer(), delimiter, max_size) catch |err| switch (err) { - error.EndOfStream => if (array_list.items.len == 0) { - return null; +/// Returns the next `len` bytes from the stream, filling the buffer as +/// necessary. +/// +/// Invalidates previously returned values from `peek`. +/// +/// Asserts that the `Reader` was initialized with a buffer capacity at +/// least as big as `len`. +/// +/// If there are fewer than `len` bytes left in the stream, `error.EndOfStream` +/// is returned instead. +/// +/// See also: +/// * `peek` +/// * `toss` +pub fn peek(r: *Reader, n: usize) Error![]u8 { + try r.fill(n); + return r.buffer[r.seek..][0..n]; +} + +/// Returns all the next buffered bytes, after filling the buffer to ensure it +/// contains at least `n` bytes. +/// +/// Invalidates previously returned values from `peek` and `peekGreedy`. +/// +/// Asserts that the `Reader` was initialized with a buffer capacity at +/// least as big as `n`. +/// +/// If there are fewer than `n` bytes left in the stream, `error.EndOfStream` +/// is returned instead. +/// +/// See also: +/// * `peek` +/// * `toss` +pub fn peekGreedy(r: *Reader, n: usize) Error![]u8 { + try r.fill(n); + return r.buffer[r.seek..r.end]; +} + +/// Skips the next `n` bytes from the stream, advancing the seek position. This +/// is typically and safely used after `peek`. +/// +/// Asserts that the number of bytes buffered is at least as many as `n`. +/// +/// The "tossed" memory remains alive until a "peek" operation occurs. +/// +/// See also: +/// * `peek`. +/// * `discard`. +pub fn toss(r: *Reader, n: usize) void { + r.seek += n; + assert(r.seek <= r.end); +} + +/// Equivalent to `toss(r.bufferedLen())`. +pub fn tossAll(r: *Reader) void { + r.seek = 0; + r.end = 0; +} + +/// Equivalent to `peek` followed by `toss`. +/// +/// The data returned is invalidated by the next call to `take`, `peek`, +/// `fill`, and functions with those prefixes. +pub fn take(r: *Reader, n: usize) Error![]u8 { + const result = try r.peek(n); + r.toss(n); + return result; +} + +/// Returns the next `n` bytes from the stream as an array, filling the buffer +/// as necessary and advancing the seek position `n` bytes. +/// +/// Asserts that the `Reader` was initialized with a buffer capacity at +/// least as big as `n`. +/// +/// If there are fewer than `n` bytes left in the stream, `error.EndOfStream` +/// is returned instead. +/// +/// See also: +/// * `take` +pub fn takeArray(r: *Reader, comptime n: usize) Error!*[n]u8 { + return (try r.take(n))[0..n]; +} + +/// Returns the next `n` bytes from the stream as an array, filling the buffer +/// as necessary, without advancing the seek position. +/// +/// Asserts that the `Reader` was initialized with a buffer capacity at +/// least as big as `n`. +/// +/// If there are fewer than `n` bytes left in the stream, `error.EndOfStream` +/// is returned instead. +/// +/// See also: +/// * `peek` +/// * `takeArray` +pub fn peekArray(r: *Reader, comptime n: usize) Error!*[n]u8 { + return (try r.peek(n))[0..n]; +} + +/// Skips the next `n` bytes from the stream, advancing the seek position. +/// +/// Unlike `toss` which is infallible, in this function `n` can be any amount. +/// +/// Returns `error.EndOfStream` if fewer than `n` bytes could be discarded. +/// +/// See also: +/// * `toss` +/// * `discardRemaining` +/// * `discardShort` +/// * `discard` +pub fn discardAll(r: *Reader, n: usize) Error!void { + if ((try r.discardShort(n)) != n) return error.EndOfStream; +} + +pub fn discardAll64(r: *Reader, n: u64) Error!void { + var remaining: u64 = n; + while (remaining > 0) { + const limited_remaining = std.math.cast(usize, remaining) orelse std.math.maxInt(usize); + try discardAll(r, limited_remaining); + remaining -= limited_remaining; + } +} + +/// Skips the next `n` bytes from the stream, advancing the seek position. +/// +/// Unlike `toss` which is infallible, in this function `n` can be any amount. +/// +/// Returns the number of bytes discarded, which is less than `n` if and only +/// if the stream reached the end. +/// +/// See also: +/// * `discardAll` +/// * `discardRemaining` +/// * `discard` +pub fn discardShort(r: *Reader, n: usize) ShortError!usize { + const proposed_seek = r.seek + n; + if (proposed_seek <= r.end) { + @branchHint(.likely); + r.seek = proposed_seek; + return n; + } + var remaining = n - (r.end - r.seek); + r.end = 0; + r.seek = 0; + while (true) { + const discard_len = r.vtable.discard(r, .limited(remaining)) catch |err| switch (err) { + error.EndOfStream => return n - remaining, + error.ReadFailed => return error.ReadFailed, + }; + remaining -= discard_len; + if (remaining == 0) return n; + } +} + +/// Fill `buffer` with the next `buffer.len` bytes from the stream, advancing +/// the seek position. +/// +/// Invalidates previously returned values from `peek`. +/// +/// If the provided buffer cannot be filled completely, `error.EndOfStream` is +/// returned instead. +/// +/// See also: +/// * `peek` +/// * `readSliceShort` +pub fn readSlice(r: *Reader, buffer: []u8) Error!void { + const n = try readSliceShort(r, buffer); + if (n != buffer.len) return error.EndOfStream; +} + +/// Fill `buffer` with the next `buffer.len` bytes from the stream, advancing +/// the seek position. +/// +/// Invalidates previously returned values from `peek`. +/// +/// Returns the number of bytes read, which is less than `buffer.len` if and +/// only if the stream reached the end. +/// +/// See also: +/// * `readSlice` +pub fn readSliceShort(r: *Reader, buffer: []u8) ShortError!usize { + const in_buffer = r.buffer[r.seek..r.end]; + const copy_len = @min(buffer.len, in_buffer.len); + @memcpy(buffer[0..copy_len], in_buffer[0..copy_len]); + if (buffer.len - copy_len == 0) { + r.seek += copy_len; + return buffer.len; + } + var i: usize = copy_len; + r.end = 0; + r.seek = 0; + while (true) { + const remaining = buffer[i..]; + var wrapper: Writer.VectorWrapper = .{ + .it = .{ + .first = remaining, + .last = r.buffer, + }, + .writer = .{ + .buffer = if (remaining.len >= r.buffer.len) remaining else r.buffer, + .vtable = &Writer.VectorWrapper.vtable, + }, + }; + const n = r.vtable.stream(r, &wrapper.writer, .unlimited) catch |err| switch (err) { + error.WriteFailed => { + if (wrapper.writer.buffer.ptr != remaining.ptr) { + assert(r.seek == 0); + r.seek = remaining.len; + r.end = wrapper.writer.end; + @memcpy(remaining, r.buffer[0..remaining.len]); + return buffer.len; + } + return buffer.len; + }, + error.EndOfStream => return i, + error.ReadFailed => return error.ReadFailed, + }; + if (n < remaining.len) { + i += n; + continue; + } + r.end = n - remaining.len; + return buffer.len; + } +} + +/// Fill `buffer` with the next `buffer.len` bytes from the stream, advancing +/// the seek position. +/// +/// Invalidates previously returned values from `peek`. +/// +/// If the provided buffer cannot be filled completely, `error.EndOfStream` is +/// returned instead. +/// +/// The function is inline to avoid the dead code in case `endian` is +/// comptime-known and matches host endianness. +/// +/// See also: +/// * `readSlice` +/// * `readSliceEndianAlloc` +pub inline fn readSliceEndian( + r: *Reader, + comptime Elem: type, + buffer: []Elem, + endian: std.builtin.Endian, +) Error!void { + try readSlice(r, @ptrCast(buffer)); + if (native_endian != endian) for (buffer) |*elem| std.mem.byteSwapAllFields(Elem, elem); +} + +pub const ReadAllocError = Error || Allocator.Error; + +/// The function is inline to avoid the dead code in case `endian` is +/// comptime-known and matches host endianness. +pub inline fn readSliceEndianAlloc( + r: *Reader, + allocator: Allocator, + comptime Elem: type, + len: usize, + endian: std.builtin.Endian, +) ReadAllocError![]Elem { + const dest = try allocator.alloc(Elem, len); + errdefer allocator.free(dest); + try readSlice(r, @ptrCast(dest)); + if (native_endian != endian) for (dest) |*elem| std.mem.byteSwapAllFields(Elem, elem); + return dest; +} + +pub fn readSliceAlloc(r: *Reader, allocator: Allocator, len: usize) ReadAllocError![]u8 { + const dest = try allocator.alloc(u8, len); + errdefer allocator.free(dest); + try readSlice(r, dest); + return dest; +} + +pub const DelimiterError = error{ + /// See the `Reader` implementation for detailed diagnostics. + ReadFailed, + /// For "inclusive" functions, stream ended before the delimiter was found. + /// For "exclusive" functions, stream ended and there are no more bytes to + /// return. + EndOfStream, + /// The delimiter was not found within a number of bytes matching the + /// capacity of the `Reader`. + StreamTooLong, +}; + +/// Returns a slice of the next bytes of buffered data from the stream until +/// `sentinel` is found, advancing the seek position. +/// +/// Returned slice has a sentinel. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `peekSentinel` +/// * `takeDelimiterExclusive` +/// * `takeDelimiterInclusive` +pub fn takeSentinel(r: *Reader, comptime sentinel: u8) DelimiterError![:sentinel]u8 { + const result = try r.peekSentinel(sentinel); + r.toss(result.len + 1); + return result; +} + +pub fn peekSentinel(r: *Reader, comptime sentinel: u8) DelimiterError![:sentinel]u8 { + const result = try r.peekDelimiterInclusive(sentinel); + return result[0 .. result.len - 1 :sentinel]; +} + +/// Returns a slice of the next bytes of buffered data from the stream until +/// `delimiter` is found, advancing the seek position. +/// +/// Returned slice includes the delimiter as the last byte. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `takeSentinel` +/// * `takeDelimiterExclusive` +/// * `peekDelimiterInclusive` +pub fn takeDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { + const result = try r.peekDelimiterInclusive(delimiter); + r.toss(result.len); + return result; +} + +/// Returns a slice of the next bytes of buffered data from the stream until +/// `delimiter` is found, without advancing the seek position. +/// +/// Returned slice includes the delimiter as the last byte. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `peekSentinel` +/// * `peekDelimiterExclusive` +/// * `takeDelimiterInclusive` +pub fn peekDelimiterInclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { + const buffer = r.buffer[0..r.end]; + const seek = r.seek; + if (std.mem.indexOfScalarPos(u8, buffer, seek, delimiter)) |end| { + @branchHint(.likely); + return buffer[seek .. end + 1]; + } + if (seek > 0) { + const remainder = buffer[seek..]; + @memmove(buffer[0..remainder.len], remainder); + r.end = remainder.len; + r.seek = 0; + } + var writer: Writer = .{ + .buffer = r.buffer, + .vtable = &.{ .drain = Writer.fixedDrain }, + }; + while (r.end < r.buffer.len) { + writer.end = r.end; + const n = r.vtable.stream(r, &writer, .limited(r.buffer.len - r.end)) catch |err| switch (err) { + error.WriteFailed => unreachable, + else => |e| return e, + }; + const prev_end = r.end; + r.end = prev_end + n; + if (std.mem.indexOfScalarPos(u8, r.buffer[0..r.end], prev_end, delimiter)) |end| { + return r.buffer[0 .. end + 1]; + } + } + return error.StreamTooLong; +} + +/// Returns a slice of the next bytes of buffered data from the stream until +/// `delimiter` is found, advancing the seek position. +/// +/// Returned slice excludes the delimiter. End-of-stream is treated equivalent +/// to a delimiter, unless it would result in a length 0 return value, in which +/// case `error.EndOfStream` is returned instead. +/// +/// If the delimiter is not found within a number of bytes matching the +/// capacity of this `Reader`, `error.StreamTooLong` is returned. In +/// such case, the stream state is unmodified as if this function was never +/// called. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `takeDelimiterInclusive` +/// * `peekDelimiterExclusive` +pub fn takeDelimiterExclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { + const result = r.peekDelimiterInclusive(delimiter) catch |err| switch (err) { + error.EndOfStream => { + if (r.end == 0) return error.EndOfStream; + r.toss(r.end); + return r.buffer[0..r.end]; }, else => |e| return e, }; - return try array_list.toOwnedSlice(); -} - -/// Deprecated: use `streamUntilDelimiter` with FixedBufferStream's writer instead. -/// Reads from the stream until specified byte is found. If the buffer is not -/// large enough to hold the entire contents, `error.StreamTooLong` is returned. -/// If end-of-stream is found, returns the rest of the stream. If this -/// function is called again after that, returns null. -/// Returns a slice of the stream data, with ptr equal to `buf.ptr`. The -/// delimiter byte is written to the output buffer but is not included -/// in the returned slice. -pub fn readUntilDelimiterOrEof(self: Self, buf: []u8, delimiter: u8) anyerror!?[]u8 { - var fbs = std.io.fixedBufferStream(buf); - self.streamUntilDelimiter(fbs.writer(), delimiter, fbs.buffer.len) catch |err| switch (err) { - error.EndOfStream => if (fbs.getWritten().len == 0) { - return null; - }, + r.toss(result.len); + return result[0 .. result.len - 1]; +} +/// Returns a slice of the next bytes of buffered data from the stream until +/// `delimiter` is found, without advancing the seek position. +/// +/// Returned slice excludes the delimiter. End-of-stream is treated equivalent +/// to a delimiter, unless it would result in a length 0 return value, in which +/// case `error.EndOfStream` is returned instead. +/// +/// If the delimiter is not found within a number of bytes matching the +/// capacity of this `Reader`, `error.StreamTooLong` is returned. In +/// such case, the stream state is unmodified as if this function was never +/// called. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `peekDelimiterInclusive` +/// * `takeDelimiterExclusive` +pub fn peekDelimiterExclusive(r: *Reader, delimiter: u8) DelimiterError![]u8 { + const result = r.peekDelimiterInclusive(delimiter) catch |err| switch (err) { + error.EndOfStream => { + if (r.end == 0) return error.EndOfStream; + return r.buffer[0..r.end]; + }, else => |e| return e, }; - const output = fbs.getWritten(); - buf[output.len] = delimiter; // emulating old behaviour - return output; + return result[0 .. result.len - 1]; } -/// Appends to the `writer` contents by reading from the stream until `delimiter` is found. +/// Appends to `w` contents by reading from the stream until `delimiter` is +/// found. Does not write the delimiter itself. +/// +/// Returns number of bytes streamed. +pub fn readDelimiter(r: *Reader, w: *Writer, delimiter: u8) StreamError!usize { + const amount, const to = try r.readAny(w, delimiter, .unlimited); + return switch (to) { + .delimiter => amount, + .limit => unreachable, + .end => error.EndOfStream, + }; +} + +/// Appends to `w` contents by reading from the stream until `delimiter` is found. /// Does not write the delimiter itself. -/// If `optional_max_size` is not null and amount of written bytes exceeds `optional_max_size`, -/// returns `error.StreamTooLong` and finishes appending. -/// If `optional_max_size` is null, appending is unbounded. -pub fn streamUntilDelimiter( - self: Self, - writer: anytype, +/// +/// Succeeds if stream ends before delimiter found. +/// +/// Returns number of bytes streamed. The end is not signaled to the writer. +pub fn readDelimiterEnding( + r: *Reader, + w: *Writer, delimiter: u8, - optional_max_size: ?usize, -) anyerror!void { - if (optional_max_size) |max_size| { - for (0..max_size) |_| { - const byte: u8 = try self.readByte(); - if (byte == delimiter) return; - try writer.writeByte(byte); - } - return error.StreamTooLong; - } else { - while (true) { - const byte: u8 = try self.readByte(); - if (byte == delimiter) return; - try writer.writeByte(byte); - } - // Can not throw `error.StreamTooLong` since there are no boundary. +) StreamRemainingError!usize { + const amount, const to = try r.readAny(w, delimiter, .unlimited); + return switch (to) { + .delimiter, .end => amount, + .limit => unreachable, + }; +} + +pub const StreamDelimiterLimitedError = StreamRemainingError || error{ + /// Stream ended before the delimiter was found. + EndOfStream, + /// The delimiter was not found within the limit. + StreamTooLong, +}; + +/// Appends to `w` contents by reading from the stream until `delimiter` is found. +/// Does not write the delimiter itself. +/// +/// Returns number of bytes streamed. +pub fn readDelimiterLimit( + r: *Reader, + w: *Writer, + delimiter: u8, + limit: Limit, +) StreamDelimiterLimitedError!usize { + const amount, const to = try r.readAny(w, delimiter, limit); + return switch (to) { + .delimiter => amount, + .limit => error.StreamTooLong, + .end => error.EndOfStream, + }; +} + +fn readAny( + r: *Reader, + w: *Writer, + delimiter: ?u8, + limit: Limit, +) StreamRemainingError!struct { usize, enum { delimiter, limit, end } } { + var amount: usize = 0; + var remaining = limit; + while (remaining.nonzero()) { + const available = remaining.slice(r.peekGreedy(1) catch |err| switch (err) { + error.ReadFailed => |e| return e, + error.EndOfStream => return .{ amount, .end }, + }); + if (delimiter) |d| if (std.mem.indexOfScalar(u8, available, d)) |delimiter_index| { + try w.writeAll(available[0..delimiter_index]); + r.toss(delimiter_index + 1); + return .{ amount + delimiter_index, .delimiter }; + }; + try w.writeAll(available); + r.toss(available.len); + amount += available.len; + remaining = remaining.subtract(available.len).?; } + return .{ amount, .limit }; } /// Reads from the stream until specified byte is found, discarding all data, /// including the delimiter. -/// If end-of-stream is found, this function succeeds. -pub fn skipUntilDelimiterOrEof(self: Self, delimiter: u8) anyerror!void { - while (true) { - const byte = self.readByte() catch |err| switch (err) { - error.EndOfStream => return, - else => |e| return e, +/// +/// If end of stream is found, this function succeeds. +pub fn discardDelimiterInclusive(r: *Reader, delimiter: u8) Error!void { + _ = r; + _ = delimiter; + @panic("TODO"); +} + +/// Reads from the stream until specified byte is found, discarding all data, +/// excluding the delimiter. +/// +/// Succeeds if stream ends before delimiter found. +pub fn discardDelimiterExclusive(r: *Reader, delimiter: u8) ShortError!void { + _ = r; + _ = delimiter; + @panic("TODO"); +} + +/// Fills the buffer such that it contains at least `n` bytes, without +/// advancing the seek position. +/// +/// Returns `error.EndOfStream` if and only if there are fewer than `n` bytes +/// remaining. +/// +/// Asserts buffer capacity is at least `n`. +pub fn fill(r: *Reader, n: usize) Error!void { + assert(n <= r.buffer.len); + if (r.seek + n <= r.end) { + @branchHint(.likely); + return; + } + rebaseCapacity(r, n); + var writer: Writer = .{ + .buffer = r.buffer, + .vtable = &.{ .drain = Writer.fixedDrain }, + }; + while (r.end < r.seek + n) { + writer.end = r.end; + r.end += r.vtable.stream(r, &writer, .limited(r.buffer.len - r.end)) catch |err| switch (err) { + error.WriteFailed => unreachable, + error.ReadFailed, error.EndOfStream => |e| return e, }; - if (byte == delimiter) return; } } -/// Reads 1 byte from the stream or returns `error.EndOfStream`. -pub fn readByte(self: Self) anyerror!u8 { - var result: [1]u8 = undefined; - const amt_read = try self.read(result[0..]); - if (amt_read < 1) return error.EndOfStream; - return result[0]; -} - -/// Same as `readByte` except the returned byte is signed. -pub fn readByteSigned(self: Self) anyerror!i8 { - return @as(i8, @bitCast(try self.readByte())); -} - -/// Reads exactly `num_bytes` bytes and returns as an array. -/// `num_bytes` must be comptime-known -pub fn readBytesNoEof(self: Self, comptime num_bytes: usize) anyerror![num_bytes]u8 { - var bytes: [num_bytes]u8 = undefined; - try self.readNoEof(&bytes); - return bytes; -} - -/// Reads bytes until `bounded.len` is equal to `num_bytes`, -/// or the stream ends. -/// -/// * it is assumed that `num_bytes` will not exceed `bounded.capacity()` -pub fn readIntoBoundedBytes( - self: Self, - comptime num_bytes: usize, - bounded: *std.BoundedArray(u8, num_bytes), -) anyerror!void { - while (bounded.len < num_bytes) { - // get at most the number of bytes free in the bounded array - const bytes_read = try self.read(bounded.unusedCapacitySlice()); - if (bytes_read == 0) return; - - // bytes_read will never be larger than @TypeOf(bounded.len) - // due to `self.read` being bounded by `bounded.unusedCapacitySlice()` - bounded.len += @as(@TypeOf(bounded.len), @intCast(bytes_read)); +/// Without advancing the seek position, does exactly one underlying read, filling the buffer as +/// much as possible. This may result in zero bytes added to the buffer, which is not an end of +/// stream condition. End of stream is communicated via returning `error.EndOfStream`. +/// +/// Asserts buffer capacity is at least 1. +pub fn fillMore(r: *Reader) Error!void { + rebaseCapacity(r, 1); + var writer: Writer = .{ + .buffer = r.buffer, + .end = r.end, + .vtable = &.{ .drain = Writer.fixedDrain }, + }; + r.end += r.vtable.stream(r, &writer, .limited(r.buffer.len - r.end)) catch |err| switch (err) { + error.WriteFailed => unreachable, + else => |e| return e, + }; +} + +/// Returns the next byte from the stream or returns `error.EndOfStream`. +/// +/// Does not advance the seek position. +/// +/// Asserts the buffer capacity is nonzero. +pub fn peekByte(r: *Reader) Error!u8 { + const buffer = r.buffer[0..r.end]; + const seek = r.seek; + if (seek >= buffer.len) { + @branchHint(.unlikely); + try fill(r, 1); } + return buffer[seek]; } -/// Reads at most `num_bytes` and returns as a bounded array. -pub fn readBoundedBytes(self: Self, comptime num_bytes: usize) anyerror!std.BoundedArray(u8, num_bytes) { - var result = std.BoundedArray(u8, num_bytes){}; - try self.readIntoBoundedBytes(num_bytes, &result); +/// Reads 1 byte from the stream or returns `error.EndOfStream`. +/// +/// Asserts the buffer capacity is nonzero. +pub fn takeByte(r: *Reader) Error!u8 { + const result = try peekByte(r); + r.seek += 1; return result; } -pub inline fn readInt(self: Self, comptime T: type, endian: std.builtin.Endian) anyerror!T { - const bytes = try self.readBytesNoEof(@divExact(@typeInfo(T).int.bits, 8)); - return mem.readInt(T, &bytes, endian); +/// Same as `takeByte` except the returned byte is signed. +pub fn takeByteSigned(r: *Reader) Error!i8 { + return @bitCast(try r.takeByte()); } -pub fn readVarInt( - self: Self, - comptime ReturnType: type, - endian: std.builtin.Endian, - size: usize, -) anyerror!ReturnType { - assert(size <= @sizeOf(ReturnType)); - var bytes_buf: [@sizeOf(ReturnType)]u8 = undefined; - const bytes = bytes_buf[0..size]; - try self.readNoEof(bytes); - return mem.readVarInt(ReturnType, bytes, endian); -} - -/// Optional parameters for `skipBytes` -pub const SkipBytesOptions = struct { - buf_size: usize = 512, -}; - -// `num_bytes` is a `u64` to match `off_t` -/// Reads `num_bytes` bytes from the stream and discards them -pub fn skipBytes(self: Self, num_bytes: u64, comptime options: SkipBytesOptions) anyerror!void { - var buf: [options.buf_size]u8 = undefined; - var remaining = num_bytes; +/// Asserts the buffer was initialized with a capacity at least `@bitSizeOf(T) / 8`. +pub inline fn takeInt(r: *Reader, comptime T: type, endian: std.builtin.Endian) Error!T { + const n = @divExact(@typeInfo(T).int.bits, 8); + return std.mem.readInt(T, try r.takeArray(n), endian); +} - while (remaining > 0) { - const amt = @min(remaining, options.buf_size); - try self.readNoEof(buf[0..amt]); - remaining -= amt; - } +/// Asserts the buffer was initialized with a capacity at least `n`. +pub fn takeVarInt(r: *Reader, comptime Int: type, endian: std.builtin.Endian, n: usize) Error!Int { + assert(n <= @sizeOf(Int)); + return std.mem.readVarInt(Int, try r.take(n), endian); } -/// Reads `slice.len` bytes from the stream and returns if they are the same as the passed slice -pub fn isBytes(self: Self, slice: []const u8) anyerror!bool { - var i: usize = 0; - var matches = true; - while (i < slice.len) : (i += 1) { - if (slice[i] != try self.readByte()) { - matches = false; - } - } - return matches; +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// Advances the seek position. +/// +/// See also: +/// * `peekStruct` +pub fn takeStruct(r: *Reader, comptime T: type) Error!*align(1) T { + // Only extern and packed structs have defined in-memory layout. + comptime assert(@typeInfo(T).@"struct".layout != .auto); + return @ptrCast(try r.takeArray(@sizeOf(T))); } -pub fn readStruct(self: Self, comptime T: type) anyerror!T { +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// Does not advance the seek position. +/// +/// See also: +/// * `takeStruct` +pub fn peekStruct(r: *Reader, comptime T: type) Error!*align(1) T { // Only extern and packed structs have defined in-memory layout. comptime assert(@typeInfo(T).@"struct".layout != .auto); - var res: [1]T = undefined; - try self.readNoEof(mem.sliceAsBytes(res[0..])); - return res[0]; + return @ptrCast(try r.peekArray(@sizeOf(T))); } -pub fn readStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian) anyerror!T { - var res = try self.readStruct(T); - if (native_endian != endian) { - mem.byteSwapAllFields(T, &res); - } +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// This function is inline to avoid referencing `std.mem.byteSwapAllFields` +/// when `endian` is comptime-known and matches the host endianness. +pub inline fn takeStructEndian(r: *Reader, comptime T: type, endian: std.builtin.Endian) Error!T { + var res = (try r.takeStruct(T)).*; + if (native_endian != endian) std.mem.byteSwapAllFields(T, &res); + return res; +} + +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// This function is inline to avoid referencing `std.mem.byteSwapAllFields` +/// when `endian` is comptime-known and matches the host endianness. +pub inline fn peekStructEndian(r: *Reader, comptime T: type, endian: std.builtin.Endian) Error!T { + var res = (try r.peekStruct(T)).*; + if (native_endian != endian) std.mem.byteSwapAllFields(T, &res); return res; } -/// Reads an integer with the same size as the given enum's tag type. If the integer matches -/// an enum tag, casts the integer to the enum tag and returns it. Otherwise, returns an `error.InvalidValue`. -/// TODO optimization taking advantage of most fields being in order -pub fn readEnum(self: Self, comptime Enum: type, endian: std.builtin.Endian) anyerror!Enum { - const E = error{ - /// An integer was read, but it did not match any of the tags in the supplied enum. - InvalidValue, +pub const TakeEnumError = Error || error{InvalidEnumTag}; + +/// Reads an integer with the same size as the given enum's tag type. If the +/// integer matches an enum tag, casts the integer to the enum tag and returns +/// it. Otherwise, returns `error.InvalidEnumTag`. +/// +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(Enum)`. +pub fn takeEnum(r: *Reader, comptime Enum: type, endian: std.builtin.Endian) TakeEnumError!Enum { + const Tag = @typeInfo(Enum).@"enum".tag_type; + const int = try r.takeInt(Tag, endian); + return std.meta.intToEnum(Enum, int); +} + +/// Reads an integer with the same size as the given nonexhaustive enum's tag type. +/// +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(Enum)`. +pub fn takeEnumNonexhaustive(r: *Reader, comptime Enum: type, endian: std.builtin.Endian) Error!Enum { + const info = @typeInfo(Enum).@"enum"; + comptime assert(!info.is_exhaustive); + comptime assert(@bitSizeOf(info.tag_type) == @sizeOf(info.tag_type) * 8); + return takeEnum(r, Enum, endian) catch |err| switch (err) { + error.InvalidEnumTag => unreachable, + else => |e| return e, }; - const type_info = @typeInfo(Enum).@"enum"; - const tag = try self.readInt(type_info.tag_type, endian); +} - inline for (std.meta.fields(Enum)) |field| { - if (tag == field.value) { - return @field(Enum, field.name); - } +pub const TakeLeb128Error = Error || error{Overflow}; + +/// Read a single LEB128 value as type T, or `error.Overflow` if the value cannot fit. +pub fn takeLeb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result { + const result_info = @typeInfo(Result).int; + return std.math.cast(Result, try r.takeMultipleOf7Leb128(@Type(.{ .int = .{ + .signedness = result_info.signedness, + .bits = std.mem.alignForwardAnyAlign(u16, result_info.bits, 7), + } }))) orelse error.Overflow; +} + +pub fn expandTotalCapacity(r: *Reader, allocator: Allocator, n: usize) Allocator.Error!void { + if (n <= r.buffer.len) return; + if (r.seek > 0) rebase(r); + var list: ArrayList(u8) = .{ + .items = r.buffer[0..r.end], + .capacity = r.buffer.len, + }; + defer r.buffer = list.allocatedSlice(); + try list.ensureTotalCapacity(allocator, n); +} + +pub const FillAllocError = Error || Allocator.Error; + +pub fn fillAlloc(r: *Reader, allocator: Allocator, n: usize) FillAllocError!void { + try expandTotalCapacity(r, allocator, n); + return fill(r, n); +} + +/// Returns a slice into the unused capacity of `buffer` with at least +/// `min_len` bytes, extending `buffer` by resizing it with `gpa` as necessary. +/// +/// After calling this function, typically the caller will follow up with a +/// call to `advanceBufferEnd` to report the actual number of bytes buffered. +pub fn writableSliceGreedyAlloc(r: *Reader, allocator: Allocator, min_len: usize) Allocator.Error![]u8 { + { + const unused = r.buffer[r.end..]; + if (unused.len >= min_len) return unused; + } + if (r.seek > 0) rebase(r); + { + var list: ArrayList(u8) = .{ + .items = r.buffer[0..r.end], + .capacity = r.buffer.len, + }; + defer r.buffer = list.allocatedSlice(); + try list.ensureUnusedCapacity(allocator, min_len); } + const unused = r.buffer[r.end..]; + assert(unused.len >= min_len); + return unused; +} - return E.InvalidValue; +/// After writing directly into the unused capacity of `buffer`, this function +/// updates `end` so that users of `Reader` can receive the data. +pub fn advanceBufferEnd(r: *Reader, n: usize) void { + assert(n <= r.buffer.len - r.end); + r.end += n; } -/// Reads the stream until the end, ignoring all the data. -/// Returns the number of bytes discarded. -pub fn discard(self: Self) anyerror!u64 { - var trash: [4096]u8 = undefined; - var index: u64 = 0; +fn takeMultipleOf7Leb128(r: *Reader, comptime Result: type) TakeLeb128Error!Result { + const result_info = @typeInfo(Result).int; + comptime assert(result_info.bits % 7 == 0); + var remaining_bits: std.math.Log2IntCeil(Result) = result_info.bits; + const UnsignedResult = @Type(.{ .int = .{ + .signedness = .unsigned, + .bits = result_info.bits, + } }); + var result: UnsignedResult = 0; + var fits = true; while (true) { - const n = try self.read(&trash); - if (n == 0) return index; - index += n; + const buffer: []const packed struct(u8) { bits: u7, more: bool } = @ptrCast(try r.peekGreedy(1)); + for (buffer, 1..) |byte, len| { + if (remaining_bits > 0) { + result = @shlExact(@as(UnsignedResult, byte.bits), result_info.bits - 7) | + if (result_info.bits > 7) @shrExact(result, 7) else 0; + remaining_bits -= 7; + } else if (fits) fits = switch (result_info.signedness) { + .signed => @as(i7, @bitCast(byte.bits)) == + @as(i7, @truncate(@as(Result, @bitCast(result)) >> (result_info.bits - 1))), + .unsigned => byte.bits == 0, + }; + if (byte.more) continue; + r.toss(len); + return if (fits) @as(Result, @bitCast(result)) >> remaining_bits else error.Overflow; + } + r.toss(buffer.len); } } -const std = @import("../std.zig"); -const Self = @This(); -const math = std.math; -const assert = std.debug.assert; -const mem = std.mem; -const testing = std.testing; -const native_endian = @import("builtin").target.cpu.arch.endian(); -const Alignment = std.mem.Alignment; +/// Left-aligns data such that `r.seek` becomes zero. +pub fn rebase(r: *Reader) void { + if (r.seek == 0) return; + const data = r.buffer[r.seek..r.end]; + @memmove(r.buffer[0..data.len], data); + r.seek = 0; + r.end = data.len; +} + +/// Ensures `capacity` more data can be buffered without rebasing, by rebasing +/// if necessary. +/// +/// Asserts `capacity` is within the buffer capacity. +pub fn rebaseCapacity(r: *Reader, capacity: usize) void { + if (r.end > r.buffer.len - capacity) rebase(r); +} + +/// Advances the stream and decreases the size of the storage buffer by `n`, +/// returning the range of bytes no longer accessible by `r`. +/// +/// This action can be undone by `restitute`. +/// +/// Asserts there are at least `n` buffered bytes already. +/// +/// Asserts that `r.seek` is zero, i.e. the buffer is in a rebased state. +pub fn steal(r: *Reader, n: usize) []u8 { + assert(r.seek == 0); + assert(n <= r.end); + const stolen = r.buffer[0..n]; + r.buffer = r.buffer[n..]; + r.end -= n; + return stolen; +} + +/// Expands the storage buffer, undoing the effects of `steal` +/// Assumes that `n` does not exceed the total number of stolen bytes. +pub fn restitute(r: *Reader, n: usize) void { + r.buffer = (r.buffer.ptr - n)[0 .. r.buffer.len + n]; + r.end += n; + r.seek += n; +} + +test fixed { + var r: Reader = .fixed("a\x02"); + try testing.expect((try r.takeByte()) == 'a'); + try testing.expect((try r.takeEnum(enum(u8) { + a = 0, + b = 99, + c = 2, + d = 3, + }, builtin.cpu.arch.endian())) == .c); + try testing.expectError(error.EndOfStream, r.takeByte()); +} + +test peek { + return error.Unimplemented; +} + +test peekGreedy { + return error.Unimplemented; +} + +test toss { + return error.Unimplemented; +} + +test take { + return error.Unimplemented; +} + +test takeArray { + return error.Unimplemented; +} + +test peekArray { + return error.Unimplemented; +} + +test discardAll { + var r: Reader = .fixed("foobar"); + try r.discard(3); + try testing.expectEqualStrings("bar", try r.take(3)); + try r.discard(0); + try testing.expectError(error.EndOfStream, r.discard(1)); +} + +test discardRemaining { + return error.Unimplemented; +} + +test stream { + return error.Unimplemented; +} -test { - _ = @import("Reader/test.zig"); +test takeSentinel { + return error.Unimplemented; +} + +test peekSentinel { + return error.Unimplemented; +} + +test takeDelimiterInclusive { + return error.Unimplemented; +} + +test peekDelimiterInclusive { + return error.Unimplemented; +} + +test takeDelimiterExclusive { + return error.Unimplemented; +} + +test peekDelimiterExclusive { + return error.Unimplemented; +} + +test readDelimiter { + return error.Unimplemented; +} + +test readDelimiterEnding { + return error.Unimplemented; +} + +test readDelimiterLimit { + return error.Unimplemented; +} + +test discardDelimiterExclusive { + return error.Unimplemented; +} + +test discardDelimiterInclusive { + return error.Unimplemented; +} + +test fill { + return error.Unimplemented; +} + +test takeByte { + return error.Unimplemented; +} + +test takeByteSigned { + return error.Unimplemented; +} + +test takeInt { + return error.Unimplemented; +} + +test takeVarInt { + return error.Unimplemented; +} + +test takeStruct { + return error.Unimplemented; +} + +test peekStruct { + return error.Unimplemented; +} + +test takeStructEndian { + return error.Unimplemented; +} + +test peekStructEndian { + return error.Unimplemented; +} + +test takeEnum { + return error.Unimplemented; +} + +test takeLeb128 { + return error.Unimplemented; +} + +test readSliceShort { + return error.Unimplemented; +} + +test readVec { + return error.Unimplemented; +} + +test "expected error.EndOfStream" { + // Unit test inspired by https://github.com/ziglang/zig/issues/17733 + var r: std.io.Reader = .fixed(""); + try std.testing.expectError(error.EndOfStream, r.readEnum(enum(u8) { a, b }, .little)); + try std.testing.expectError(error.EndOfStream, r.isBytes("foo")); +} + +fn endingStream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { + _ = r; + _ = w; + _ = limit; + return error.EndOfStream; +} + +fn endingDiscard(r: *Reader, limit: Limit) Error!usize { + _ = r; + _ = limit; + return error.EndOfStream; +} + +fn failingStream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { + _ = r; + _ = w; + _ = limit; + return error.ReadFailed; +} + +fn failingDiscard(r: *Reader, limit: Limit) Error!usize { + _ = r; + _ = limit; + return error.ReadFailed; +} + +test "readAlloc when the backing reader provides one byte at a time" { + const OneByteReader = struct { + str: []const u8, + curr: usize, + + fn read(self: *@This(), dest: []u8) usize { + if (self.str.len <= self.curr or dest.len == 0) + return 0; + + dest[0] = self.str[self.curr]; + self.curr += 1; + return 1; + } + }; + + const str = "This is a test"; + var one_byte_stream: OneByteReader = .init(str); + const res = try one_byte_stream.reader().streamReadAlloc(std.testing.allocator, str.len + 1); + defer std.testing.allocator.free(res); + try std.testing.expectEqualStrings(str, res); +} + +/// Provides a `Reader` implementation by passing data from an underlying +/// reader through `Hasher.update`. +/// +/// The underlying reader is best unbuffered. +/// +/// This implementation makes suboptimal buffering decisions due to being +/// generic. A better solution will involve creating a reader for each hash +/// function, where the discard buffer can be tailored to the hash +/// implementation details. +pub fn Hashed(comptime Hasher: type) type { + return struct { + in: *Reader, + hasher: Hasher, + interface: Reader, + + pub fn init(in: *Reader, hasher: Hasher, buffer: []u8) @This() { + return .{ + .in = in, + .hasher = hasher, + .interface = .{ + .vtable = &.{ + .read = @This().read, + .discard = @This().discard, + }, + .buffer = buffer, + .end = 0, + .seek = 0, + }, + }; + } + + fn read(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { + const this: *@This() = @alignCast(@fieldParentPtr("interface", r)); + const data = w.writableVector(limit); + const n = try this.in.readVec(data); + const result = w.advanceVector(n); + var remaining: usize = n; + for (data) |slice| { + if (remaining < slice.len) { + this.hasher.update(slice[0..remaining]); + return result; + } else { + remaining -= slice.len; + this.hasher.update(slice); + } + } + assert(remaining == 0); + return result; + } + + fn discard(r: *Reader, limit: Limit) Error!usize { + const this: *@This() = @alignCast(@fieldParentPtr("interface", r)); + var w = this.hasher.writer(&.{}); + const n = this.in.stream(&w, limit) catch |err| switch (err) { + error.WriteFailed => unreachable, + else => |e| return e, + }; + return n; + } + }; } diff --git a/lib/std/io/Reader/Limited.zig b/lib/std/io/Reader/Limited.zig new file mode 100644 index 0000000000..9476b97804 --- /dev/null +++ b/lib/std/io/Reader/Limited.zig @@ -0,0 +1,42 @@ +const Limited = @This(); + +const std = @import("../../std.zig"); +const Reader = std.io.Reader; +const Writer = std.io.Writer; +const Limit = std.io.Limit; + +unlimited: *Reader, +remaining: Limit, +interface: Reader, + +pub fn init(reader: *Reader, limit: Limit, buffer: []u8) Limited { + return .{ + .unlimited = reader, + .remaining = limit, + .interface = .{ + .vtable = &.{ + .stream = stream, + .discard = discard, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + }; +} + +fn stream(context: ?*anyopaque, w: *Writer, limit: Limit) Reader.StreamError!usize { + const l: *Limited = @alignCast(@ptrCast(context)); + const combined_limit = limit.min(l.remaining); + const n = try l.unlimited_reader.read(w, combined_limit); + l.remaining = l.remaining.subtract(n).?; + return n; +} + +fn discard(context: ?*anyopaque, limit: Limit) Reader.Error!usize { + const l: *Limited = @alignCast(@ptrCast(context)); + const combined_limit = limit.min(l.remaining); + const n = try l.unlimited_reader.discard(combined_limit); + l.remaining = l.remaining.subtract(n).?; + return n; +} diff --git a/lib/std/io/Writer.zig b/lib/std/io/Writer.zig index 26d4f88def..c1371d3434 100644 --- a/lib/std/io/Writer.zig +++ b/lib/std/io/Writer.zig @@ -1,83 +1,2206 @@ +const builtin = @import("builtin"); +const native_endian = builtin.target.cpu.arch.endian(); + +const Writer = @This(); const std = @import("../std.zig"); const assert = std.debug.assert; -const mem = std.mem; -const native_endian = @import("builtin").target.cpu.arch.endian(); +const Limit = std.io.Limit; +const File = std.fs.File; +const testing = std.testing; +const Allocator = std.mem.Allocator; + +vtable: *const VTable, +/// If this has length zero, the writer is unbuffered, and `flush` is a no-op. +buffer: []u8, +/// In `buffer` before this are buffered bytes, after this is `undefined`. +end: usize = 0, +/// Tracks total number of bytes written to this `Writer`. This value +/// only increases. In the case of fixed mode, this value always equals `end`. +/// +/// This value is maintained by the interface; `VTable` function +/// implementations need not modify it. +count: usize = 0, + +pub const VTable = struct { + /// Sends bytes to the logical sink. A write will only be sent here if it + /// could not fit into `buffer`, or during a `flush` operation. + /// + /// `buffer[0..end]` is consumed first, followed by each slice of `data` in + /// order. Elements of `data` may alias each other but may not alias + /// `buffer`. + /// + /// This function modifies `Writer.end` and `Writer.buffer` in an + /// implementation-defined manner. + /// + /// `data.len` must be nonzero. + /// + /// The last element of `data` is repeated as necessary so that it is + /// written `splat` number of times, which may be zero. + /// + /// Number of bytes consumed from `data` is returned, excluding bytes from + /// `buffer`. + /// + /// Number of bytes returned may be zero, which does not indicate stream + /// end. A subsequent call may return nonzero, or signal end of stream via + /// `error.WriteFailed`. + drain: *const fn (w: *Writer, data: []const []const u8, splat: usize) Error!usize, + + /// Copies contents from an open file to the logical sink. `buffer[0..end]` + /// is consumed first, followed by `limit` bytes from `file_reader`. + /// + /// Number of bytes logically written is returned. This excludes bytes from + /// `buffer` because they have already been logically written. Number of + /// bytes consumed from `buffer` are tracked by modifying `end`. + /// + /// Number of bytes returned may be zero, which does not indicate stream + /// end. A subsequent call may return nonzero, or signal end of stream via + /// `error.WriteFailed`. Caller may check `file_reader` state + /// (`File.Reader.atEnd`) to disambiguate between a zero-length read or + /// write, and whether the file reached the end. + /// + /// `error.Unimplemented` indicates the callee cannot offer a more + /// efficient implementation than the caller performing its own reads. + sendFile: *const fn ( + w: *Writer, + file_reader: *File.Reader, + /// Maximum amount of bytes to read from the file. Implementations may + /// assume that the file size does not exceed this amount. Data from + /// `buffer` does not count towards this limit. + limit: Limit, + ) FileError!usize = unimplementedSendFile, + + /// Consumes all remaining buffer. + /// + /// The default flush implementation calls drain repeatedly until `end` is + /// zero, however it is legal for implementations to manage `end` + /// differently. For instance, `Allocating` flush is a no-op. + /// + /// There may be subsequent calls to `drain` and `sendFile` after a `flush` + /// operation. + flush: *const fn (w: *Writer) Error!void = defaultFlush, +}; + +pub const Error = error{ + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, +}; -context: *const anyopaque, -writeFn: *const fn (context: *const anyopaque, bytes: []const u8) anyerror!usize, +pub const FileAllError = error{ + /// Detailed diagnostics are found on the `File.Reader` struct. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, +}; -const Self = @This(); -pub const Error = anyerror; +pub const FileReadingError = error{ + /// Detailed diagnostics are found on the `File.Reader` struct. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, + /// Reached the end of the file being read. + EndOfStream, +}; -pub fn write(self: Self, bytes: []const u8) anyerror!usize { - return self.writeFn(self.context, bytes); +pub const FileError = error{ + /// Detailed diagnostics are found on the `File.Reader` struct. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, + /// Reached the end of the file being read. + EndOfStream, + /// Indicates the caller should do its own file reading; the callee cannot + /// offer a more efficient implementation. + Unimplemented, +}; + +/// Writes to `buffer` and returns `error.WriteFailed` when it is full. Unless +/// modified externally, `count` will always equal `end`. +pub fn fixed(buffer: []u8) Writer { + return .{ + .vtable = &.{ .drain = fixedDrain }, + .buffer = buffer, + }; } -pub fn writeAll(self: Self, bytes: []const u8) anyerror!void { - var index: usize = 0; - while (index != bytes.len) { - index += try self.write(bytes[index..]); +pub fn hashed(w: *Writer, hasher: anytype) Hashed(@TypeOf(hasher)) { + return .{ .out = w, .hasher = hasher }; +} + +pub const failing: Writer = .{ + .vtable = &.{ + .drain = failingDrain, + .sendFile = failingSendFile, + }, +}; + +pub fn discarding(buffer: []u8) Writer { + return .{ + .vtable = &.{ + .drain = discardingDrain, + .sendFile = discardingSendFile, + }, + .buffer = buffer, + }; +} + +/// Returns the contents not yet drained. +pub fn buffered(w: *const Writer) []u8 { + return w.buffer[0..w.end]; +} + +pub fn countSplat(data: []const []const u8, splat: usize) usize { + var total: usize = 0; + for (data[0 .. data.len - 1]) |buf| total += buf.len; + total += data[data.len - 1].len * splat; + return total; +} + +pub fn countSendFileLowerBound(n: usize, file_reader: *File.Reader, limit: Limit) ?usize { + const total: u64 = @min(@intFromEnum(limit), file_reader.getSize() catch return null); + return std.math.lossyCast(usize, total + n); +} + +/// If the total number of bytes of `data` fits inside `unusedCapacitySlice`, +/// this function is guaranteed to not fail, not call into `VTable`, and return +/// the total bytes inside `data`. +pub fn writeVec(w: *Writer, data: []const []const u8) Error!usize { + return writeSplat(w, data, 1); +} + +/// If the number of bytes to write based on `data` and `splat` fits inside +/// `unusedCapacitySlice`, this function is guaranteed to not fail, not call +/// into `VTable`, and return the full number of bytes. +pub fn writeSplat(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + assert(data.len > 0); + const buffer = w.buffer; + const count = countSplat(data, splat); + if (w.end + count > buffer.len) { + const n = try w.vtable.drain(w, data, splat); + w.count += n; + return n; + } + w.count += count; + for (data) |bytes| { + @memcpy(buffer[w.end..][0..bytes.len], bytes); + w.end += bytes.len; } + const pattern = data[data.len - 1]; + if (splat == 0) { + @branchHint(.unlikely); + w.end -= pattern.len; + return count; + } + const remaining_splat = splat - 1; + switch (pattern.len) { + 0 => {}, + 1 => { + @memset(buffer[w.end..][0..remaining_splat], pattern[0]); + w.end += remaining_splat; + }, + else => { + const new_end = w.end + pattern.len * remaining_splat; + while (w.end < new_end) : (w.end += pattern.len) { + @memcpy(buffer[w.end..][0..pattern.len], pattern); + } + }, + } + return count; } -pub fn print(self: Self, comptime format: []const u8, args: anytype) anyerror!void { - return std.fmt.format(self, format, args); +/// Equivalent to `writeSplat` but writes at most `limit` bytes. +pub fn writeSplatLimit( + w: *Writer, + data: []const []const u8, + splat: usize, + limit: Limit, +) Error!usize { + _ = w; + _ = data; + _ = splat; + _ = limit; + @panic("TODO"); } -pub fn writeByte(self: Self, byte: u8) anyerror!void { - const array = [1]u8{byte}; - return self.writeAll(&array); +/// Returns how many bytes were consumed from `header` and `data`. +pub fn writeSplatHeader( + w: *Writer, + header: []const u8, + data: []const []const u8, + splat: usize, +) Error!usize { + const new_end = w.end + header.len; + if (new_end <= w.buffer.len) { + @memcpy(w.buffer[w.end..][0..header.len], header); + w.end = new_end; + w.count += header.len; + return header.len + try writeSplat(w, data, splat); + } + var vecs: [8][]const u8 = undefined; // Arbitrarily chosen size. + var i: usize = 1; + vecs[0] = header; + for (data) |buf| { + if (buf.len == 0) continue; + vecs[i] = buf; + i += 1; + if (vecs.len - i == 0) break; + } + const new_splat = if (vecs[i - 1].ptr == data[data.len - 1].ptr) splat else 1; + const n = try w.vtable.drain(w, vecs[0..i], new_splat); + w.count += n; + return n; } -pub fn writeByteNTimes(self: Self, byte: u8, n: usize) anyerror!void { - var bytes: [256]u8 = undefined; - @memset(bytes[0..], byte); +/// Equivalent to `writeSplatHeader` but writes at most `limit` bytes. +pub fn writeSplatHeaderLimit( + w: *Writer, + header: []const u8, + data: []const []const u8, + splat: usize, + limit: Limit, +) Error!usize { + _ = w; + _ = header; + _ = data; + _ = splat; + _ = limit; + @panic("TODO"); +} - var remaining: usize = n; - while (remaining > 0) { - const to_write = @min(remaining, bytes.len); - try self.writeAll(bytes[0..to_write]); - remaining -= to_write; +/// Drains all remaining buffered data. +pub fn flush(w: *Writer) Error!void { + return w.vtable.flush(w); +} + +/// Repeatedly calls `VTable.drain` until `end` is zero. +pub fn defaultFlush(w: *Writer) Error!void { + const drainFn = w.vtable.drain; + while (w.end != 0) _ = try drainFn(w, &.{""}, 1); +} + +/// Does nothing. +pub fn noopFlush(w: *Writer) Error!void { + _ = w; +} + +/// Calls `VTable.drain` but hides the last `preserve_length` bytes from the +/// implementation, keeping them buffered. +pub fn drainPreserve(w: *Writer, preserve_length: usize) Error!void { + const temp_end = w.end -| preserve_length; + const preserved = w.buffer[temp_end..w.end]; + w.end = temp_end; + defer w.end += preserved.len; + assert(0 == try w.vtable.drain(w, &.{""}, 1)); + assert(w.end <= temp_end + preserved.len); + @memmove(w.buffer[w.end..][0..preserved.len], preserved); +} + +pub fn unusedCapacitySlice(w: *const Writer) []u8 { + return w.buffer[w.end..]; +} + +pub fn unusedCapacityLen(w: *const Writer) usize { + return w.buffer.len - w.end; +} + +/// Asserts the provided buffer has total capacity enough for `len`. +/// +/// Advances the buffer end position by `len`. +pub fn writableArray(w: *Writer, comptime len: usize) Error!*[len]u8 { + const big_slice = try w.writableSliceGreedy(len); + advance(w, len); + return big_slice[0..len]; +} + +/// Asserts the provided buffer has total capacity enough for `len`. +/// +/// Advances the buffer end position by `len`. +pub fn writableSlice(w: *Writer, len: usize) Error![]u8 { + const big_slice = try w.writableSliceGreedy(len); + advance(w, len); + return big_slice[0..len]; +} + +/// Asserts the provided buffer has total capacity enough for `minimum_length`. +/// +/// Does not `advance` the buffer end position. +/// +/// If `minimum_length` is zero, this is equivalent to `unusedCapacitySlice`. +pub fn writableSliceGreedy(w: *Writer, minimum_length: usize) Error![]u8 { + assert(w.buffer.len >= minimum_length); + while (w.buffer.len - w.end < minimum_length) { + assert(0 == try w.vtable.drain(w, &.{""}, 1)); + } else { + @branchHint(.likely); + return w.buffer[w.end..]; } } -pub fn writeBytesNTimes(self: Self, bytes: []const u8, n: usize) anyerror!void { +/// Asserts the provided buffer has total capacity enough for `minimum_length` +/// and `preserve_length` combined. +/// +/// Does not `advance` the buffer end position. +/// +/// When draining the buffer, ensures that at least `preserve_length` bytes +/// remain buffered. +/// +/// If `preserve_length` is zero, this is equivalent to `writableSliceGreedy`. +pub fn writableSliceGreedyPreserve(w: *Writer, preserve_length: usize, minimum_length: usize) Error![]u8 { + assert(w.buffer.len >= preserve_length + minimum_length); + while (w.buffer.len - w.end < minimum_length) { + try drainPreserve(w, preserve_length); + } else { + @branchHint(.likely); + return w.buffer[w.end..]; + } +} + +pub const WritableVectorIterator = struct { + first: []u8, + middle: []const []u8 = &.{}, + last: []u8 = &.{}, + index: usize = 0, + + pub fn next(it: *WritableVectorIterator) ?[]u8 { + while (true) { + const i = it.index; + it.index += 1; + if (i == 0) { + if (it.first.len == 0) continue; + return it.first; + } + const middle_index = i - 1; + if (middle_index < it.middle.len) { + const middle = it.middle[middle_index]; + if (middle.len == 0) continue; + return middle; + } + if (middle_index == it.middle.len) { + if (it.last.len == 0) continue; + return it.last; + } + return null; + } + } +}; + +pub const VectorWrapper = struct { + writer: Writer, + it: WritableVectorIterator, + pub const vtable: VTable = .{ .drain = fixedDrain }; +}; + +pub fn writableVectorIterator(w: *Writer) Error!WritableVectorIterator { + if (w.vtable == &VectorWrapper.vtable) { + const wrapper: *VectorWrapper = @fieldParentPtr("writer", w); + return wrapper.it; + } + return .{ .first = try writableSliceGreedy(w, 1) }; +} + +pub fn writableVectorPosix(w: *Writer, buffer: []std.posix.iovec, limit: Limit) Error![]std.posix.iovec { + var it = try writableVectorIterator(w); var i: usize = 0; - while (i < n) : (i += 1) { - try self.writeAll(bytes); + var remaining = limit; + while (it.next()) |full_buffer| { + if (!remaining.nonzero()) break; + if (buffer.len - i == 0) break; + const buf = remaining.slice(full_buffer); + if (buf.len == 0) continue; + buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + remaining = remaining.subtract(buf.len).?; + } + return buffer[0..i]; +} + +pub fn ensureUnusedCapacity(w: *Writer, n: usize) Error!void { + _ = try writableSliceGreedy(w, n); +} + +pub fn undo(w: *Writer, n: usize) void { + w.end -= n; + w.count -= n; +} + +/// After calling `writableSliceGreedy`, this function tracks how many bytes +/// were written to it. +/// +/// This is not needed when using `writableSlice` or `writableArray`. +pub fn advance(w: *Writer, n: usize) void { + const new_end = w.end + n; + assert(new_end <= w.buffer.len); + w.end = new_end; + w.count += n; +} + +/// After calling `writableVector`, this function tracks how many bytes were +/// written to it. +pub fn advanceVector(w: *Writer, n: usize) usize { + w.count += n; + return consume(w, n); +} + +/// The `data` parameter is mutable because this function needs to mutate the +/// fields in order to handle partial writes from `VTable.writeSplat`. +pub fn writeVecAll(w: *Writer, data: [][]const u8) Error!void { + var index: usize = 0; + var truncate: usize = 0; + while (index < data.len) { + { + const untruncated = data[index]; + data[index] = untruncated[truncate..]; + defer data[index] = untruncated; + truncate += try w.writeVec(data[index..]); + } + while (index < data.len and truncate >= data[index].len) { + truncate -= data[index].len; + index += 1; + } } } -pub inline fn writeInt(self: Self, comptime T: type, value: T, endian: std.builtin.Endian) anyerror!void { +/// The `data` parameter is mutable because this function needs to mutate the +/// fields in order to handle partial writes from `VTable.writeSplat`. +pub fn writeSplatAll(w: *Writer, data: [][]const u8, splat: usize) Error!void { + var index: usize = 0; + var truncate: usize = 0; + var remaining_splat = splat; + while (index + 1 < data.len) { + { + const untruncated = data[index]; + data[index] = untruncated[truncate..]; + defer data[index] = untruncated; + truncate += try w.writeSplat(data[index..], remaining_splat); + } + while (truncate >= data[index].len) { + if (index + 1 < data.len) { + truncate -= data[index].len; + index += 1; + } else { + const last = data[data.len - 1]; + remaining_splat -= @divExact(truncate, last.len); + while (remaining_splat > 0) { + const n = try w.writeSplat(data[data.len - 1 ..][0..1], remaining_splat); + remaining_splat -= @divExact(n, last.len); + } + return; + } + } + } +} + +pub fn write(w: *Writer, bytes: []const u8) Error!usize { + if (w.end + bytes.len <= w.buffer.len) { + @branchHint(.likely); + @memcpy(w.buffer[w.end..][0..bytes.len], bytes); + w.end += bytes.len; + w.count += bytes.len; + return bytes.len; + } + const n = try w.vtable.drain(w, &.{bytes}, 1); + w.count += n; + return n; +} + +/// Asserts `buffer` capacity exceeds `preserve_length`. +pub fn writePreserve(w: *Writer, preserve_length: usize, bytes: []const u8) Error!usize { + assert(preserve_length <= w.buffer.len); + if (w.end + bytes.len <= w.buffer.len) { + @branchHint(.likely); + @memcpy(w.buffer[w.end..][0..bytes.len], bytes); + w.end += bytes.len; + w.count += bytes.len; + return bytes.len; + } + const temp_end = w.end -| preserve_length; + const preserved = w.buffer[temp_end..w.end]; + w.end = temp_end; + defer w.end += preserved.len; + const n = try w.vtable.drain(w, &.{bytes}, 1); + w.count += n; + assert(w.end <= temp_end + preserved.len); + @memmove(w.buffer[w.end..][0..preserved.len], preserved); + return n; +} + +/// Calls `drain` as many times as necessary such that all of `bytes` are +/// transferred. +pub fn writeAll(w: *Writer, bytes: []const u8) Error!void { + var index: usize = 0; + while (index < bytes.len) index += try w.write(bytes[index..]); +} + +/// Calls `drain` as many times as necessary such that all of `bytes` are +/// transferred. +/// +/// When draining the buffer, ensures that at least `preserve_length` bytes +/// remain buffered. +/// +/// Asserts `buffer` capacity exceeds `preserve_length`. +pub fn writeAllPreserve(w: *Writer, preserve_length: usize, bytes: []const u8) Error!void { + var index: usize = 0; + while (index < bytes.len) index += try w.writePreserve(preserve_length, bytes[index..]); +} + +pub fn print(w: *Writer, comptime format: []const u8, args: anytype) Error!void { + try std.fmt.format(w, format, args); +} + +/// Calls `drain` as many times as necessary such that `byte` is transferred. +pub fn writeByte(w: *Writer, byte: u8) Error!void { + while (w.buffer.len - w.end == 0) { + const n = try w.vtable.drain(w, &.{&.{byte}}, 1); + if (n > 0) { + w.count += 1; + return; + } + } else { + @branchHint(.likely); + w.buffer[w.end] = byte; + w.end += 1; + w.count += 1; + } +} + +/// When draining the buffer, ensures that at least `preserve_length` bytes +/// remain buffered. +pub fn writeBytePreserve(w: *Writer, preserve_length: usize, byte: u8) Error!void { + while (w.buffer.len - w.end == 0) { + try drainPreserve(w, preserve_length); + } else { + @branchHint(.likely); + w.buffer[w.end] = byte; + w.end += 1; + w.count += 1; + } +} + +/// Writes the same byte many times, performing the underlying write call as +/// many times as necessary. +pub fn splatByteAll(w: *Writer, byte: u8, n: usize) Error!void { + var remaining: usize = n; + while (remaining > 0) remaining -= try w.splatByte(byte, remaining); +} + +/// Writes the same byte many times, allowing short writes. +/// +/// Does maximum of one underlying `VTable.drain`. +pub fn splatByte(w: *Writer, byte: u8, n: usize) Error!usize { + return writeSplat(w, &.{&.{byte}}, n); +} + +/// Writes the same slice many times, performing the underlying write call as +/// many times as necessary. +pub fn splatBytesAll(w: *Writer, bytes: []const u8, splat: usize) Error!void { + var remaining_bytes: usize = bytes.len * splat; + remaining_bytes -= try w.splatBytes(bytes, splat); + while (remaining_bytes > 0) { + const leftover = remaining_bytes % bytes.len; + const buffers: [2][]const u8 = .{ bytes[bytes.len - leftover ..], bytes }; + remaining_bytes -= try w.splatBytes(&buffers, splat); + } +} + +/// Writes the same slice many times, allowing short writes. +/// +/// Does maximum of one underlying `VTable.writeSplat`. +pub fn splatBytes(w: *Writer, bytes: []const u8, n: usize) Error!usize { + return writeSplat(w, &.{bytes}, n); +} + +/// Asserts the `buffer` was initialized with a capacity of at least `@sizeOf(T)` bytes. +pub inline fn writeInt(w: *Writer, comptime T: type, value: T, endian: std.builtin.Endian) Error!void { var bytes: [@divExact(@typeInfo(T).int.bits, 8)]u8 = undefined; - mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian); - return self.writeAll(&bytes); + std.mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian); + return w.writeAll(&bytes); } -pub fn writeStruct(self: Self, value: anytype) anyerror!void { +pub fn writeStruct(w: *Writer, value: anytype) Error!void { // Only extern and packed structs have defined in-memory layout. comptime assert(@typeInfo(@TypeOf(value)).@"struct".layout != .auto); - return self.writeAll(mem.asBytes(&value)); + return w.writeAll(std.mem.asBytes(&value)); } -pub fn writeStructEndian(self: Self, value: anytype, endian: std.builtin.Endian) anyerror!void { - // TODO: make sure this value is not a reference type +/// The function is inline to avoid the dead code in case `endian` is +/// comptime-known and matches host endianness. +/// TODO: make sure this value is not a reference type +pub inline fn writeStructEndian(w: *Writer, value: anytype, endian: std.builtin.Endian) Error!void { if (native_endian == endian) { - return self.writeStruct(value); + return w.writeStruct(value); } else { var copy = value; - mem.byteSwapAllFields(@TypeOf(value), ©); - return self.writeStruct(copy); + std.mem.byteSwapAllFields(@TypeOf(value), ©); + return w.writeStruct(copy); + } +} + +pub inline fn writeSliceEndian( + w: *Writer, + Elem: type, + slice: []const Elem, + endian: std.builtin.Endian, +) Error!void { + if (native_endian == endian) { + return writeAll(w, @ptrCast(slice)); + } else { + return w.writeArraySwap(w, Elem, slice); + } +} + +/// Asserts that the buffer storage capacity is at least enough to store `@sizeOf(Elem)` +pub fn writeSliceSwap(w: *Writer, Elem: type, slice: []const Elem) Error!void { + // copy to storage first, then swap in place + _ = w; + _ = slice; + @panic("TODO"); +} + +/// Unlike `writeSplat` and `writeVec`, this function will call into `VTable` +/// even if there is enough buffer capacity for the file contents. +/// +/// Although it would be possible to eliminate `error.Unimplemented` from the +/// error set by reading directly into the buffer in such case, this is not +/// done because it is more efficient to do it higher up the call stack so that +/// the error does not occur with each write. +/// +/// See `sendFileReading` for an alternative that does not have +/// `error.Unimplemented` in the error set. +pub fn sendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { + return w.vtable.sendFile(w, file_reader, limit); +} + +/// Returns how many bytes from `header` and `file_reader` were consumed. +pub fn sendFileHeader( + w: *Writer, + header: []const u8, + file_reader: *File.Reader, + limit: Limit, +) FileError!usize { + const new_end = w.end + header.len; + if (new_end <= w.buffer.len) { + @memcpy(w.buffer[w.end..][0..header.len], header); + w.end = new_end; + w.count += header.len; + return header.len + try w.vtable.sendFile(w, file_reader, limit); + } + const buffered_contents = limit.slice(file_reader.interface.buffered()); + const n = try w.vtable.drain(w, &.{ header, buffered_contents }, 1); + w.count += n; + file_reader.interface.toss(n - header.len); + return n; +} + +/// Asserts nonzero buffer capacity. +pub fn sendFileReading(w: *Writer, file_reader: *File.Reader, limit: Limit) FileReadingError!usize { + const dest = limit.slice(try w.writableSliceGreedy(1)); + const n = try file_reader.read(dest); + w.advance(n); + return n; +} + +pub fn sendFileAll(w: *Writer, file_reader: *File.Reader, limit: Limit) FileAllError!usize { + var remaining = @intFromEnum(limit); + while (remaining > 0) { + const n = sendFile(w, file_reader, .limited(remaining)) catch |err| switch (err) { + error.EndOfStream => break, + error.Unimplemented => { + file_reader.mode = file_reader.mode.toReading(); + remaining -= try w.sendFileReadingAll(file_reader, .limited(remaining)); + break; + }, + else => |e| return e, + }; + remaining -= n; + } + return @intFromEnum(limit) - remaining; +} + +/// Equivalent to `sendFileAll` but uses direct `pread` and `read` calls on +/// `file` rather than `sendFile`. This is generally used as a fallback when +/// the underlying implementation returns `error.Unimplemented`, which is why +/// that error code does not appear in this function's error set. +/// +/// Asserts nonzero buffer capacity. +pub fn sendFileReadingAll(w: *Writer, file_reader: *File.Reader, limit: Limit) FileAllError!usize { + var remaining = @intFromEnum(limit); + while (remaining > 0) { + remaining -= sendFileReading(w, file_reader, .limited(remaining)) catch |err| switch (err) { + error.EndOfStream => break, + else => |e| return e, + }; } + return @intFromEnum(limit) - remaining; } -pub fn writeFile(self: Self, file: std.fs.File) anyerror!void { - // TODO: figure out how to adjust std lib abstractions so that this ends up - // doing sendfile or maybe even copy_file_range under the right conditions. - var buf: [4000]u8 = undefined; +pub fn alignBuffer( + w: *Writer, + buffer: []const u8, + width: usize, + alignment: std.fmt.Alignment, + fill: u8, +) Error!void { + const padding = if (buffer.len < width) width - buffer.len else 0; + if (padding == 0) { + @branchHint(.likely); + return w.writeAll(buffer); + } + switch (alignment) { + .left => { + try w.writeAll(buffer); + try w.splatByteAll(fill, padding); + }, + .center => { + const left_padding = padding / 2; + const right_padding = (padding + 1) / 2; + try w.splatByteAll(fill, left_padding); + try w.writeAll(buffer); + try w.splatByteAll(fill, right_padding); + }, + .right => { + try w.splatByteAll(fill, padding); + try w.writeAll(buffer); + }, + } +} + +pub fn alignBufferOptions(w: *Writer, buffer: []const u8, options: std.fmt.Options) Error!void { + return w.alignBuffer(buffer, options.width orelse buffer.len, options.alignment, options.fill); +} + +pub fn printAddress(w: *Writer, value: anytype) Error!void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |info| { + try w.writeAll(@typeName(info.child) ++ "@"); + if (info.size == .slice) + try w.printIntOptions(@intFromPtr(value.ptr), 16, .lower, .{}) + else + try w.printIntOptions(@intFromPtr(value), 16, .lower, .{}); + return; + }, + .optional => |info| { + if (@typeInfo(info.child) == .pointer) { + try w.writeAll(@typeName(info.child) ++ "@"); + try w.printIntOptions(@intFromPtr(value), 16, .lower, .{}); + return; + } + }, + else => {}, + } + + @compileError("cannot format non-pointer type " ++ @typeName(T) ++ " with * specifier"); +} + +pub fn printValue( + w: *Writer, + comptime fmt: []const u8, + options: std.fmt.Options, + value: anytype, + max_depth: usize, +) Error!void { + const T = @TypeOf(value); + + if (comptime std.mem.eql(u8, fmt, "*")) { + return w.printAddress(value); + } + + const is_any = comptime std.mem.eql(u8, fmt, ANY); + if (!is_any and std.meta.hasMethod(T, "format")) { + if (fmt.len > 0 and fmt[0] == 'f') { + return value.format(w, fmt[1..]); + } else if (fmt.len == 0) { + // after 0.15.0 is tagged, delete the hasMethod condition and this compile error + @compileError("ambiguous format string; specify {f} to call format method, or {any} to skip it"); + } + } + + switch (@typeInfo(T)) { + .float, .comptime_float => return w.printFloat(if (is_any) "d" else fmt, options, value), + .int, .comptime_int => return w.printInt(if (is_any) "d" else fmt, options, value), + .bool => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return w.alignBufferOptions(if (value) "true" else "false", options); + }, + .void => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return w.alignBufferOptions("void", options); + }, + .optional => { + const remaining_fmt = comptime if (fmt.len > 0 and fmt[0] == '?') + stripOptionalOrErrorUnionSpec(fmt) + else if (is_any) + ANY + else + @compileError("cannot print optional without a specifier (i.e. {?} or {any})"); + if (value) |payload| { + return w.printValue(remaining_fmt, options, payload, max_depth); + } else { + return w.alignBufferOptions("null", options); + } + }, + .error_union => { + const remaining_fmt = comptime if (fmt.len > 0 and fmt[0] == '!') + stripOptionalOrErrorUnionSpec(fmt) + else if (is_any) + ANY + else + @compileError("cannot print error union without a specifier (i.e. {!} or {any})"); + if (value) |payload| { + return w.printValue(remaining_fmt, options, payload, max_depth); + } else |err| { + return w.printValue("", options, err, max_depth); + } + }, + .error_set => { + if (fmt.len == 1 and fmt[0] == 's') return w.writeAll(@errorName(value)); + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + try printErrorSet(w, value); + }, + .@"enum" => { + if (fmt.len == 1 and fmt[0] == 's') { + try w.writeAll(@tagName(value)); + return; + } + if (!is_any) { + if (fmt.len != 0) return printValue(w, fmt, options, @intFromEnum(value), max_depth); + return printValue(w, ANY, options, value, max_depth); + } + const enum_info = @typeInfo(T).@"enum"; + if (enum_info.is_exhaustive) { + var vecs: [3][]const u8 = .{ @typeName(T), ".", @tagName(value) }; + try w.writeVecAll(&vecs); + return; + } + try w.writeAll(@typeName(T)); + @setEvalBranchQuota(3 * enum_info.fields.len); + inline for (enum_info.fields) |field| { + if (@intFromEnum(value) == field.value) { + try w.writeAll("."); + try w.writeAll(@tagName(value)); + return; + } + } + try w.writeByte('('); + try w.printValue(ANY, options, @intFromEnum(value), max_depth); + try w.writeByte(')'); + }, + .@"union" => |info| { + if (!is_any) { + if (fmt.len != 0) invalidFmtError(fmt, value); + return printValue(w, ANY, options, value, max_depth); + } + try w.writeAll(@typeName(T)); + if (max_depth == 0) { + try w.writeAll("{ ... }"); + return; + } + if (info.tag_type) |UnionTagType| { + try w.writeAll("{ ."); + try w.writeAll(@tagName(@as(UnionTagType, value))); + try w.writeAll(" = "); + inline for (info.fields) |u_field| { + if (value == @field(UnionTagType, u_field.name)) { + try w.printValue(ANY, options, @field(value, u_field.name), max_depth - 1); + } + } + try w.writeAll(" }"); + } else { + try w.writeByte('@'); + try w.printIntOptions(@intFromPtr(&value), 16, .lower, options); + } + }, + .@"struct" => |info| { + if (!is_any) { + if (fmt.len != 0) invalidFmtError(fmt, value); + return printValue(w, ANY, options, value, max_depth); + } + if (info.is_tuple) { + // Skip the type and field names when formatting tuples. + if (max_depth == 0) { + try w.writeAll("{ ... }"); + return; + } + try w.writeAll("{"); + inline for (info.fields, 0..) |f, i| { + if (i == 0) { + try w.writeAll(" "); + } else { + try w.writeAll(", "); + } + try w.printValue(ANY, options, @field(value, f.name), max_depth - 1); + } + try w.writeAll(" }"); + return; + } + try w.writeAll(@typeName(T)); + if (max_depth == 0) { + try w.writeAll("{ ... }"); + return; + } + try w.writeAll("{"); + inline for (info.fields, 0..) |f, i| { + if (i == 0) { + try w.writeAll(" ."); + } else { + try w.writeAll(", ."); + } + try w.writeAll(f.name); + try w.writeAll(" = "); + try w.printValue(ANY, options, @field(value, f.name), max_depth - 1); + } + try w.writeAll(" }"); + }, + .pointer => |ptr_info| switch (ptr_info.size) { + .one => switch (@typeInfo(ptr_info.child)) { + .array, .@"enum", .@"union", .@"struct" => { + return w.printValue(fmt, options, value.*, max_depth); + }, + else => { + var buffers: [2][]const u8 = .{ @typeName(ptr_info.child), "@" }; + try w.writeVecAll(&buffers); + try w.printIntOptions(@intFromPtr(value), 16, .lower, options); + return; + }, + }, + .many, .c => { + if (ptr_info.sentinel() != null) + return w.printValue(fmt, options, std.mem.span(value), max_depth); + if (fmt.len == 1 and fmt[0] == 's' and ptr_info.child == u8) + return w.alignBufferOptions(std.mem.span(value), options); + if (!is_any and fmt.len == 0) + @compileError("cannot format pointer without a specifier (i.e. {s} or {*})"); + if (!is_any and fmt.len != 0) + invalidFmtError(fmt, value); + try w.printAddress(value); + }, + .slice => { + if (!is_any and fmt.len == 0) + @compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})"); + if (max_depth == 0) + return w.writeAll("{ ... }"); + if (ptr_info.child == u8) switch (fmt.len) { + 1 => switch (fmt[0]) { + 's' => return w.alignBufferOptions(value, options), + 'x' => return w.printHex(value, .lower), + 'X' => return w.printHex(value, .upper), + else => {}, + }, + 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') { + return w.printBase64(value); + }, + else => {}, + }; + try w.writeAll("{ "); + for (value, 0..) |elem, i| { + try w.printValue(fmt, options, elem, max_depth - 1); + if (i != value.len - 1) { + try w.writeAll(", "); + } + } + try w.writeAll(" }"); + }, + }, + .array => |info| { + if (fmt.len == 0) + @compileError("cannot format array without a specifier (i.e. {s} or {any})"); + if (max_depth == 0) { + return w.writeAll("{ ... }"); + } + if (info.child == u8) { + if (fmt[0] == 's') { + return w.alignBufferOptions(&value, options); + } else if (fmt[0] == 'x') { + return w.printHex(&value, .lower); + } else if (fmt[0] == 'X') { + return w.printHex(&value, .upper); + } + } + try w.writeAll("{ "); + for (value, 0..) |elem, i| { + try w.printValue(fmt, options, elem, max_depth - 1); + if (i < value.len - 1) { + try w.writeAll(", "); + } + } + try w.writeAll(" }"); + }, + .vector => |info| { + if (max_depth == 0) { + return w.writeAll("{ ... }"); + } + try w.writeAll("{ "); + var i: usize = 0; + while (i < info.len) : (i += 1) { + try w.printValue(fmt, options, value[i], max_depth - 1); + if (i < info.len - 1) { + try w.writeAll(", "); + } + } + try w.writeAll(" }"); + }, + .@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"), + .type => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return w.alignBufferOptions(@typeName(value), options); + }, + .enum_literal => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + const buffer = [_]u8{'.'} ++ @tagName(value); + return w.alignBufferOptions(buffer, options); + }, + .null => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return w.alignBufferOptions("null", options); + }, + else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"), + } +} + +fn printErrorSet(w: *Writer, error_set: anyerror) Error!void { + var vecs: [2][]const u8 = .{ "error.", @errorName(error_set) }; + try w.writeVecAll(&vecs); +} + +pub fn printInt( + w: *Writer, + comptime fmt: []const u8, + options: std.fmt.Options, + value: anytype, +) Error!void { + const int_value = if (@TypeOf(value) == comptime_int) blk: { + const Int = std.math.IntFittingRange(value, value); + break :blk @as(Int, value); + } else value; + + switch (fmt.len) { + 0 => return w.printIntOptions(int_value, 10, .lower, options), + 1 => switch (fmt[0]) { + 'd' => return w.printIntOptions(int_value, 10, .lower, options), + 'c' => { + if (@typeInfo(@TypeOf(int_value)).int.bits <= 8) { + return w.printAsciiChar(@as(u8, int_value), options); + } else { + @compileError("cannot print integer that is larger than 8 bits as an ASCII character"); + } + }, + 'u' => { + if (@typeInfo(@TypeOf(int_value)).int.bits <= 21) { + return w.printUnicodeCodepoint(@as(u21, int_value), options); + } else { + @compileError("cannot print integer that is larger than 21 bits as an UTF-8 sequence"); + } + }, + 'b' => return w.printIntOptions(int_value, 2, .lower, options), + 'x' => return w.printIntOptions(int_value, 16, .lower, options), + 'X' => return w.printIntOptions(int_value, 16, .upper, options), + 'o' => return w.printIntOptions(int_value, 8, .lower, options), + 'B' => return w.printByteSize(int_value, .decimal, options), + 'D' => return w.printDuration(int_value, options), + else => invalidFmtError(fmt, value), + }, + 2 => { + if (fmt[0] == 'B' and fmt[1] == 'i') { + return w.printByteSize(int_value, .binary, options); + } else { + invalidFmtError(fmt, value); + } + }, + else => invalidFmtError(fmt, value), + } + comptime unreachable; +} + +pub fn printAsciiChar(w: *Writer, c: u8, options: std.fmt.Options) Error!void { + return w.alignBufferOptions(@as(*const [1]u8, &c), options); +} + +pub fn printAscii(w: *Writer, bytes: []const u8, options: std.fmt.Options) Error!void { + return w.alignBufferOptions(bytes, options); +} + +pub fn printUnicodeCodepoint(w: *Writer, c: u21, options: std.fmt.Options) Error!void { + var buf: [4]u8 = undefined; + const len = try std.unicode.utf8Encode(c, &buf); + return w.alignBufferOptions(buf[0..len], options); +} + +pub fn printIntOptions( + w: *Writer, + value: anytype, + base: u8, + case: std.fmt.Case, + options: std.fmt.Options, +) Error!void { + assert(base >= 2); + + const int_value = if (@TypeOf(value) == comptime_int) blk: { + const Int = std.math.IntFittingRange(value, value); + break :blk @as(Int, value); + } else value; + + const value_info = @typeInfo(@TypeOf(int_value)).int; + + // The type must have the same size as `base` or be wider in order for the + // division to work + const min_int_bits = comptime @max(value_info.bits, 8); + const MinInt = std.meta.Int(.unsigned, min_int_bits); + + const abs_value = @abs(int_value); + // The worst case in terms of space needed is base 2, plus 1 for the sign + var buf: [1 + @max(@as(comptime_int, value_info.bits), 1)]u8 = undefined; + + var a: MinInt = abs_value; + var index: usize = buf.len; + + if (base == 10) { + while (a >= 100) : (a = @divTrunc(a, 100)) { + index -= 2; + buf[index..][0..2].* = std.fmt.digits2(@intCast(a % 100)); + } + + if (a < 10) { + index -= 1; + buf[index] = '0' + @as(u8, @intCast(a)); + } else { + index -= 2; + buf[index..][0..2].* = std.fmt.digits2(@intCast(a)); + } + } else { + while (true) { + const digit = a % base; + index -= 1; + buf[index] = std.fmt.digitToChar(@intCast(digit), case); + a /= base; + if (a == 0) break; + } + } + + if (value_info.signedness == .signed) { + if (value < 0) { + // Negative integer + index -= 1; + buf[index] = '-'; + } else if (options.width == null or options.width.? == 0) { + // Positive integer, omit the plus sign + } else { + // Positive integer + index -= 1; + buf[index] = '+'; + } + } + + return w.alignBufferOptions(buf[index..], options); +} + +pub fn printFloat( + w: *Writer, + comptime fmt: []const u8, + options: std.fmt.Options, + value: anytype, +) Error!void { + var buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined; + + if (fmt.len > 1) invalidFmtError(fmt, value); + switch (if (fmt.len == 0) 'e' else fmt[0]) { + 'e' => { + const s = std.fmt.float.render(&buf, value, .{ .mode = .scientific, .precision = options.precision }) catch |err| switch (err) { + error.BufferTooSmall => "(float)", + }; + return w.alignBufferOptions(s, options); + }, + 'd' => { + const s = std.fmt.float.render(&buf, value, .{ .mode = .decimal, .precision = options.precision }) catch |err| switch (err) { + error.BufferTooSmall => "(float)", + }; + return w.alignBufferOptions(s, options); + }, + 'x' => { + var sub_bw: Writer = .fixed(&buf); + sub_bw.printFloatHexadecimal(value, options.precision) catch unreachable; + return w.alignBufferOptions(sub_bw.buffered(), options); + }, + else => invalidFmtError(fmt, value), + } +} + +pub fn printFloatHexadecimal(w: *Writer, value: anytype, opt_precision: ?usize) Error!void { + if (std.math.signbit(value)) try w.writeByte('-'); + if (std.math.isNan(value)) return w.writeAll("nan"); + if (std.math.isInf(value)) return w.writeAll("inf"); + + const T = @TypeOf(value); + const TU = std.meta.Int(.unsigned, @bitSizeOf(T)); + + const mantissa_bits = std.math.floatMantissaBits(T); + const fractional_bits = std.math.floatFractionalBits(T); + const exponent_bits = std.math.floatExponentBits(T); + const mantissa_mask = (1 << mantissa_bits) - 1; + const exponent_mask = (1 << exponent_bits) - 1; + const exponent_bias = (1 << (exponent_bits - 1)) - 1; + + const as_bits: TU = @bitCast(value); + var mantissa = as_bits & mantissa_mask; + var exponent: i32 = @as(u16, @truncate((as_bits >> mantissa_bits) & exponent_mask)); + + const is_denormal = exponent == 0 and mantissa != 0; + const is_zero = exponent == 0 and mantissa == 0; + + if (is_zero) { + // Handle this case here to simplify the logic below. + try w.writeAll("0x0"); + if (opt_precision) |precision| { + if (precision > 0) { + try w.writeAll("."); + try w.splatByteAll('0', precision); + } + } else { + try w.writeAll(".0"); + } + try w.writeAll("p0"); + return; + } + + if (is_denormal) { + // Adjust the exponent for printing. + exponent += 1; + } else { + if (fractional_bits == mantissa_bits) + mantissa |= 1 << fractional_bits; // Add the implicit integer bit. + } + + const mantissa_digits = (fractional_bits + 3) / 4; + // Fill in zeroes to round the fraction width to a multiple of 4. + mantissa <<= mantissa_digits * 4 - fractional_bits; + + if (opt_precision) |precision| { + // Round if needed. + if (precision < mantissa_digits) { + // We always have at least 4 extra bits. + var extra_bits = (mantissa_digits - precision) * 4; + // The result LSB is the Guard bit, we need two more (Round and + // Sticky) to round the value. + while (extra_bits > 2) { + mantissa = (mantissa >> 1) | (mantissa & 1); + extra_bits -= 1; + } + // Round to nearest, tie to even. + mantissa |= @intFromBool(mantissa & 0b100 != 0); + mantissa += 1; + // Drop the excess bits. + mantissa >>= 2; + // Restore the alignment. + mantissa <<= @as(std.math.Log2Int(TU), @intCast((mantissa_digits - precision) * 4)); + + const overflow = mantissa & (1 << 1 + mantissa_digits * 4) != 0; + // Prefer a normalized result in case of overflow. + if (overflow) { + mantissa >>= 1; + exponent += 1; + } + } + } + + // +1 for the decimal part. + var buf: [1 + mantissa_digits]u8 = undefined; + assert(std.fmt.printInt(&buf, mantissa, 16, .lower, .{ .fill = '0', .width = 1 + mantissa_digits }) == buf.len); + + try w.writeAll("0x"); + try w.writeByte(buf[0]); + const trimmed = std.mem.trimRight(u8, buf[1..], "0"); + if (opt_precision) |precision| { + if (precision > 0) try w.writeAll("."); + } else if (trimmed.len > 0) { + try w.writeAll("."); + } + try w.writeAll(trimmed); + // Add trailing zeros if explicitly requested. + if (opt_precision) |precision| if (precision > 0) { + if (precision > trimmed.len) + try w.splatByteAll('0', precision - trimmed.len); + }; + try w.writeAll("p"); + try w.printIntOptions(exponent - exponent_bias, 10, .lower, .{}); +} + +pub const ByteSizeUnits = enum { + /// This formatter represents the number as multiple of 1000 and uses the SI + /// measurement units (kB, MB, GB, ...). + decimal, + /// This formatter represents the number as multiple of 1024 and uses the IEC + /// measurement units (KiB, MiB, GiB, ...). + binary, +}; + +/// Format option `precision` is ignored when `value` is less than 1kB +pub fn printByteSize( + w: *std.io.Writer, + value: u64, + comptime units: ByteSizeUnits, + options: std.fmt.Options, +) Error!void { + if (value == 0) return w.alignBufferOptions("0B", options); + // The worst case in terms of space needed is 32 bytes + 3 for the suffix. + var buf: [std.fmt.float.min_buffer_size + 3]u8 = undefined; + + const mags_si = " kMGTPEZY"; + const mags_iec = " KMGTPEZY"; + + const log2 = std.math.log2(value); + const base = switch (units) { + .decimal => 1000, + .binary => 1024, + }; + const magnitude = switch (units) { + .decimal => @min(log2 / comptime std.math.log2(1000), mags_si.len - 1), + .binary => @min(log2 / 10, mags_iec.len - 1), + }; + const new_value = std.math.lossyCast(f64, value) / std.math.pow(f64, std.math.lossyCast(f64, base), std.math.lossyCast(f64, magnitude)); + const suffix = switch (units) { + .decimal => mags_si[magnitude], + .binary => mags_iec[magnitude], + }; + + const s = switch (magnitude) { + 0 => buf[0..std.fmt.printInt(&buf, value, 10, .lower, .{})], + else => std.fmt.float.render(&buf, new_value, .{ .mode = .decimal, .precision = options.precision }) catch |err| switch (err) { + error.BufferTooSmall => unreachable, + }, + }; + + var i: usize = s.len; + if (suffix == ' ') { + buf[i] = 'B'; + i += 1; + } else switch (units) { + .decimal => { + buf[i..][0..2].* = [_]u8{ suffix, 'B' }; + i += 2; + }, + .binary => { + buf[i..][0..3].* = [_]u8{ suffix, 'i', 'B' }; + i += 3; + }, + } + + return w.alignBufferOptions(buf[0..i], options); +} + +// This ANY const is a workaround for: https://github.com/ziglang/zig/issues/7948 +const ANY = "any"; + +fn stripOptionalOrErrorUnionSpec(comptime fmt: []const u8) []const u8 { + return if (std.mem.eql(u8, fmt[1..], ANY)) + ANY + else + fmt[1..]; +} + +pub fn invalidFmtError(comptime fmt: []const u8, value: anytype) noreturn { + @compileError("invalid format string '" ++ fmt ++ "' for type '" ++ @typeName(@TypeOf(value)) ++ "'"); +} + +pub fn printDurationSigned(w: *Writer, ns: i64) Error!void { + if (ns < 0) try w.writeByte('-'); + return w.printDurationUnsigned(@abs(ns)); +} + +pub fn printDurationUnsigned(w: *Writer, ns: u64) Error!void { + var ns_remaining = ns; + inline for (.{ + .{ .ns = 365 * std.time.ns_per_day, .sep = 'y' }, + .{ .ns = std.time.ns_per_week, .sep = 'w' }, + .{ .ns = std.time.ns_per_day, .sep = 'd' }, + .{ .ns = std.time.ns_per_hour, .sep = 'h' }, + .{ .ns = std.time.ns_per_min, .sep = 'm' }, + }) |unit| { + if (ns_remaining >= unit.ns) { + const units = ns_remaining / unit.ns; + try w.printIntOptions(units, 10, .lower, .{}); + try w.writeByte(unit.sep); + ns_remaining -= units * unit.ns; + if (ns_remaining == 0) return; + } + } + + inline for (.{ + .{ .ns = std.time.ns_per_s, .sep = "s" }, + .{ .ns = std.time.ns_per_ms, .sep = "ms" }, + .{ .ns = std.time.ns_per_us, .sep = "us" }, + }) |unit| { + const kunits = ns_remaining * 1000 / unit.ns; + if (kunits >= 1000) { + try w.printIntOptions(kunits / 1000, 10, .lower, .{}); + const frac = kunits % 1000; + if (frac > 0) { + // Write up to 3 decimal places + var decimal_buf = [_]u8{ '.', 0, 0, 0 }; + var inner: Writer = .fixed(decimal_buf[1..]); + inner.printIntOptions(frac, 10, .lower, .{ .fill = '0', .width = 3 }) catch unreachable; + var end: usize = 4; + while (end > 1) : (end -= 1) { + if (decimal_buf[end - 1] != '0') break; + } + try w.writeAll(decimal_buf[0..end]); + } + return w.writeAll(unit.sep); + } + } + + try w.printIntOptions(ns_remaining, 10, .lower, .{}); + try w.writeAll("ns"); +} + +/// Writes number of nanoseconds according to its signed magnitude: +/// `[#y][#w][#d][#h][#m]#[.###][n|u|m]s` +/// `nanoseconds` must be an integer that coerces into `u64` or `i64`. +pub fn printDuration(w: *Writer, nanoseconds: anytype, options: std.fmt.Options) Error!void { + // worst case: "-XXXyXXwXXdXXhXXmXX.XXXs".len = 24 + var buf: [24]u8 = undefined; + var sub_bw: Writer = .fixed(&buf); + switch (@typeInfo(@TypeOf(nanoseconds)).int.signedness) { + .signed => sub_bw.printDurationSigned(nanoseconds) catch unreachable, + .unsigned => sub_bw.printDurationUnsigned(nanoseconds) catch unreachable, + } + return w.alignBufferOptions(sub_bw.buffered(), options); +} + +pub fn printHex(w: *Writer, bytes: []const u8, case: std.fmt.Case) Error!void { + const charset = switch (case) { + .upper => "0123456789ABCDEF", + .lower => "0123456789abcdef", + }; + for (bytes) |c| { + try w.writeByte(charset[c >> 4]); + try w.writeByte(charset[c & 15]); + } +} + +pub fn printBase64(w: *Writer, bytes: []const u8) Error!void { + var chunker = std.mem.window(u8, bytes, 3, 3); + var temp: [5]u8 = undefined; + while (chunker.next()) |chunk| { + try w.writeAll(std.base64.standard.Encoder.encode(&temp, chunk)); + } +} + +/// Write a single unsigned integer as LEB128 to the given writer. +pub fn writeUleb128(w: *Writer, value: anytype) Error!void { + try w.writeLeb128(switch (@typeInfo(@TypeOf(value))) { + .comptime_int => @as(std.math.IntFittingRange(0, @abs(value)), value), + .int => |value_info| switch (value_info.signedness) { + .signed => @as(@Type(.{ .int = .{ .signedness = .unsigned, .bits = value_info.bits -| 1 } }), @intCast(value)), + .unsigned => value, + }, + else => comptime unreachable, + }); +} + +/// Write a single signed integer as LEB128 to the given writer. +pub fn writeSleb128(w: *Writer, value: anytype) Error!void { + try w.writeLeb128(switch (@typeInfo(@TypeOf(value))) { + .comptime_int => @as(std.math.IntFittingRange(@min(value, -1), @max(0, value)), value), + .int => |value_info| switch (value_info.signedness) { + .signed => value, + .unsigned => @as(@Type(.{ .int = .{ .signedness = .signed, .bits = value_info.bits + 1 } }), value), + }, + else => comptime unreachable, + }); +} + +/// Write a single integer as LEB128 to the given writer. +pub fn writeLeb128(w: *Writer, value: anytype) Error!void { + const value_info = @typeInfo(@TypeOf(value)).int; + try w.writeMultipleOf7Leb128(@as(@Type(.{ .int = .{ + .signedness = value_info.signedness, + .bits = std.mem.alignForwardAnyAlign(u16, value_info.bits, 7), + } }), value)); +} + +fn writeMultipleOf7Leb128(w: *Writer, value: anytype) Error!void { + const value_info = @typeInfo(@TypeOf(value)).int; + comptime assert(value_info.bits % 7 == 0); + var remaining = value; while (true) { - const n = try file.readAll(&buf); - try self.writeAll(buf[0..n]); - if (n < buf.len) return; + const buffer: []packed struct(u8) { bits: u7, more: bool } = @ptrCast(try w.writableSliceGreedy(1)); + for (buffer, 1..) |*byte, len| { + const more = switch (value_info.signedness) { + .signed => remaining >> 6 != remaining >> (value_info.bits - 1), + .unsigned => remaining > std.math.maxInt(u7), + }; + byte.* = if (@inComptime()) @typeInfo(@TypeOf(buffer)).pointer.child{ + .bits = @bitCast(@as(@Type(.{ .int = .{ + .signedness = value_info.signedness, + .bits = 7, + } }), @truncate(remaining))), + .more = more, + } else .{ + .bits = @bitCast(@as(@Type(.{ .int = .{ + .signedness = value_info.signedness, + .bits = 7, + } }), @truncate(remaining))), + .more = more, + }; + if (value_info.bits > 7) remaining >>= 7; + if (!more) return w.advance(len); + } + w.advance(buffer.len); + } +} + +test "formatValue max_depth" { + const Vec2 = struct { + const SelfType = @This(); + x: f32, + y: f32, + + pub fn format( + self: SelfType, + comptime fmt: []const u8, + options: std.fmt.Options, + w: *Writer, + ) Error!void { + _ = options; + if (fmt.len == 0) { + return w.print("({d:.3},{d:.3})", .{ self.x, self.y }); + } else { + @compileError("unknown format string: '" ++ fmt ++ "'"); + } + } + }; + const E = enum { + One, + Two, + Three, + }; + const TU = union(enum) { + const SelfType = @This(); + float: f32, + int: u32, + ptr: ?*SelfType, + }; + const S = struct { + const SelfType = @This(); + a: ?*SelfType, + tu: TU, + e: E, + vec: Vec2, + }; + + var inst = S{ + .a = null, + .tu = TU{ .ptr = null }, + .e = E.Two, + .vec = Vec2{ .x = 10.2, .y = 2.22 }, + }; + inst.a = &inst; + inst.tu.ptr = &inst.tu; + + var buf: [1000]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printValue("", .{}, inst, 0); + try testing.expectEqualStrings("io.Writer.test.printValue max_depth.S{ ... }", w.buffered()); + + w.reset(); + try w.printValue("", .{}, inst, 1); + try testing.expectEqualStrings("io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ ... }, .tu = io.Writer.test.printValue max_depth.TU{ ... }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", w.buffered()); + + w.reset(); + try w.printValue("", .{}, inst, 2); + try testing.expectEqualStrings("io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ ... }, .tu = io.Writer.test.printValue max_depth.TU{ ... }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.Writer.test.printValue max_depth.TU{ .ptr = io.Writer.test.printValue max_depth.TU{ ... } }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", w.buffered()); + + w.reset(); + try w.printValue("", .{}, inst, 3); + try testing.expectEqualStrings("io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ .a = io.Writer.test.printValue max_depth.S{ ... }, .tu = io.Writer.test.printValue max_depth.TU{ ... }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.Writer.test.printValue max_depth.TU{ .ptr = io.Writer.test.printValue max_depth.TU{ ... } }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.Writer.test.printValue max_depth.TU{ .ptr = io.Writer.test.printValue max_depth.TU{ .ptr = io.Writer.test.printValue max_depth.TU{ ... } } }, .e = io.Writer.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", w.buffered()); + + const vec: @Vector(4, i32) = .{ 1, 2, 3, 4 }; + w.reset(); + try w.printValue("", .{}, vec, 0); + try testing.expectEqualStrings("{ ... }", w.buffered()); + + w.reset(); + try w.printValue("", .{}, vec, 1); + try testing.expectEqualStrings("{ 1, 2, 3, 4 }", w.buffered()); +} + +test printDuration { + testDurationCase("0ns", 0); + testDurationCase("1ns", 1); + testDurationCase("999ns", std.time.ns_per_us - 1); + testDurationCase("1us", std.time.ns_per_us); + testDurationCase("1.45us", 1450); + testDurationCase("1.5us", 3 * std.time.ns_per_us / 2); + testDurationCase("14.5us", 14500); + testDurationCase("145us", 145000); + testDurationCase("999.999us", std.time.ns_per_ms - 1); + testDurationCase("1ms", std.time.ns_per_ms + 1); + testDurationCase("1.5ms", 3 * std.time.ns_per_ms / 2); + testDurationCase("1.11ms", 1110000); + testDurationCase("1.111ms", 1111000); + testDurationCase("1.111ms", 1111100); + testDurationCase("999.999ms", std.time.ns_per_s - 1); + testDurationCase("1s", std.time.ns_per_s); + testDurationCase("59.999s", std.time.ns_per_min - 1); + testDurationCase("1m", std.time.ns_per_min); + testDurationCase("1h", std.time.ns_per_hour); + testDurationCase("1d", std.time.ns_per_day); + testDurationCase("1w", std.time.ns_per_week); + testDurationCase("1y", 365 * std.time.ns_per_day); + testDurationCase("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1 + testDurationCase("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); + testDurationCase("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); + testDurationCase("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); + testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); + testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); + testDurationCase("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); + testDurationCase("584y49w23h34m33.709s", std.math.maxInt(u64)); + + testing.expectFmt("=======0ns", "{D:=>10}", .{0}); + testing.expectFmt("1ns=======", "{D:=<10}", .{1}); + testing.expectFmt(" 999ns ", "{D:^10}", .{std.time.ns_per_us - 1}); +} + +test printDurationSigned { + testDurationCaseSigned("0ns", 0); + testDurationCaseSigned("1ns", 1); + testDurationCaseSigned("-1ns", -(1)); + testDurationCaseSigned("999ns", std.time.ns_per_us - 1); + testDurationCaseSigned("-999ns", -(std.time.ns_per_us - 1)); + testDurationCaseSigned("1us", std.time.ns_per_us); + testDurationCaseSigned("-1us", -(std.time.ns_per_us)); + testDurationCaseSigned("1.45us", 1450); + testDurationCaseSigned("-1.45us", -(1450)); + testDurationCaseSigned("1.5us", 3 * std.time.ns_per_us / 2); + testDurationCaseSigned("-1.5us", -(3 * std.time.ns_per_us / 2)); + testDurationCaseSigned("14.5us", 14500); + testDurationCaseSigned("-14.5us", -(14500)); + testDurationCaseSigned("145us", 145000); + testDurationCaseSigned("-145us", -(145000)); + testDurationCaseSigned("999.999us", std.time.ns_per_ms - 1); + testDurationCaseSigned("-999.999us", -(std.time.ns_per_ms - 1)); + testDurationCaseSigned("1ms", std.time.ns_per_ms + 1); + testDurationCaseSigned("-1ms", -(std.time.ns_per_ms + 1)); + testDurationCaseSigned("1.5ms", 3 * std.time.ns_per_ms / 2); + testDurationCaseSigned("-1.5ms", -(3 * std.time.ns_per_ms / 2)); + testDurationCaseSigned("1.11ms", 1110000); + testDurationCaseSigned("-1.11ms", -(1110000)); + testDurationCaseSigned("1.111ms", 1111000); + testDurationCaseSigned("-1.111ms", -(1111000)); + testDurationCaseSigned("1.111ms", 1111100); + testDurationCaseSigned("-1.111ms", -(1111100)); + testDurationCaseSigned("999.999ms", std.time.ns_per_s - 1); + testDurationCaseSigned("-999.999ms", -(std.time.ns_per_s - 1)); + testDurationCaseSigned("1s", std.time.ns_per_s); + testDurationCaseSigned("-1s", -(std.time.ns_per_s)); + testDurationCaseSigned("59.999s", std.time.ns_per_min - 1); + testDurationCaseSigned("-59.999s", -(std.time.ns_per_min - 1)); + testDurationCaseSigned("1m", std.time.ns_per_min); + testDurationCaseSigned("-1m", -(std.time.ns_per_min)); + testDurationCaseSigned("1h", std.time.ns_per_hour); + testDurationCaseSigned("-1h", -(std.time.ns_per_hour)); + testDurationCaseSigned("1d", std.time.ns_per_day); + testDurationCaseSigned("-1d", -(std.time.ns_per_day)); + testDurationCaseSigned("1w", std.time.ns_per_week); + testDurationCaseSigned("-1w", -(std.time.ns_per_week)); + testDurationCaseSigned("1y", 365 * std.time.ns_per_day); + testDurationCaseSigned("-1y", -(365 * std.time.ns_per_day)); + testDurationCaseSigned("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1d + testDurationCaseSigned("-1y52w23h59m59.999s", -(730 * std.time.ns_per_day - 1)); // 365d = 52w1d + testDurationCaseSigned("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); + testDurationCaseSigned("-1y1h1.001s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms)); + testDurationCaseSigned("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); + testDurationCaseSigned("-1y1h1s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us)); + testDurationCaseSigned("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); + testDurationCaseSigned("-1y1h999.999us", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1)); + testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); + testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms)); + testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); + testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1)); + testDurationCaseSigned("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); + testDurationCaseSigned("-1y1m999ns", -(365 * std.time.ns_per_day + std.time.ns_per_min + 999)); + testDurationCaseSigned("292y24w3d23h47m16.854s", std.math.maxInt(i64)); + testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64) + 1); + testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64)); + + testing.expectFmt("=======0ns", "{s:=>10}", .{0}); + testing.expectFmt("1ns=======", "{s:=<10}", .{1}); + testing.expectFmt("-1ns======", "{s:=<10}", .{-(1)}); + testing.expectFmt(" -999ns ", "{s:^10}", .{-(std.time.ns_per_us - 1)}); +} + +fn testDurationCase(expected: []const u8, input: u64) !void { + var buf: [24]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printDurationUnsigned(input); + try testing.expectEqualStrings(expected, w.buffered()); +} + +fn testDurationCaseSigned(expected: []const u8, input: i64) !void { + var buf: [24]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printDurationSigned(input); + try testing.expectEqualStrings(expected, w.buffered()); +} + +test printIntOptions { + try testPrintIntCase("-1", @as(i1, -1), 10, .lower, .{}); + + try testPrintIntCase("-101111000110000101001110", @as(i32, -12345678), 2, .lower, .{}); + try testPrintIntCase("-12345678", @as(i32, -12345678), 10, .lower, .{}); + try testPrintIntCase("-bc614e", @as(i32, -12345678), 16, .lower, .{}); + try testPrintIntCase("-BC614E", @as(i32, -12345678), 16, .upper, .{}); + + try testPrintIntCase("12345678", @as(u32, 12345678), 10, .upper, .{}); + + try testPrintIntCase(" 666", @as(u32, 666), 10, .lower, .{ .width = 6 }); + try testPrintIntCase(" 1234", @as(u32, 0x1234), 16, .lower, .{ .width = 6 }); + try testPrintIntCase("1234", @as(u32, 0x1234), 16, .lower, .{ .width = 1 }); + + try testPrintIntCase("+42", @as(i32, 42), 10, .lower, .{ .width = 3 }); + try testPrintIntCase("-42", @as(i32, -42), 10, .lower, .{ .width = 3 }); +} + +test "printInt with comptime_int" { + var buf: [20]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printInt(@as(comptime_int, 123456789123456789), "", .{}); + try std.testing.expectEqualStrings("123456789123456789", w.buffered()); +} + +test "printFloat with comptime_float" { + var buf: [20]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printFloat("", .{}, @as(comptime_float, 1.0)); + try std.testing.expectEqualStrings(w.buffered(), "1e0"); + try std.testing.expectFmt("1e0", "{}", .{1.0}); +} + +fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void { + var buffer: [100]u8 = undefined; + var w: Writer = .fixed(&buffer); + w.printIntOptions(value, base, case, options); + try testing.expectEqualStrings(expected, w.buffered()); +} + +test printByteSize { + try testing.expectFmt("file size: 42B\n", "file size: {B}\n", .{42}); + try testing.expectFmt("file size: 42B\n", "file size: {Bi}\n", .{42}); + try testing.expectFmt("file size: 63MB\n", "file size: {B}\n", .{63 * 1000 * 1000}); + try testing.expectFmt("file size: 63MiB\n", "file size: {Bi}\n", .{63 * 1024 * 1024}); + try testing.expectFmt("file size: 42B\n", "file size: {B:.2}\n", .{42}); + try testing.expectFmt("file size: 42B\n", "file size: {B:>9.2}\n", .{42}); + try testing.expectFmt("file size: 66.06MB\n", "file size: {B:.2}\n", .{63 * 1024 * 1024}); + try testing.expectFmt("file size: 60.08MiB\n", "file size: {Bi:.2}\n", .{63 * 1000 * 1000}); + try testing.expectFmt("file size: =66.06MB=\n", "file size: {B:=^9.2}\n", .{63 * 1024 * 1024}); + try testing.expectFmt("file size: 66.06MB\n", "file size: {B: >9.2}\n", .{63 * 1024 * 1024}); + try testing.expectFmt("file size: 66.06MB \n", "file size: {B: <9.2}\n", .{63 * 1024 * 1024}); + try testing.expectFmt("file size: 0.01844674407370955ZB\n", "file size: {B}\n", .{std.math.maxInt(u64)}); +} + +test "bytes.hex" { + const some_bytes = "\xCA\xFE\xBA\xBE"; + try std.testing.expectFmt("lowercase: cafebabe\n", "lowercase: {x}\n", .{some_bytes}); + try std.testing.expectFmt("uppercase: CAFEBABE\n", "uppercase: {X}\n", .{some_bytes}); + try std.testing.expectFmt("uppercase: CAFE\n", "uppercase: {X}\n", .{some_bytes[0..2]}); + try std.testing.expectFmt("lowercase: babe\n", "lowercase: {x}\n", .{some_bytes[2..]}); + const bytes_with_zeros = "\x00\x0E\xBA\xBE"; + try std.testing.expectFmt("lowercase: 000ebabe\n", "lowercase: {x}\n", .{bytes_with_zeros}); +} + +test fixed { + { + var buf: [255]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.print("{s}{s}!", .{ "Hello", "World" }); + try testing.expectEqualStrings("HelloWorld!", w.buffered()); + } + + comptime { + var buf: [255]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.print("{s}{s}!", .{ "Hello", "World" }); + try testing.expectEqualStrings("HelloWorld!", w.buffered()); + } +} + +test "fixed output" { + var buffer: [10]u8 = undefined; + var w: Writer = .fixed(&buffer); + + try w.writeAll("Hello"); + try testing.expect(std.mem.eql(u8, w.buffered(), "Hello")); + + try w.writeAll("world"); + try testing.expect(std.mem.eql(u8, w.buffered(), "Helloworld")); + + try testing.expectError(error.WriteStreamEnd, w.writeAll("!")); + try testing.expect(std.mem.eql(u8, w.buffered(), "Helloworld")); + + w.reset(); + try testing.expect(w.buffered().len == 0); + + try testing.expectError(error.WriteStreamEnd, w.writeAll("Hello world!")); + try testing.expect(std.mem.eql(u8, w.buffered(), "Hello worl")); + + try w.seekTo((try w.getEndPos()) + 1); + try testing.expectError(error.WriteStreamEnd, w.writeAll("H")); +} + +pub fn failingDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + _ = w; + _ = data; + _ = splat; + return error.WriteFailed; +} + +pub fn failingSendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { + _ = w; + _ = file_reader; + _ = limit; + return error.WriteFailed; +} + +pub fn discardingDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + const slice = data[0 .. data.len - 1]; + const pattern = data[slice.len..]; + var written: usize = pattern.len * splat; + for (slice) |bytes| written += bytes.len; + w.end = 0; + return written; +} + +pub fn discardingSendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { + if (File.Handle == void) return error.Unimplemented; + w.end = 0; + if (file_reader.getSize()) |size| { + const n = limit.minInt(size - file_reader.pos); + file_reader.seekBy(@intCast(n)) catch return error.Unimplemented; + w.end = 0; + return n; + } else |_| { + // Error is observable on `file_reader` instance, and it is better to + // treat the file as a pipe. + return error.Unimplemented; + } +} + +/// Removes the first `n` bytes from `buffer` by shifting buffer contents, +/// returning how many bytes are left after consuming the entire buffer, or +/// zero if the entire buffer was not consumed. +/// +/// Useful for `VTable.drain` function implementations to implement partial +/// drains. +pub fn consume(w: *Writer, n: usize) usize { + if (n < w.end) { + const remaining = w.buffer[n..w.end]; + @memmove(w.buffer[0..remaining.len], remaining); + w.end = remaining.len; + return 0; + } + defer w.end = 0; + return n - w.end; +} + +/// Shortcut for setting `end` to zero and returning zero. Equivalent to +/// calling `consume` with `end`. +pub fn consumeAll(w: *Writer) usize { + w.end = 0; + return 0; +} + +/// For use when the `Writer` implementation can cannot offer a more efficient +/// implementation than a basic read/write loop on the file. +pub fn unimplementedSendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { + _ = w; + _ = file_reader; + _ = limit; + return error.Unimplemented; +} + +/// When this function is called it usually means the buffer got full, so it's +/// time to return an error. However, we still need to make sure all of the +/// available buffer has been filled. Also, it may be called from `flush` in +/// which case it should return successfully. +pub fn fixedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + if (data.len == 0) return 0; + for (data[0 .. data.len - 1]) |bytes| { + const dest = w.buffer[w.end..]; + const len = @min(bytes.len, dest.len); + @memcpy(dest[0..len], bytes[0..len]); + w.end += len; + if (bytes.len > dest.len) return error.WriteFailed; + } + const pattern = data[data.len - 1]; + const dest = w.buffer[w.end..]; + switch (pattern.len) { + 0 => return w.end, + 1 => { + assert(splat >= dest.len); + @memset(dest, pattern[0]); + w.end += dest.len; + return error.WriteFailed; + }, + else => { + for (0..splat) |i| { + const remaining = dest[i * pattern.len ..]; + const len = @min(pattern.len, remaining.len); + @memcpy(remaining[0..len], pattern[0..len]); + w.end += len; + if (pattern.len > remaining.len) return error.WriteFailed; + } + unreachable; + }, } } + +/// Provides a `Writer` implementation based on calling `Hasher.update`, sending +/// all data also to an underlying `Writer`. +/// +/// When using this, the underlying writer is best unbuffered because all +/// writes are passed on directly to it. +/// +/// This implementation makes suboptimal buffering decisions due to being +/// generic. A better solution will involve creating a writer for each hash +/// function, where the splat buffer can be tailored to the hash implementation +/// details. +pub fn Hashed(comptime Hasher: type) type { + return struct { + out: *Writer, + hasher: Hasher, + interface: Writer, + + pub fn init(out: *Writer) @This() { + return .{ + .out = out, + .hasher = .{}, + .interface = .{ + .vtable = &.{@This().drain}, + }, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + const this: *@This() = @alignCast(@fieldParentPtr("interface", w)); + if (data.len == 0) { + const buf = w.buffered(); + try this.out.writeAll(buf); + this.hasher.update(buf); + w.end = 0; + return buf.len; + } + const aux_n = try this.out.writeSplatAux(w.buffered(), data, splat); + if (aux_n < w.end) { + this.hasher.update(w.buffer[0..aux_n]); + const remaining = w.buffer[aux_n..w.end]; + @memmove(w.buffer[0..remaining.len], remaining); + w.end = remaining.len; + return 0; + } + this.hasher.update(w.buffered()); + const n = aux_n - w.end; + w.end = 0; + var remaining: usize = n; + const short_data = data[0 .. data.len - @intFromBool(splat == 0)]; + for (short_data) |slice| { + if (remaining < slice.len) { + this.hasher.update(slice[0..remaining]); + return n; + } else { + remaining -= slice.len; + this.hasher.update(slice); + } + } + const remaining_splat = switch (splat) { + 0, 1 => { + assert(remaining == 0); + return n; + }, + else => splat - 1, + }; + const pattern = data[data.len - 1]; + assert(remaining == remaining_splat * pattern.len); + switch (pattern.len) { + 0 => { + assert(remaining == 0); + }, + 1 => { + var buffer: [64]u8 = undefined; + @memset(&buffer, pattern[0]); + while (remaining > 0) { + const update_len = @min(remaining, buffer.len); + this.hasher.update(buffer[0..update_len]); + remaining -= update_len; + } + }, + else => { + while (remaining > 0) { + const update_len = @min(remaining, pattern.len); + this.hasher.update(pattern[0..update_len]); + remaining -= update_len; + } + }, + } + return n; + } + }; +} + +/// Maintains `Writer` state such that it writes to the unused capacity of an +/// array list, filling it up completely before making a call through the +/// vtable, causing a resize. Consequently, the same, optimized, non-generic +/// machine code that uses `std.io.Reader`, such as formatted printing, takes +/// the hot paths when using this API. +/// +/// When using this API, it is not necessary to call `flush`. +pub const Allocating = struct { + allocator: Allocator, + interface: Writer, + + pub fn init(allocator: Allocator) Allocating { + return .{ + .allocator = allocator, + .interface = .{ + .buffer = &.{}, + .vtable = &vtable, + }, + }; + } + + pub fn initCapacity(allocator: Allocator, capacity: usize) error{OutOfMemory}!Allocating { + return .{ + .allocator = allocator, + .interface = .{ + .buffer = try allocator.alloc(u8, capacity), + .vtable = &vtable, + }, + }; + } + + pub fn initOwnedSlice(allocator: Allocator, slice: []u8) Allocating { + return .{ + .allocator = allocator, + .interface = .{ + .buffer = slice, + .vtable = &vtable, + }, + }; + } + + /// Replaces `array_list` with empty, taking ownership of the memory. + pub fn fromArrayList(allocator: Allocator, array_list: *std.ArrayListUnmanaged(u8)) Allocating { + defer array_list.* = .empty; + return .{ + .allocator = allocator, + .interface = .{ + .vtable = &vtable, + .buffer = array_list.allocatedSlice(), + .end = array_list.items.len, + }, + }; + } + + const vtable: VTable = .{ + .drain = Allocating.drain, + .sendFile = Allocating.sendFile, + .flush = noopFlush, + }; + + pub fn deinit(a: *Allocating) void { + a.allocator.free(a.interface.buffer); + a.* = undefined; + } + + /// Returns an array list that takes ownership of the allocated memory. + /// Resets the `Allocating` to an empty state. + pub fn toArrayList(a: *Allocating) std.ArrayListUnmanaged(u8) { + const w = &a.interface; + const result: std.ArrayListUnmanaged(u8) = .{ + .items = w.buffer[0..w.end], + .capacity = w.buffer.len, + }; + w.buffer = &.{}; + w.end = 0; + return result; + } + + pub fn toOwnedSlice(a: *Allocating) error{OutOfMemory}![]u8 { + var list = a.toArrayList(); + return list.toOwnedSlice(a.allocator); + } + + pub fn toOwnedSliceSentinel(a: *Allocating, comptime sentinel: u8) error{OutOfMemory}![:sentinel]u8 { + const gpa = a.allocator; + var list = toArrayList(a); + return list.toOwnedSliceSentinel(gpa, sentinel); + } + + pub fn getWritten(a: *Allocating) []u8 { + return a.interface.buffered(); + } + + pub fn shrinkRetainingCapacity(a: *Allocating, new_len: usize) void { + const shrink_by = a.interface.end - new_len; + a.interface.end = new_len; + a.interface.count -= shrink_by; + } + + pub fn clearRetainingCapacity(a: *Allocating) void { + a.shrinkRetainingCapacity(0); + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + const a: *Allocating = @fieldParentPtr("interface", w); + const gpa = a.allocator; + const pattern = data[data.len - 1]; + const splat_len = pattern.len * splat; + var list = a.toArrayList(); + defer setArrayList(a, list); + const start_len = list.items.len; + for (data) |bytes| { + list.ensureUnusedCapacity(gpa, bytes.len + splat_len) catch return error.WriteFailed; + list.appendSliceAssumeCapacity(bytes); + } + if (splat == 0) { + list.items.len -= pattern.len; + } else switch (pattern.len) { + 0 => {}, + 1 => list.appendNTimesAssumeCapacity(pattern[0], splat - 1), + else => for (0..splat - 1) |_| list.appendSliceAssumeCapacity(pattern), + } + return list.items.len - start_len; + } + + fn sendFile(w: *Writer, file_reader: *File.Reader, limit: std.io.Limit) FileError!usize { + if (File.Handle == void) return error.Unimplemented; + const a: *Allocating = @fieldParentPtr("interface", w); + const gpa = a.allocator; + var list = a.toArrayList(); + defer setArrayList(a, list); + const pos = file_reader.pos; + const additional = if (file_reader.getSize()) |size| size - pos else |_| std.atomic.cache_line; + list.ensureUnusedCapacity(gpa, limit.minInt(additional)) catch return error.WriteFailed; + const dest = limit.slice(list.unusedCapacitySlice()); + const n = file_reader.read(dest) catch |err| switch (err) { + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => 0, + }; + list.items.len += n; + return n; + } + + fn setArrayList(a: *Allocating, list: std.ArrayListUnmanaged(u8)) void { + a.interface.buffer = list.allocatedSlice(); + a.interface.end = list.items.len; + } + + test Allocating { + var a: Allocating = .init(std.testing.allocator); + defer a.deinit(); + const w = &a.interface; + + const x: i32 = 42; + const y: i32 = 1234; + try w.print("x: {}\ny: {}\n", .{ x, y }); + + try testing.expectEqualSlices(u8, "x: 42\ny: 1234\n", a.getWritten()); + } +}; diff --git a/lib/std/io/buffered_atomic_file.zig b/lib/std/io/buffered_atomic_file.zig index 71edabb20a..59f3767ebd 100644 --- a/lib/std/io/buffered_atomic_file.zig +++ b/lib/std/io/buffered_atomic_file.zig @@ -11,7 +11,7 @@ pub const BufferedAtomicFile = struct { pub const buffer_size = 4096; pub const BufferedWriter = std.io.BufferedWriter(buffer_size, File.Writer); - pub const Writer = std.io.Writer(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); + pub const Writer = std.io.GenericWriter(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); /// TODO when https://github.com/ziglang/zig/issues/2761 is solved /// this API will not need an allocator diff --git a/lib/std/io/buffered_reader.zig b/lib/std/io/buffered_reader.zig index bcf54fb882..548dd92f73 100644 --- a/lib/std/io/buffered_reader.zig +++ b/lib/std/io/buffered_reader.zig @@ -12,7 +12,7 @@ pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) ty end: usize = 0, pub const Error = ReaderType.Error; - pub const Reader = io.Reader(*Self, Error, read); + pub const Reader = io.GenericReader(*Self, Error, read); const Self = @This(); @@ -61,7 +61,7 @@ test "OneByte" { const Error = error{NoError}; const Self = @This(); - const Reader = io.Reader(*Self, Error, read); + const Reader = io.GenericReader(*Self, Error, read); fn init(str: []const u8) Self { return Self{ @@ -105,7 +105,7 @@ test "Block" { const Error = error{NoError}; const Self = @This(); - const Reader = io.Reader(*Self, Error, read); + const Reader = io.GenericReader(*Self, Error, read); fn init(block: []const u8, reads_allowed: usize) Self { return Self{ diff --git a/lib/std/io/buffered_writer.zig b/lib/std/io/buffered_writer.zig index 906d6cce49..ef95de0f0c 100644 --- a/lib/std/io/buffered_writer.zig +++ b/lib/std/io/buffered_writer.zig @@ -10,7 +10,7 @@ pub fn BufferedWriter(comptime buffer_size: usize, comptime WriterType: type) ty end: usize = 0, pub const Error = WriterType.Error; - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); const Self = @This(); diff --git a/lib/std/io/c_writer.zig b/lib/std/io/c_writer.zig index 8c25e51029..30d0cabcf5 100644 --- a/lib/std/io/c_writer.zig +++ b/lib/std/io/c_writer.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const io = std.io; const testing = std.testing; -pub const CWriter = io.Writer(*std.c.FILE, std.fs.File.WriteError, cWriterWrite); +pub const CWriter = io.GenericWriter(*std.c.FILE, std.fs.File.WriteError, cWriterWrite); pub fn cWriter(c_file: *std.c.FILE) CWriter { return .{ .context = c_file }; diff --git a/lib/std/io/change_detection_stream.zig b/lib/std/io/change_detection_stream.zig index 5ba2bb3c10..d9da1c4a0e 100644 --- a/lib/std/io/change_detection_stream.zig +++ b/lib/std/io/change_detection_stream.zig @@ -8,7 +8,7 @@ pub fn ChangeDetectionStream(comptime WriterType: type) type { return struct { const Self = @This(); pub const Error = WriterType.Error; - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); anything_changed: bool, underlying_writer: WriterType, diff --git a/lib/std/io/counting_reader.zig b/lib/std/io/counting_reader.zig index 2ff9b8a08f..bc1e1b6ec7 100644 --- a/lib/std/io/counting_reader.zig +++ b/lib/std/io/counting_reader.zig @@ -9,7 +9,7 @@ pub fn CountingReader(comptime ReaderType: anytype) type { bytes_read: u64 = 0, pub const Error = ReaderType.Error; - pub const Reader = io.Reader(*@This(), Error, read); + pub const Reader = io.GenericReader(*@This(), Error, read); pub fn read(self: *@This(), buf: []u8) Error!usize { const amt = try self.child_reader.read(buf); diff --git a/lib/std/io/counting_writer.zig b/lib/std/io/counting_writer.zig index 9043e1a47c..32c3ed930f 100644 --- a/lib/std/io/counting_writer.zig +++ b/lib/std/io/counting_writer.zig @@ -9,7 +9,7 @@ pub fn CountingWriter(comptime WriterType: type) type { child_stream: WriterType, pub const Error = WriterType.Error; - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); const Self = @This(); diff --git a/lib/std/io/find_byte_writer.zig b/lib/std/io/find_byte_writer.zig index cb7efac2d9..fe6836f603 100644 --- a/lib/std/io/find_byte_writer.zig +++ b/lib/std/io/find_byte_writer.zig @@ -8,7 +8,7 @@ pub fn FindByteWriter(comptime UnderlyingWriter: type) type { return struct { const Self = @This(); pub const Error = UnderlyingWriter.Error; - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); underlying_writer: UnderlyingWriter, byte_found: bool, diff --git a/lib/std/io/fixed_buffer_stream.zig b/lib/std/io/fixed_buffer_stream.zig index bfc25eb6ac..67d6f3d286 100644 --- a/lib/std/io/fixed_buffer_stream.zig +++ b/lib/std/io/fixed_buffer_stream.zig @@ -4,8 +4,8 @@ const testing = std.testing; const mem = std.mem; const assert = std.debug.assert; -/// This turns a byte buffer into an `io.Writer`, `io.Reader`, or `io.SeekableStream`. -/// If the supplied byte buffer is const, then `io.Writer` is not available. +/// This turns a byte buffer into an `io.GenericWriter`, `io.GenericReader`, or `io.SeekableStream`. +/// If the supplied byte buffer is const, then `io.GenericWriter` is not available. pub fn FixedBufferStream(comptime Buffer: type) type { return struct { /// `Buffer` is either a `[]u8` or `[]const u8`. @@ -17,8 +17,8 @@ pub fn FixedBufferStream(comptime Buffer: type) type { pub const SeekError = error{}; pub const GetSeekPosError = error{}; - pub const Reader = io.Reader(*Self, ReadError, read); - pub const Writer = io.Writer(*Self, WriteError, write); + pub const Reader = io.GenericReader(*Self, ReadError, read); + pub const Writer = io.GenericWriter(*Self, WriteError, write); pub const SeekableStream = io.SeekableStream( *Self, diff --git a/lib/std/io/limited_reader.zig b/lib/std/io/limited_reader.zig index d7e2503881..b6b555f76d 100644 --- a/lib/std/io/limited_reader.zig +++ b/lib/std/io/limited_reader.zig @@ -9,7 +9,7 @@ pub fn LimitedReader(comptime ReaderType: type) type { bytes_left: u64, pub const Error = ReaderType.Error; - pub const Reader = io.Reader(*Self, Error, read); + pub const Reader = io.GenericReader(*Self, Error, read); const Self = @This(); diff --git a/lib/std/io/multi_writer.zig b/lib/std/io/multi_writer.zig index be109867b7..20e9e782de 100644 --- a/lib/std/io/multi_writer.zig +++ b/lib/std/io/multi_writer.zig @@ -15,7 +15,7 @@ pub fn MultiWriter(comptime Writers: type) type { streams: Writers, pub const Error = ErrSet; - pub const Writer = io.Writer(*Self, Error, write); + pub const Writer = io.GenericWriter(*Self, Error, write); pub fn writer(self: *Self) Writer { return .{ .context = self }; diff --git a/lib/std/io/stream_source.zig b/lib/std/io/stream_source.zig index 6e06af8204..2a3527e479 100644 --- a/lib/std/io/stream_source.zig +++ b/lib/std/io/stream_source.zig @@ -2,9 +2,9 @@ const std = @import("../std.zig"); const builtin = @import("builtin"); const io = std.io; -/// Provides `io.Reader`, `io.Writer`, and `io.SeekableStream` for in-memory buffers as +/// Provides `io.GenericReader`, `io.GenericWriter`, and `io.SeekableStream` for in-memory buffers as /// well as files. -/// For memory sources, if the supplied byte buffer is const, then `io.Writer` is not available. +/// For memory sources, if the supplied byte buffer is const, then `io.GenericWriter` is not available. /// The error set of the stream functions is the error set of the corresponding file functions. pub const StreamSource = union(enum) { // TODO: expose UEFI files to std.os in a way that allows this to be true @@ -26,8 +26,8 @@ pub const StreamSource = union(enum) { pub const SeekError = io.FixedBufferStream([]u8).SeekError || (if (has_file) std.fs.File.SeekError else error{}); pub const GetSeekPosError = io.FixedBufferStream([]u8).GetSeekPosError || (if (has_file) std.fs.File.GetSeekPosError else error{}); - pub const Reader = io.Reader(*StreamSource, ReadError, read); - pub const Writer = io.Writer(*StreamSource, WriteError, write); + pub const Reader = io.GenericReader(*StreamSource, ReadError, read); + pub const Writer = io.GenericWriter(*StreamSource, WriteError, write); pub const SeekableStream = io.SeekableStream( *StreamSource, SeekError, diff --git a/lib/std/json.zig b/lib/std/json.zig index d977c34577..c0fb064c6a 100644 --- a/lib/std/json.zig +++ b/lib/std/json.zig @@ -1,12 +1,12 @@ //! JSON parsing and stringification conforming to RFC 8259. https://datatracker.ietf.org/doc/html/rfc8259 //! //! The low-level `Scanner` API produces `Token`s from an input slice or successive slices of inputs, -//! The `Reader` API connects a `std.io.Reader` to a `Scanner`. +//! The `Reader` API connects a `std.io.GenericReader` to a `Scanner`. //! //! The high-level `parseFromSlice` and `parseFromTokenSource` deserialize a JSON document into a Zig type. //! Parse into a dynamically-typed `Value` to load any JSON value for runtime inspection. //! -//! The low-level `writeStream` emits syntax-conformant JSON tokens to a `std.io.Writer`. +//! The low-level `writeStream` emits syntax-conformant JSON tokens to a `std.io.GenericWriter`. //! The high-level `stringify` serializes a Zig or `Value` type into JSON. const builtin = @import("builtin"); diff --git a/lib/std/json/scanner.zig b/lib/std/json/scanner.zig index 85a058af38..1836d6775b 100644 --- a/lib/std/json/scanner.zig +++ b/lib/std/json/scanner.zig @@ -219,7 +219,7 @@ pub const AllocWhen = enum { alloc_if_needed, alloc_always }; /// This limit can be specified by calling `nextAllocMax()` instead of `nextAlloc()`. pub const default_max_value_len = 4 * 1024 * 1024; -/// Connects a `std.io.Reader` to a `std.json.Scanner`. +/// Connects a `std.io.GenericReader` to a `std.json.Scanner`. /// All `next*()` methods here handle `error.BufferUnderrun` from `std.json.Scanner`, and then read from the reader. pub fn Reader(comptime buffer_size: usize, comptime ReaderType: type) type { return struct { diff --git a/lib/std/json/stringify.zig b/lib/std/json/stringify.zig index db2ba85318..d5add74124 100644 --- a/lib/std/json/stringify.zig +++ b/lib/std/json/stringify.zig @@ -38,7 +38,7 @@ pub const StringifyOptions = struct { emit_nonportable_numbers_as_strings: bool = false, }; -/// Writes the given value to the `std.io.Writer` stream. +/// Writes the given value to the `std.io.GenericWriter` stream. /// See `WriteStream` for how the given value is serialized into JSON. /// The maximum nesting depth of the output JSON document is 256. /// See also `stringifyMaxDepth` and `stringifyArbitraryDepth`. @@ -81,7 +81,7 @@ pub fn stringifyArbitraryDepth( } /// Calls `stringifyArbitraryDepth` and stores the result in dynamically allocated memory -/// instead of taking a `std.io.Writer`. +/// instead of taking a `std.io.GenericWriter`. /// /// Caller owns returned memory. pub fn stringifyAlloc( diff --git a/lib/std/json/stringify_test.zig b/lib/std/json/stringify_test.zig index 52e38d1e30..122616b596 100644 --- a/lib/std/json/stringify_test.zig +++ b/lib/std/json/stringify_test.zig @@ -307,7 +307,7 @@ test "stringify tuple" { fn testStringify(expected: []const u8, value: anytype, options: StringifyOptions) !void { const ValidationWriter = struct { const Self = @This(); - pub const Writer = std.io.Writer(*Self, Error, write); + pub const Writer = std.io.GenericWriter(*Self, Error, write); pub const Error = error{ TooMuchData, DifferentData, diff --git a/lib/std/net.zig b/lib/std/net.zig index 9d821c4399..a2b5fff70a 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -1845,8 +1845,8 @@ pub const Stream = struct { pub const ReadError = posix.ReadError; pub const WriteError = posix.WriteError; - pub const Reader = io.Reader(Stream, ReadError, read); - pub const Writer = io.Writer(Stream, WriteError, write); + pub const Reader = io.GenericReader(Stream, ReadError, read); + pub const Writer = io.GenericWriter(Stream, WriteError, write); pub fn reader(self: Stream) Reader { return .{ .context = self }; diff --git a/lib/std/os/uefi/protocol/file.zig b/lib/std/os/uefi/protocol/file.zig index 2e9c251642..88ac361862 100644 --- a/lib/std/os/uefi/protocol/file.zig +++ b/lib/std/os/uefi/protocol/file.zig @@ -88,8 +88,8 @@ pub const File = extern struct { getPosition, getEndPos, ); - pub const Reader = io.Reader(*File, ReadError, read); - pub const Writer = io.Writer(*File, WriteError, write); + pub const Reader = io.GenericReader(*File, ReadError, read); + pub const Writer = io.GenericWriter(*File, WriteError, write); pub fn seekableStream(self: *File) SeekableStream { return .{ .context = self }; diff --git a/lib/std/tar.zig b/lib/std/tar.zig index f3aba4d381..729a07db0a 100644 --- a/lib/std/tar.zig +++ b/lib/std/tar.zig @@ -348,7 +348,7 @@ pub fn Iterator(comptime ReaderType: type) type { unread_bytes: *u64, parent_reader: ReaderType, - pub const Reader = std.io.Reader(File, ReaderType.Error, File.read); + pub const Reader = std.io.GenericReader(File, ReaderType.Error, File.read); pub fn reader(self: File) Reader { return .{ .context = self }; diff --git a/lib/std/zig/llvm/Builder.zig b/lib/std/zig/llvm/Builder.zig index 0e25af08c6..26c37ba5c1 100644 --- a/lib/std/zig/llvm/Builder.zig +++ b/lib/std/zig/llvm/Builder.zig @@ -9520,7 +9520,7 @@ fn WriterWithErrors(comptime BackingWriter: type, comptime ExtraErrors: type) ty backing_writer: BackingWriter, pub const Error = BackingWriter.Error || ExtraErrors; - pub const Writer = std.io.Writer(*const Self, Error, write); + pub const Writer = std.io.GenericWriter(*const Self, Error, write); const Self = @This(); diff --git a/lib/std/zig/render.zig b/lib/std/zig/render.zig index c4c3379f32..6ec8cde18d 100644 --- a/lib/std/zig/render.zig +++ b/lib/std/zig/render.zig @@ -3245,7 +3245,7 @@ fn AutoIndentingStream(comptime UnderlyingWriter: type) type { return struct { const Self = @This(); pub const WriteError = UnderlyingWriter.Error; - pub const Writer = std.io.Writer(*Self, WriteError, write); + pub const Writer = std.io.GenericWriter(*Self, WriteError, write); pub const IndentType = enum { normal, diff --git a/lib/std/zig/string_literal.zig b/lib/std/zig/string_literal.zig index 972219abbd..58187d0f26 100644 --- a/lib/std/zig/string_literal.zig +++ b/lib/std/zig/string_literal.zig @@ -322,7 +322,7 @@ test parseCharLiteral { ); } -/// Parses `bytes` as a Zig string literal and writes the result to the std.io.Writer type. +/// Parses `bytes` as a Zig string literal and writes the result to the `std.io.GenericWriter` type. /// Asserts `bytes` has '"' at beginning and end. pub fn parseWrite(writer: anytype, bytes: []const u8) error{OutOfMemory}!Result { assert(bytes.len >= 2 and bytes[0] == '"' and bytes[bytes.len - 1] == '"'); diff --git a/lib/std/zip.zig b/lib/std/zip.zig index c149584fd5..3a1ce9bb95 100644 --- a/lib/std/zip.zig +++ b/lib/std/zip.zig @@ -106,7 +106,7 @@ pub const EndRecord = extern struct { /// Find and return the end record for the given seekable zip stream. /// Note that `seekable_stream` must be an instance of `std.io.SeekableStream` and /// its context must also have a `.reader()` method that returns an instance of -/// `std.io.Reader`. +/// `std.io.GenericReader`. pub fn findEndRecord(seekable_stream: anytype, stream_len: u64) !EndRecord { var buf: [@sizeOf(EndRecord) + std.math.maxInt(u16)]u8 = undefined; const record_len_max = @min(stream_len, buf.len); @@ -617,7 +617,7 @@ pub const ExtractOptions = struct { /// Extract the zipped files inside `seekable_stream` to the given `dest` directory. /// Note that `seekable_stream` must be an instance of `std.io.SeekableStream` and /// its context must also have a `.reader()` method that returns an instance of -/// `std.io.Reader`. +/// `std.io.GenericReader`. pub fn extract(dest: std.fs.Dir, seekable_stream: anytype, options: ExtractOptions) !void { const SeekableStream = @TypeOf(seekable_stream); var iter = try Iterator(SeekableStream).init(seekable_stream); |
