diff --git a/CMakeLists.txt b/CMakeLists.txt index 57e7cba0857cc03c550edd18cb2b7d8e7c04bbd8..db580b05fa2802df85b1c8a89614602185c86b68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,6 +387,18 @@ set(ZIG_STAGE2_SOURCES lib/std/Build.zig lib/std/Build/Cache.zig lib/std/Build/Cache/DepTokenizer.zig + lib/std/Io.zig + lib/std/Io/Reader.zig + lib/std/Io/Writer.zig + lib/std/Io/buffered_atomic_file.zig + lib/std/Io/buffered_writer.zig + lib/std/Io/change_detection_stream.zig + lib/std/Io/counting_reader.zig + lib/std/Io/counting_writer.zig + lib/std/Io/find_byte_writer.zig + lib/std/Io/fixed_buffer_stream.zig + lib/std/Io/limited_reader.zig + lib/std/Io/seekable_stream.zig lib/std/Progress.zig lib/std/Random.zig lib/std/Target.zig @@ -449,18 +461,6 @@ set(ZIG_STAGE2_SOURCES lib/std/hash_map.zig lib/std/heap.zig lib/std/heap/arena_allocator.zig - lib/std/io.zig - lib/std/io/Reader.zig - lib/std/io/Writer.zig - lib/std/io/buffered_atomic_file.zig - lib/std/io/buffered_writer.zig - lib/std/io/change_detection_stream.zig - lib/std/io/counting_reader.zig - lib/std/io/counting_writer.zig - lib/std/io/find_byte_writer.zig - lib/std/io/fixed_buffer_stream.zig - lib/std/io/limited_reader.zig - lib/std/io/seekable_stream.zig lib/std/json.zig lib/std/json/stringify.zig lib/std/leb128.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig new file mode 100644 index 0000000000000000000000000000000000000000..00ff0cea98362557d96e76c2a408afc94d15dc6f --- /dev/null +++ b/lib/std/Io.zig @@ -0,0 +1,884 @@ +const std = @import("std.zig"); +const builtin = @import("builtin"); +const root = @import("root"); +const c = std.c; +const is_windows = builtin.os.tag == .windows; +const windows = std.os.windows; +const posix = std.posix; +const math = std.math; +const assert = std.debug.assert; +const fs = std.fs; +const mem = std.mem; +const meta = std.meta; +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); + } + + /// Any value grater than `std.math.maxInt(usize)` is interpreted to mean + /// `.unlimited`. + pub fn limited64(n: u64) Limit { + return @enumFromInt(@min(n, std.math.maxInt(usize))); + } + + 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 minInt64(l: Limit, n: u64) 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"); + +/// Deprecated in favor of `Reader`. +pub fn GenericReader( + comptime Context: type, + comptime ReadError: type, + /// 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. + comptime readFn: fn (context: Context, buffer: []u8) ReadError!usize, +) type { + return struct { + context: Context, + + pub const Error = ReadError; + pub const NoEofError = ReadError || error{ + EndOfStream, + }; + + pub inline fn read(self: Self, buffer: []u8) Error!usize { + return readFn(self.context, buffer); + } + + pub inline fn readAll(self: Self, buffer: []u8) Error!usize { + return @errorCast(self.any().readAll(buffer)); + } + + pub inline fn readAtLeast(self: Self, buffer: []u8, len: usize) Error!usize { + return @errorCast(self.any().readAtLeast(buffer, len)); + } + + pub inline fn readNoEof(self: Self, buf: []u8) NoEofError!void { + return @errorCast(self.any().readNoEof(buf)); + } + + pub inline fn readAllArrayList( + self: Self, + array_list: *std.ArrayList(u8), + max_append_size: usize, + ) (error{StreamTooLong} || Allocator.Error || Error)!void { + return @errorCast(self.any().readAllArrayList(array_list, max_append_size)); + } + + pub inline fn readAllArrayListAligned( + self: Self, + comptime alignment: ?Alignment, + array_list: *std.ArrayListAligned(u8, alignment), + max_append_size: usize, + ) (error{StreamTooLong} || Allocator.Error || Error)!void { + return @errorCast(self.any().readAllArrayListAligned( + alignment, + array_list, + max_append_size, + )); + } + + pub inline fn readAllAlloc( + self: Self, + allocator: Allocator, + max_size: usize, + ) (Error || Allocator.Error || error{StreamTooLong})![]u8 { + return @errorCast(self.any().readAllAlloc(allocator, max_size)); + } + + pub inline fn readUntilDelimiterArrayList( + self: Self, + array_list: *std.ArrayList(u8), + delimiter: u8, + max_size: usize, + ) (NoEofError || Allocator.Error || error{StreamTooLong})!void { + return @errorCast(self.any().readUntilDelimiterArrayList( + array_list, + delimiter, + max_size, + )); + } + + pub inline fn readUntilDelimiterAlloc( + self: Self, + allocator: Allocator, + delimiter: u8, + max_size: usize, + ) (NoEofError || Allocator.Error || error{StreamTooLong})![]u8 { + return @errorCast(self.any().readUntilDelimiterAlloc( + allocator, + delimiter, + max_size, + )); + } + + pub inline fn readUntilDelimiter( + self: Self, + buf: []u8, + delimiter: u8, + ) (NoEofError || error{StreamTooLong})![]u8 { + return @errorCast(self.any().readUntilDelimiter(buf, delimiter)); + } + + pub inline fn readUntilDelimiterOrEofAlloc( + self: Self, + allocator: Allocator, + delimiter: u8, + max_size: usize, + ) (Error || Allocator.Error || error{StreamTooLong})!?[]u8 { + return @errorCast(self.any().readUntilDelimiterOrEofAlloc( + allocator, + delimiter, + max_size, + )); + } + + pub inline fn readUntilDelimiterOrEof( + self: Self, + buf: []u8, + delimiter: u8, + ) (Error || error{StreamTooLong})!?[]u8 { + return @errorCast(self.any().readUntilDelimiterOrEof(buf, delimiter)); + } + + pub inline fn streamUntilDelimiter( + self: Self, + writer: anytype, + delimiter: u8, + optional_max_size: ?usize, + ) (NoEofError || error{StreamTooLong} || @TypeOf(writer).Error)!void { + return @errorCast(self.any().streamUntilDelimiter( + writer, + delimiter, + optional_max_size, + )); + } + + pub inline fn skipUntilDelimiterOrEof(self: Self, delimiter: u8) Error!void { + return @errorCast(self.any().skipUntilDelimiterOrEof(delimiter)); + } + + pub inline fn readByte(self: Self) NoEofError!u8 { + return @errorCast(self.any().readByte()); + } + + pub inline fn readByteSigned(self: Self) NoEofError!i8 { + return @errorCast(self.any().readByteSigned()); + } + + pub inline fn readBytesNoEof( + self: Self, + comptime num_bytes: usize, + ) NoEofError![num_bytes]u8 { + return @errorCast(self.any().readBytesNoEof(num_bytes)); + } + + pub inline fn readIntoBoundedBytes( + self: Self, + comptime num_bytes: usize, + bounded: *std.BoundedArray(u8, num_bytes), + ) Error!void { + return @errorCast(self.any().readIntoBoundedBytes(num_bytes, bounded)); + } + + pub inline fn readBoundedBytes( + self: Self, + comptime num_bytes: usize, + ) Error!std.BoundedArray(u8, num_bytes) { + return @errorCast(self.any().readBoundedBytes(num_bytes)); + } + + pub inline fn readInt(self: Self, comptime T: type, endian: std.builtin.Endian) NoEofError!T { + return @errorCast(self.any().readInt(T, endian)); + } + + pub inline fn readVarInt( + self: Self, + comptime ReturnType: type, + endian: std.builtin.Endian, + size: usize, + ) NoEofError!ReturnType { + return @errorCast(self.any().readVarInt(ReturnType, endian, size)); + } + + pub const SkipBytesOptions = AnyReader.SkipBytesOptions; + + pub inline fn skipBytes( + self: Self, + num_bytes: u64, + comptime options: SkipBytesOptions, + ) NoEofError!void { + return @errorCast(self.any().skipBytes(num_bytes, options)); + } + + pub inline fn isBytes(self: Self, slice: []const u8) NoEofError!bool { + return @errorCast(self.any().isBytes(slice)); + } + + pub inline fn readStruct(self: Self, comptime T: type) NoEofError!T { + return @errorCast(self.any().readStruct(T)); + } + + pub inline fn readStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian) NoEofError!T { + return @errorCast(self.any().readStructEndian(T, endian)); + } + + pub const ReadEnumError = NoEofError || error{ + /// An integer was read, but it did not match any of the tags in the supplied enum. + InvalidValue, + }; + + pub inline fn readEnum( + self: Self, + comptime Enum: type, + endian: std.builtin.Endian, + ) ReadEnumError!Enum { + return @errorCast(self.any().readEnum(Enum, endian)); + } + + pub inline fn any(self: *const Self) AnyReader { + return .{ + .context = @ptrCast(&self.context), + .readFn = typeErasedReadFn, + }; + } + + const Self = @This(); + + fn typeErasedReadFn(context: *const anyopaque, buffer: []u8) anyerror!usize { + const ptr: *const Context = @alignCast(@ptrCast(context)); + return readFn(ptr.*, buffer); + } + }; +} + +/// Deprecated in favor of `Writer`. +pub fn GenericWriter( + comptime Context: type, + comptime WriteError: type, + comptime writeFn: fn (context: Context, bytes: []const u8) WriteError!usize, +) type { + return struct { + context: Context, + + const Self = @This(); + pub const Error = WriteError; + + pub inline fn write(self: Self, bytes: []const u8) Error!usize { + return writeFn(self.context, bytes); + } + + pub inline fn writeAll(self: Self, bytes: []const u8) Error!void { + return @errorCast(self.any().writeAll(bytes)); + } + + pub inline fn print(self: Self, comptime format: []const u8, args: anytype) Error!void { + return @errorCast(self.any().print(format, args)); + } + + pub inline fn writeByte(self: Self, byte: u8) Error!void { + return @errorCast(self.any().writeByte(byte)); + } + + pub inline fn writeByteNTimes(self: Self, byte: u8, n: usize) Error!void { + return @errorCast(self.any().writeByteNTimes(byte, n)); + } + + pub inline fn writeBytesNTimes(self: Self, bytes: []const u8, n: usize) Error!void { + return @errorCast(self.any().writeBytesNTimes(bytes, n)); + } + + pub inline fn writeInt(self: Self, comptime T: type, value: T, endian: std.builtin.Endian) Error!void { + return @errorCast(self.any().writeInt(T, value, endian)); + } + + pub inline fn writeStruct(self: Self, value: anytype) Error!void { + return @errorCast(self.any().writeStruct(value)); + } + + pub inline fn writeStructEndian(self: Self, value: anytype, endian: std.builtin.Endian) Error!void { + return @errorCast(self.any().writeStructEndian(value, endian)); + } + + pub inline fn any(self: *const Self) AnyWriter { + return .{ + .context = @ptrCast(&self.context), + .writeFn = typeErasedWriteFn, + }; + } + + fn typeErasedWriteFn(context: *const anyopaque, bytes: []const u8) anyerror!usize { + const ptr: *const Context = @alignCast(@ptrCast(context)); + return writeFn(ptr.*, bytes); + } + + /// Helper for bridging to the new `Writer` API while upgrading. + pub fn adaptToNewApi(self: *const Self) Adapter { + return .{ + .derp_writer = self.*, + .new_interface = .{ + .buffer = &.{}, + .vtable = &.{ .drain = Adapter.drain }, + }, + }; + } + + pub const Adapter = struct { + derp_writer: Self, + new_interface: Writer, + err: ?Error = null, + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { + _ = splat; + const a: *@This() = @fieldParentPtr("new_interface", w); + return a.derp_writer.write(data[0]) catch |err| { + a.err = err; + return error.WriteFailed; + }; + } + }; + }; +} + +/// 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; + +pub const BufferedWriter = @import("Io/buffered_writer.zig").BufferedWriter; +pub const bufferedWriter = @import("Io/buffered_writer.zig").bufferedWriter; + +pub const BufferedReader = @import("Io/buffered_reader.zig").BufferedReader; +pub const bufferedReader = @import("Io/buffered_reader.zig").bufferedReader; +pub const bufferedReaderSize = @import("Io/buffered_reader.zig").bufferedReaderSize; + +pub const FixedBufferStream = @import("Io/fixed_buffer_stream.zig").FixedBufferStream; +pub const fixedBufferStream = @import("Io/fixed_buffer_stream.zig").fixedBufferStream; + +pub const CWriter = @import("Io/c_writer.zig").CWriter; +pub const cWriter = @import("Io/c_writer.zig").cWriter; + +pub const LimitedReader = @import("Io/limited_reader.zig").LimitedReader; +pub const limitedReader = @import("Io/limited_reader.zig").limitedReader; + +pub const CountingWriter = @import("Io/counting_writer.zig").CountingWriter; +pub const countingWriter = @import("Io/counting_writer.zig").countingWriter; +pub const CountingReader = @import("Io/counting_reader.zig").CountingReader; +pub const countingReader = @import("Io/counting_reader.zig").countingReader; + +pub const MultiWriter = @import("Io/multi_writer.zig").MultiWriter; +pub const multiWriter = @import("Io/multi_writer.zig").multiWriter; + +pub const BitReader = @import("Io/bit_reader.zig").BitReader; +pub const bitReader = @import("Io/bit_reader.zig").bitReader; + +pub const BitWriter = @import("Io/bit_writer.zig").BitWriter; +pub const bitWriter = @import("Io/bit_writer.zig").bitWriter; + +pub const ChangeDetectionStream = @import("Io/change_detection_stream.zig").ChangeDetectionStream; +pub const changeDetectionStream = @import("Io/change_detection_stream.zig").changeDetectionStream; + +pub const FindByteWriter = @import("Io/find_byte_writer.zig").FindByteWriter; +pub const findByteWriter = @import("Io/find_byte_writer.zig").findByteWriter; + +pub const BufferedAtomicFile = @import("Io/buffered_atomic_file.zig").BufferedAtomicFile; + +pub const StreamSource = @import("Io/stream_source.zig").StreamSource; + +pub const tty = @import("Io/tty.zig"); + +/// A Writer that doesn't write to anything. +pub const null_writer: NullWriter = .{ .context = {} }; + +pub const NullWriter = GenericWriter(void, error{}, dummyWrite); +fn dummyWrite(context: void, data: []const u8) error{}!usize { + _ = context; + return data.len; +} + +test null_writer { + null_writer.writeAll("yay" ** 10) catch |err| switch (err) {}; +} + +pub fn poll( + allocator: Allocator, + comptime StreamEnum: type, + files: PollFiles(StreamEnum), +) Poller(StreamEnum) { + const enum_fields = @typeInfo(StreamEnum).@"enum".fields; + var result: Poller(StreamEnum) = undefined; + + if (is_windows) result.windows = .{ + .first_read_done = false, + .overlapped = [1]windows.OVERLAPPED{ + mem.zeroes(windows.OVERLAPPED), + } ** enum_fields.len, + .small_bufs = undefined, + .active = .{ + .count = 0, + .handles_buf = undefined, + .stream_map = undefined, + }, + }; + + inline for (0..enum_fields.len) |i| { + result.fifos[i] = .{ + .allocator = allocator, + .buf = &.{}, + .head = 0, + .count = 0, + }; + if (is_windows) { + result.windows.active.handles_buf[i] = @field(files, enum_fields[i].name).handle; + } else { + result.poll_fds[i] = .{ + .fd = @field(files, enum_fields[i].name).handle, + .events = posix.POLL.IN, + .revents = undefined, + }; + } + } + return result; +} + +pub const PollFifo = std.fifo.LinearFifo(u8, .Dynamic); + +pub fn Poller(comptime StreamEnum: type) type { + return struct { + const enum_fields = @typeInfo(StreamEnum).@"enum".fields; + const PollFd = if (is_windows) void else posix.pollfd; + + fifos: [enum_fields.len]PollFifo, + poll_fds: [enum_fields.len]PollFd, + windows: if (is_windows) struct { + first_read_done: bool, + overlapped: [enum_fields.len]windows.OVERLAPPED, + small_bufs: [enum_fields.len][128]u8, + active: struct { + count: math.IntFittingRange(0, enum_fields.len), + handles_buf: [enum_fields.len]windows.HANDLE, + stream_map: [enum_fields.len]StreamEnum, + + pub fn removeAt(self: *@This(), index: u32) void { + std.debug.assert(index < self.count); + for (index + 1..self.count) |i| { + self.handles_buf[i - 1] = self.handles_buf[i]; + self.stream_map[i - 1] = self.stream_map[i]; + } + self.count -= 1; + } + }, + } else void, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + if (is_windows) { + // cancel any pending IO to prevent clobbering OVERLAPPED value + for (self.windows.active.handles_buf[0..self.windows.active.count]) |h| { + _ = windows.kernel32.CancelIo(h); + } + } + inline for (&self.fifos) |*q| q.deinit(); + self.* = undefined; + } + + pub fn poll(self: *Self) !bool { + if (is_windows) { + return pollWindows(self, null); + } else { + return pollPosix(self, null); + } + } + + pub fn pollTimeout(self: *Self, nanoseconds: u64) !bool { + if (is_windows) { + return pollWindows(self, nanoseconds); + } else { + return pollPosix(self, nanoseconds); + } + } + + pub inline fn fifo(self: *Self, comptime which: StreamEnum) *PollFifo { + return &self.fifos[@intFromEnum(which)]; + } + + fn pollWindows(self: *Self, nanoseconds: ?u64) !bool { + const bump_amt = 512; + + if (!self.windows.first_read_done) { + var already_read_data = false; + for (0..enum_fields.len) |i| { + const handle = self.windows.active.handles_buf[i]; + switch (try windowsAsyncReadToFifoAndQueueSmallRead( + handle, + &self.windows.overlapped[i], + &self.fifos[i], + &self.windows.small_bufs[i], + bump_amt, + )) { + .populated, .empty => |state| { + if (state == .populated) already_read_data = true; + self.windows.active.handles_buf[self.windows.active.count] = handle; + self.windows.active.stream_map[self.windows.active.count] = @as(StreamEnum, @enumFromInt(i)); + self.windows.active.count += 1; + }, + .closed => {}, // don't add to the wait_objects list + .closed_populated => { + // don't add to the wait_objects list, but we did already get data + already_read_data = true; + }, + } + } + self.windows.first_read_done = true; + if (already_read_data) return true; + } + + while (true) { + if (self.windows.active.count == 0) return false; + + const status = windows.kernel32.WaitForMultipleObjects( + self.windows.active.count, + &self.windows.active.handles_buf, + 0, + if (nanoseconds) |ns| + @min(std.math.cast(u32, ns / std.time.ns_per_ms) orelse (windows.INFINITE - 1), windows.INFINITE - 1) + else + windows.INFINITE, + ); + if (status == windows.WAIT_FAILED) + return windows.unexpectedError(windows.GetLastError()); + if (status == windows.WAIT_TIMEOUT) + return true; + + if (status < windows.WAIT_OBJECT_0 or status > windows.WAIT_OBJECT_0 + enum_fields.len - 1) + unreachable; + + const active_idx = status - windows.WAIT_OBJECT_0; + + const stream_idx = @intFromEnum(self.windows.active.stream_map[active_idx]); + const handle = self.windows.active.handles_buf[active_idx]; + + const overlapped = &self.windows.overlapped[stream_idx]; + const stream_fifo = &self.fifos[stream_idx]; + const small_buf = &self.windows.small_bufs[stream_idx]; + + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => { + self.windows.active.removeAt(active_idx); + continue; + }, + .aborted => unreachable, + }; + try stream_fifo.write(small_buf[0..num_bytes_read]); + + switch (try windowsAsyncReadToFifoAndQueueSmallRead( + handle, + overlapped, + stream_fifo, + small_buf, + bump_amt, + )) { + .empty => {}, // irrelevant, we already got data from the small buffer + .populated => {}, + .closed, + .closed_populated, // identical, since we already got data from the small buffer + => self.windows.active.removeAt(active_idx), + } + return true; + } + } + + fn pollPosix(self: *Self, nanoseconds: ?u64) !bool { + // We ask for ensureUnusedCapacity with this much extra space. This + // has more of an effect on small reads because once the reads + // start to get larger the amount of space an ArrayList will + // allocate grows exponentially. + const bump_amt = 512; + + const err_mask = posix.POLL.ERR | posix.POLL.NVAL | posix.POLL.HUP; + + const events_len = try posix.poll(&self.poll_fds, if (nanoseconds) |ns| + std.math.cast(i32, ns / std.time.ns_per_ms) orelse std.math.maxInt(i32) + else + -1); + if (events_len == 0) { + for (self.poll_fds) |poll_fd| { + if (poll_fd.fd != -1) return true; + } else return false; + } + + var keep_polling = false; + inline for (&self.poll_fds, &self.fifos) |*poll_fd, *q| { + // Try reading whatever is available before checking the error + // conditions. + // It's still possible to read after a POLL.HUP is received, + // always check if there's some data waiting to be read first. + if (poll_fd.revents & posix.POLL.IN != 0) { + const buf = try q.writableWithSize(bump_amt); + const amt = posix.read(poll_fd.fd, buf) catch |err| switch (err) { + error.BrokenPipe => 0, // Handle the same as EOF. + else => |e| return e, + }; + q.update(amt); + if (amt == 0) { + // Remove the fd when the EOF condition is met. + poll_fd.fd = -1; + } else { + keep_polling = true; + } + } else if (poll_fd.revents & err_mask != 0) { + // Exclude the fds that signaled an error. + poll_fd.fd = -1; + } else if (poll_fd.fd != -1) { + keep_polling = true; + } + } + return keep_polling; + } + }; +} + +/// The `ReadFile` docuementation states that `lpNumberOfBytesRead` does not have a meaningful +/// result when using overlapped I/O, but also that it cannot be `null` on Windows 7. For +/// compatibility, we point it to this dummy variables, which we never otherwise access. +/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile +var win_dummy_bytes_read: u32 = undefined; + +/// Read as much data as possible from `handle` with `overlapped`, and write it to the FIFO. Before +/// returning, queue a read into `small_buf` so that `WaitForMultipleObjects` returns when more data +/// is available. `handle` must have no pending asynchronous operation. +fn windowsAsyncReadToFifoAndQueueSmallRead( + handle: windows.HANDLE, + overlapped: *windows.OVERLAPPED, + fifo: *PollFifo, + small_buf: *[128]u8, + bump_amt: usize, +) !enum { empty, populated, closed_populated, closed } { + var read_any_data = false; + while (true) { + const fifo_read_pending = while (true) { + const buf = try fifo.writableWithSize(bump_amt); + const buf_len = math.cast(u32, buf.len) orelse math.maxInt(u32); + + if (0 == windows.kernel32.ReadFile( + handle, + buf.ptr, + buf_len, + &win_dummy_bytes_read, + overlapped, + )) switch (windows.GetLastError()) { + .IO_PENDING => break true, + .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, + else => |err| return windows.unexpectedError(err), + }; + + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => unreachable, + }; + + read_any_data = true; + fifo.update(num_bytes_read); + + if (num_bytes_read == buf_len) { + // We filled the buffer, so there's probably more data available. + continue; + } else { + // We didn't fill the buffer, so assume we're out of data. + // There is no pending read. + break false; + } + }; + + if (fifo_read_pending) cancel_read: { + // Cancel the pending read into the FIFO. + _ = windows.kernel32.CancelIo(handle); + + // We have to wait for the handle to be signalled, i.e. for the cancellation to complete. + switch (windows.kernel32.WaitForSingleObject(handle, windows.INFINITE)) { + windows.WAIT_OBJECT_0 => {}, + windows.WAIT_FAILED => return windows.unexpectedError(windows.GetLastError()), + else => unreachable, + } + + // If it completed before we canceled, make sure to tell the FIFO! + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, true)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => break :cancel_read, + }; + read_any_data = true; + fifo.update(num_bytes_read); + } + + // Try to queue the 1-byte read. + if (0 == windows.kernel32.ReadFile( + handle, + small_buf, + small_buf.len, + &win_dummy_bytes_read, + overlapped, + )) switch (windows.GetLastError()) { + .IO_PENDING => { + // 1-byte read pending as intended + return if (read_any_data) .populated else .empty; + }, + .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, + else => |err| return windows.unexpectedError(err), + }; + + // We got data back this time. Write it to the FIFO and run the main loop again. + const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { + .success => |n| n, + .closed => return if (read_any_data) .closed_populated else .closed, + .aborted => unreachable, + }; + try fifo.write(small_buf[0..num_bytes_read]); + read_any_data = true; + } +} + +/// Simple wrapper around `GetOverlappedResult` to determine the result of a `ReadFile` operation. +/// If `!allow_aborted`, then `aborted` is never returned (`OPERATION_ABORTED` is considered unexpected). +/// +/// The `ReadFile` documentation states that the number of bytes read by an overlapped `ReadFile` must be determined using `GetOverlappedResult`, even if the +/// operation immediately returns data: +/// "Use NULL for [lpNumberOfBytesRead] if this is an asynchronous operation to avoid potentially +/// erroneous results." +/// "If `hFile` was opened with `FILE_FLAG_OVERLAPPED`, the following conditions are in effect: [...] +/// The lpNumberOfBytesRead parameter should be set to NULL. Use the GetOverlappedResult function to +/// get the actual number of bytes read." +/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile +fn windowsGetReadResult( + handle: windows.HANDLE, + overlapped: *windows.OVERLAPPED, + allow_aborted: bool, +) !union(enum) { + success: u32, + closed, + aborted, +} { + var num_bytes_read: u32 = undefined; + if (0 == windows.kernel32.GetOverlappedResult( + handle, + overlapped, + &num_bytes_read, + 0, + )) switch (windows.GetLastError()) { + .BROKEN_PIPE => return .closed, + .OPERATION_ABORTED => |err| if (allow_aborted) { + return .aborted; + } else { + return windows.unexpectedError(err); + }, + else => |err| return windows.unexpectedError(err), + }; + return .{ .success = num_bytes_read }; +} + +/// Given an enum, returns a struct with fields of that enum, each field +/// representing an I/O stream for polling. +pub fn PollFiles(comptime StreamEnum: type) type { + const enum_fields = @typeInfo(StreamEnum).@"enum".fields; + var struct_fields: [enum_fields.len]std.builtin.Type.StructField = undefined; + for (&struct_fields, enum_fields) |*struct_field, enum_field| { + struct_field.* = .{ + .name = enum_field.name, + .type = fs.File, + .default_value_ptr = null, + .is_comptime = false, + .alignment = @alignOf(fs.File), + }; + } + return @Type(.{ .@"struct" = .{ + .layout = .auto, + .fields = &struct_fields, + .decls = &.{}, + .is_tuple = false, + } }); +} + +test { + _ = Reader; + _ = Writer; + _ = @import("Io/bit_reader.zig"); + _ = @import("Io/bit_writer.zig"); + _ = @import("Io/buffered_atomic_file.zig"); + _ = @import("Io/buffered_reader.zig"); + _ = @import("Io/buffered_writer.zig"); + _ = @import("Io/c_writer.zig"); + _ = @import("Io/counting_writer.zig"); + _ = @import("Io/counting_reader.zig"); + _ = @import("Io/fixed_buffer_stream.zig"); + _ = @import("Io/seekable_stream.zig"); + _ = @import("Io/stream_source.zig"); + _ = @import("Io/test.zig"); +} diff --git a/lib/std/Io/DeprecatedReader.zig b/lib/std/Io/DeprecatedReader.zig new file mode 100644 index 0000000000000000000000000000000000000000..3f2429c3aead2a048179dde86991f87b2f59cba8 --- /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 0000000000000000000000000000000000000000..391b9853570100e5adecc1e2b0d562372f911aa7 --- /dev/null +++ b/lib/std/Io/DeprecatedWriter.zig @@ -0,0 +1,109 @@ +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; + } +} + +/// Helper for bridging to the new `Writer` API while upgrading. +pub fn adaptToNewApi(self: *const Self) Adapter { + return .{ + .derp_writer = self.*, + .new_interface = .{ + .buffer = &.{}, + .vtable = &.{ .drain = Adapter.drain }, + }, + }; +} + +pub const Adapter = struct { + derp_writer: Self, + new_interface: std.io.Writer, + err: ?Error = null, + + fn drain(w: *std.io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { + _ = splat; + const a: *@This() = @fieldParentPtr("new_interface", w); + return a.derp_writer.write(data[0]) catch |err| { + a.err = err; + return error.WriteFailed; + }; + } +}; diff --git a/lib/std/Io/Reader.zig b/lib/std/Io/Reader.zig new file mode 100644 index 0000000000000000000000000000000000000000..c2f0b25017c290436c8dc194a5962fa606807608 --- /dev/null +++ b/lib/std/Io/Reader.zig @@ -0,0 +1,1731 @@ +const Reader = @This(); + +const builtin = @import("builtin"); +const native_endian = builtin.target.cpu.arch.endian(); + +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`, a value whose minimum size is determined by the + /// stream implementation. + /// + /// 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. + /// + /// Although this function is usually called when `buffer` is empty, it is + /// also called when it needs to be filled more due to the API user + /// requesting contiguous memory. In either case, the existing buffer data + /// should be ignored; new data written to `w`. + /// + /// In addition to, or instead of writing to `w`, the implementation may + /// choose to store data in `buffer`, modifying `seek` and `end` + /// accordingly. Stream implementations are encouraged to take advantage of + /// this if simplifies the logic. + 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. + /// + /// This function is only called when `buffer` is empty. + 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 .init(r, limit, buffer); +} + +/// 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, + }; +} + +pub fn stream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { + const buffer = limit.slice(r.buffer[r.seek..r.end]); + if (buffer.len > 0) { + @branchHint(.likely); + const n = try w.write(buffer); + r.seek += n; + return n; + } + const n = try r.vtable.stream(r, w, limit); + assert(n <= @intFromEnum(limit)); + return n; +} + +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 dw: Writer.Discarding = .init(r.buffer); + const n = r.stream(&dw.writer, limit) catch |err| switch (err) { + error.WriteFailed => unreachable, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => return error.EndOfStream, + }; + assert(n <= @intFromEnum(limit)); + 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) { + 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; + 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); +} + +/// 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; + 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); +} + +/// 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 => { + assert(!wrapper.used); + if (wrapper.writer.buffer.ptr == first.ptr) { + remaining -= wrapper.writer.end; + } else { + assert(wrapper.writer.end <= r.buffer.len); + r.end = wrapper.writer.end; + } + break; + }, + else => |e| return e, + }; + if (!wrapper.used) { + if (wrapper.writer.buffer.ptr == first.ptr) { + remaining -= n; + } else { + assert(n <= r.buffer.len); + 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; + } + assert(n <= r.buffer.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 }; +} + +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; + } + } +} + +/// 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 tossBuffered(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 readSliceAll(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: +/// * `readSliceAll` +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.used) { + assert(r.seek == 0); + r.seek = remaining.len; + r.end = wrapper.writer.end; + @memcpy(remaining, r.buffer[0..remaining.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: +/// * `readSliceAll` +/// * `readSliceEndianAlloc` +pub inline fn readSliceEndian( + r: *Reader, + comptime Elem: type, + buffer: []Elem, + endian: std.builtin.Endian, +) Error!void { + try readSliceAll(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 readSliceAll(r, @ptrCast(dest)); + if (native_endian != endian) for (dest) |*elem| std.mem.byteSwapAllFields(Elem, elem); + return dest; +} + +/// Shortcut for calling `readSliceAll` with a buffer provided by `allocator`. +pub fn readAlloc(r: *Reader, allocator: Allocator, len: usize) ReadAllocError![]u8 { + const dest = try allocator.alloc(u8, len); + errdefer allocator.free(dest); + try readSliceAll(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; +} + +/// Returns a slice of the next bytes of buffered data from the stream until +/// `sentinel` is found, without advancing the seek position. +/// +/// Returned slice has a sentinel; end of stream does not count as a delimiter. +/// +/// Invalidates previously returned values from `peek`. +/// +/// See also: +/// * `takeSentinel` +/// * `peekDelimiterExclusive` +/// * `peekDelimiterInclusive` +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 (r.vtable.stream == &endingStream) { + // Protect the `@constCast` of `fixed`. + return error.EndOfStream; + } + r.rebase(); + while (r.buffer.len - r.end != 0) { + const end_cap = r.buffer[r.end..]; + var writer: Writer = .fixed(end_cap); + const n = r.vtable.stream(r, &writer, .limited(end_cap.len)) catch |err| switch (err) { + error.WriteFailed => unreachable, + else => |e| return e, + }; + r.end += n; + if (std.mem.indexOfScalarPos(u8, end_cap[0..n], 0, delimiter)) |end| { + return r.buffer[0 .. r.end - n + 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 => { + const remaining = r.buffer[r.seek..r.end]; + if (remaining.len == 0) return error.EndOfStream; + r.toss(remaining.len); + return remaining; + }, + else => |e| return e, + }; + 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 => { + const remaining = r.buffer[r.seek..r.end]; + if (remaining.len == 0) return error.EndOfStream; + r.toss(remaining.len); + return remaining; + }, + else => |e| return e, + }; + return result[0 .. result.len - 1]; +} + +/// Appends to `w` contents by reading from the stream until `delimiter` is +/// found. Does not write the delimiter itself. +/// +/// Returns number of bytes streamed, which may be zero, or error.EndOfStream +/// if the delimiter was not found. +/// +/// See also: +/// * `streamDelimiterEnding` +/// * `streamDelimiterLimit` +pub fn streamDelimiter(r: *Reader, w: *Writer, delimiter: u8) StreamError!usize { + const n = streamDelimiterLimit(r, w, delimiter, .unlimited) catch |err| switch (err) { + error.StreamTooLong => unreachable, // unlimited is passed + else => |e| return e, + }; + if (r.seek == r.end) return error.EndOfStream; + return n; +} + +/// Appends to `w` contents by reading from the stream until `delimiter` is found. +/// Does not write the delimiter itself. +/// +/// Returns number of bytes streamed, which may be zero. End of stream can be +/// detected by checking if the next byte in the stream is the delimiter. +/// +/// See also: +/// * `streamDelimiter` +/// * `streamDelimiterLimit` +pub fn streamDelimiterEnding( + r: *Reader, + w: *Writer, + delimiter: u8, +) StreamRemainingError!usize { + return streamDelimiterLimit(r, w, delimiter, .unlimited) catch |err| switch (err) { + error.StreamTooLong => unreachable, // unlimited is passed + else => |e| return e, + }; +} + +pub const StreamDelimiterLimitError = error{ + ReadFailed, + WriteFailed, + /// 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, which may be zero. End of stream can be +/// detected by checking if the next byte in the stream is the delimiter. +pub fn streamDelimiterLimit( + r: *Reader, + w: *Writer, + delimiter: u8, + limit: Limit, +) StreamDelimiterLimitError!usize { + var remaining = @intFromEnum(limit); + while (remaining != 0) { + const available = Limit.limited(remaining).slice(r.peekGreedy(1) catch |err| switch (err) { + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => return @intFromEnum(limit) - remaining, + }); + if (std.mem.indexOfScalar(u8, available, delimiter)) |delimiter_index| { + try w.writeAll(available[0..delimiter_index]); + r.toss(delimiter_index); + remaining -= delimiter_index; + return @intFromEnum(limit) - remaining; + } + try w.writeAll(available); + r.toss(available.len); + remaining -= available.len; + } + return error.StreamTooLong; +} + +/// Reads from the stream until specified byte is found, discarding all data, +/// including the delimiter. +/// +/// Returns number of bytes discarded, or `error.EndOfStream` if the delimiter +/// is not found. +/// +/// See also: +/// * `discardDelimiterExclusive` +/// * `discardDelimiterLimit` +pub fn discardDelimiterInclusive(r: *Reader, delimiter: u8) Error!usize { + const n = discardDelimiterLimit(r, delimiter, .unlimited) catch |err| switch (err) { + error.StreamTooLong => unreachable, // unlimited is passed + else => |e| return e, + }; + if (r.seek == r.end) return error.EndOfStream; + assert(r.buffer[r.seek] == delimiter); + toss(r, 1); + return n + 1; +} + +/// Reads from the stream until specified byte is found, discarding all data, +/// excluding the delimiter. +/// +/// Returns the number of bytes discarded. +/// +/// Succeeds if stream ends before delimiter found. End of stream can be +/// detected by checking if the delimiter is buffered. +/// +/// See also: +/// * `discardDelimiterInclusive` +/// * `discardDelimiterLimit` +pub fn discardDelimiterExclusive(r: *Reader, delimiter: u8) ShortError!usize { + return discardDelimiterLimit(r, delimiter, .unlimited) catch |err| switch (err) { + error.StreamTooLong => unreachable, // unlimited is passed + else => |e| return e, + }; +} + +pub const DiscardDelimiterLimitError = error{ + ReadFailed, + /// The delimiter was not found within the limit. + StreamTooLong, +}; + +/// Reads from the stream until specified byte is found, discarding all data, +/// excluding the delimiter. +/// +/// Returns the number of bytes discarded. +/// +/// Succeeds if stream ends before delimiter found. End of stream can be +/// detected by checking if the delimiter is buffered. +pub fn discardDelimiterLimit(r: *Reader, delimiter: u8, limit: Limit) DiscardDelimiterLimitError!usize { + var remaining = @intFromEnum(limit); + while (remaining != 0) { + const available = Limit.limited(remaining).slice(r.peekGreedy(1) catch |err| switch (err) { + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => return @intFromEnum(limit) - remaining, + }); + if (std.mem.indexOfScalar(u8, available, delimiter)) |delimiter_index| { + r.toss(delimiter_index); + remaining -= delimiter_index; + return @intFromEnum(limit) - remaining; + } + r.toss(available.len); + remaining -= available.len; + } + return error.StreamTooLong; +} + +/// 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; + } + if (r.seek + n <= r.buffer.len) while (true) { + const end_cap = r.buffer[r.end..]; + var writer: Writer = .fixed(end_cap); + r.end += r.vtable.stream(r, &writer, .limited(end_cap.len)) catch |err| switch (err) { + error.WriteFailed => unreachable, + else => |e| return e, + }; + if (r.seek + n <= r.end) return; + }; + if (r.vtable.stream == &endingStream) { + // Protect the `@constCast` of `fixed`. + return error.EndOfStream; + } + 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, + }; + } +} + +/// 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(.likely); + return buffer[seek]; + } + try fill(r, 1); + return r.buffer[r.seek]; +} + +/// 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; +} + +/// Same as `takeByte` except the returned byte is signed. +pub fn takeByteSigned(r: *Reader) Error!i8 { + return @bitCast(try r.takeByte()); +} + +/// 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); +} + +/// 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); +} + +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// Advances the seek position. +/// +/// See also: +/// * `peekStruct` +/// * `takeStructEndian` +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))); +} + +/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. +/// +/// Does not advance the seek position. +/// +/// See also: +/// * `takeStruct` +/// * `peekStructEndian` +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); + return @ptrCast(try r.peekArray(@sizeOf(T))); +} + +/// 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. +/// +/// See also: +/// * `takeStruct` +/// * `peekStructEndian` +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. +/// +/// See also: +/// * `takeStructEndian` +/// * `peekStruct` +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; +} + +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, + }; +} + +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; +} + +/// 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; +} + +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 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); + } +} + +/// 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 { + var r: Reader = .fixed("abc"); + try testing.expectEqualStrings("ab", try r.peek(2)); + try testing.expectEqualStrings("a", try r.peek(1)); +} + +test peekGreedy { + var r: Reader = .fixed("abc"); + try testing.expectEqualStrings("abc", try r.peekGreedy(1)); +} + +test toss { + var r: Reader = .fixed("abc"); + r.toss(1); + try testing.expectEqualStrings("bc", r.buffered()); +} + +test take { + var r: Reader = .fixed("abc"); + try testing.expectEqualStrings("ab", try r.take(2)); + try testing.expectEqualStrings("c", try r.take(1)); +} + +test takeArray { + var r: Reader = .fixed("abc"); + try testing.expectEqualStrings("ab", try r.takeArray(2)); + try testing.expectEqualStrings("c", try r.takeArray(1)); +} + +test peekArray { + var r: Reader = .fixed("abc"); + try testing.expectEqualStrings("ab", try r.peekArray(2)); + try testing.expectEqualStrings("a", try r.peekArray(1)); +} + +test discardAll { + var r: Reader = .fixed("foobar"); + try r.discardAll(3); + try testing.expectEqualStrings("bar", try r.take(3)); + try r.discardAll(0); + try testing.expectError(error.EndOfStream, r.discardAll(1)); +} + +test discardRemaining { + var r: Reader = .fixed("foobar"); + r.toss(1); + try testing.expectEqual(5, try r.discardRemaining()); + try testing.expectEqual(0, try r.discardRemaining()); +} + +test stream { + var out_buffer: [10]u8 = undefined; + var r: Reader = .fixed("foobar"); + var w: Writer = .fixed(&out_buffer); + // Short streams are possible with this function but not with fixed. + try testing.expectEqual(2, try r.stream(&w, .limited(2))); + try testing.expectEqualStrings("fo", w.buffered()); + try testing.expectEqual(4, try r.stream(&w, .unlimited)); + try testing.expectEqualStrings("foobar", w.buffered()); +} + +test takeSentinel { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab", try r.takeSentinel('\n')); + try testing.expectError(error.EndOfStream, r.takeSentinel('\n')); + try testing.expectEqualStrings("c", try r.peek(1)); +} + +test peekSentinel { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab", try r.peekSentinel('\n')); + try testing.expectEqualStrings("ab", try r.peekSentinel('\n')); +} + +test takeDelimiterInclusive { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab\n", try r.takeDelimiterInclusive('\n')); + try testing.expectError(error.EndOfStream, r.takeDelimiterInclusive('\n')); +} + +test peekDelimiterInclusive { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n')); + try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n')); + r.toss(3); + try testing.expectError(error.EndOfStream, r.peekDelimiterInclusive('\n')); +} + +test takeDelimiterExclusive { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab", try r.takeDelimiterExclusive('\n')); + try testing.expectEqualStrings("c", try r.takeDelimiterExclusive('\n')); + try testing.expectError(error.EndOfStream, r.takeDelimiterExclusive('\n')); +} + +test peekDelimiterExclusive { + var r: Reader = .fixed("ab\nc"); + try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n')); + try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n')); + r.toss(3); + try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n')); +} + +test streamDelimiter { + var out_buffer: [10]u8 = undefined; + var r: Reader = .fixed("foo\nbars"); + var w: Writer = .fixed(&out_buffer); + try testing.expectEqual(3, try r.streamDelimiter(&w, '\n')); + try testing.expectEqualStrings("foo", w.buffered()); + try testing.expectEqual(0, try r.streamDelimiter(&w, '\n')); + r.toss(1); + try testing.expectError(error.EndOfStream, r.streamDelimiter(&w, '\n')); +} + +test streamDelimiterEnding { + var out_buffer: [10]u8 = undefined; + var r: Reader = .fixed("foo\nbars"); + var w: Writer = .fixed(&out_buffer); + try testing.expectEqual(3, try r.streamDelimiterEnding(&w, '\n')); + try testing.expectEqualStrings("foo", w.buffered()); + r.toss(1); + try testing.expectEqual(4, try r.streamDelimiterEnding(&w, '\n')); + try testing.expectEqualStrings("foobars", w.buffered()); + try testing.expectEqual(0, try r.streamDelimiterEnding(&w, '\n')); + try testing.expectEqual(0, try r.streamDelimiterEnding(&w, '\n')); +} + +test streamDelimiterLimit { + var out_buffer: [10]u8 = undefined; + var r: Reader = .fixed("foo\nbars"); + var w: Writer = .fixed(&out_buffer); + try testing.expectError(error.StreamTooLong, r.streamDelimiterLimit(&w, '\n', .limited(2))); + try testing.expectEqual(1, try r.streamDelimiterLimit(&w, '\n', .limited(3))); + try testing.expectEqualStrings("\n", try r.take(1)); + try testing.expectEqual(4, try r.streamDelimiterLimit(&w, '\n', .unlimited)); + try testing.expectEqualStrings("foobars", w.buffered()); +} + +test discardDelimiterExclusive { + var r: Reader = .fixed("foob\nar"); + try testing.expectEqual(4, try r.discardDelimiterExclusive('\n')); + try testing.expectEqualStrings("\n", try r.take(1)); + try testing.expectEqual(2, try r.discardDelimiterExclusive('\n')); + try testing.expectEqual(0, try r.discardDelimiterExclusive('\n')); +} + +test discardDelimiterInclusive { + var r: Reader = .fixed("foob\nar"); + try testing.expectEqual(5, try r.discardDelimiterInclusive('\n')); + try testing.expectError(error.EndOfStream, r.discardDelimiterInclusive('\n')); +} + +test discardDelimiterLimit { + var r: Reader = .fixed("foob\nar"); + try testing.expectError(error.StreamTooLong, r.discardDelimiterLimit('\n', .limited(4))); + try testing.expectEqual(0, try r.discardDelimiterLimit('\n', .limited(2))); + try testing.expectEqualStrings("\n", try r.take(1)); + try testing.expectEqual(2, try r.discardDelimiterLimit('\n', .unlimited)); + try testing.expectEqual(0, try r.discardDelimiterLimit('\n', .unlimited)); +} + +test fill { + var r: Reader = .fixed("abc"); + try r.fill(1); + try r.fill(3); +} + +test takeByte { + var r: Reader = .fixed("ab"); + try testing.expectEqual('a', try r.takeByte()); + try testing.expectEqual('b', try r.takeByte()); + try testing.expectError(error.EndOfStream, r.takeByte()); +} + +test takeByteSigned { + var r: Reader = .fixed(&.{ 255, 5 }); + try testing.expectEqual(-1, try r.takeByteSigned()); + try testing.expectEqual(5, try r.takeByteSigned()); + try testing.expectError(error.EndOfStream, r.takeByteSigned()); +} + +test takeInt { + var r: Reader = .fixed(&.{ 0x12, 0x34, 0x56 }); + try testing.expectEqual(0x1234, try r.takeInt(u16, .big)); + try testing.expectError(error.EndOfStream, r.takeInt(u16, .little)); +} + +test takeVarInt { + var r: Reader = .fixed(&.{ 0x12, 0x34, 0x56 }); + try testing.expectEqual(0x123456, try r.takeVarInt(u64, .big, 3)); + try testing.expectError(error.EndOfStream, r.takeVarInt(u16, .little, 1)); +} + +test takeStruct { + var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); + const S = extern struct { a: u8, b: u16 }; + switch (native_endian) { + .little => try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.takeStruct(S)).*), + .big => try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.takeStruct(S)).*), + } + try testing.expectError(error.EndOfStream, r.takeStruct(S)); +} + +test peekStruct { + var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); + const S = extern struct { a: u8, b: u16 }; + switch (native_endian) { + .little => { + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.peekStruct(S)).*); + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.peekStruct(S)).*); + }, + .big => { + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.peekStruct(S)).*); + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.peekStruct(S)).*); + }, + } +} + +test takeStructEndian { + var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); + const S = extern struct { a: u8, b: u16 }; + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), try r.takeStructEndian(S, .big)); + try testing.expectError(error.EndOfStream, r.takeStructEndian(S, .little)); +} + +test peekStructEndian { + var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); + const S = extern struct { a: u8, b: u16 }; + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), try r.peekStructEndian(S, .big)); + try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), try r.peekStructEndian(S, .little)); +} + +test takeEnum { + var r: Reader = .fixed(&.{ 2, 0, 1 }); + const E1 = enum(u8) { a, b, c }; + const E2 = enum(u16) { _ }; + try testing.expectEqual(E1.c, try r.takeEnum(E1, .little)); + try testing.expectEqual(@as(E2, @enumFromInt(0x0001)), try r.takeEnum(E2, .big)); +} + +test takeLeb128 { + var r: Reader = .fixed("\xc7\x9f\x7f\x80"); + try testing.expectEqual(-12345, try r.takeLeb128(i64)); + try testing.expectEqual(0x80, try r.peekByte()); + try testing.expectError(error.EndOfStream, r.takeLeb128(i64)); +} + +test readSliceShort { + var r: Reader = .fixed("HelloFren"); + var buf: [5]u8 = undefined; + try testing.expectEqual(5, try r.readSliceShort(&buf)); + try testing.expectEqualStrings("Hello", buf[0..5]); + try testing.expectEqual(4, try r.readSliceShort(&buf)); + try testing.expectEqualStrings("Fren", buf[0..4]); + try testing.expectEqual(0, try r.readSliceShort(&buf)); +} + +test readVec { + var r: Reader = .fixed(std.ascii.letters); + var flat_buffer: [52]u8 = undefined; + var bufs: [2][]u8 = .{ + flat_buffer[0..26], + flat_buffer[26..], + }; + // Short reads are possible with this function but not with fixed. + try testing.expectEqual(26 * 2, try r.readVec(&bufs)); + try testing.expectEqualStrings(std.ascii.letters[0..26], bufs[0]); + try testing.expectEqualStrings(std.ascii.letters[26..], bufs[1]); +} + +test readVecLimit { + var r: Reader = .fixed(std.ascii.letters); + var flat_buffer: [52]u8 = undefined; + var bufs: [2][]u8 = .{ + flat_buffer[0..26], + flat_buffer[26..], + }; + // Short reads are possible with this function but not with fixed. + try testing.expectEqual(50, try r.readVecLimit(&bufs, .limited(50))); + try testing.expectEqualStrings(std.ascii.letters[0..26], bufs[0]); + try testing.expectEqualStrings(std.ascii.letters[26..50], bufs[1][0..24]); +} + +test "expected error.EndOfStream" { + // Unit test inspired by https://github.com/ziglang/zig/issues/17733 + var buffer: [3]u8 = undefined; + var r: std.io.Reader = .fixed(&buffer); + r.end = 0; // capacity 3, but empty + try std.testing.expectError(error.EndOfStream, r.takeEnum(enum(u8) { a, b }, .little)); + try std.testing.expectError(error.EndOfStream, r.take(3)); +} + +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, + i: usize, + reader: Reader, + + fn stream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { + assert(@intFromEnum(limit) >= 1); + const self: *@This() = @fieldParentPtr("reader", r); + if (self.str.len - self.i == 0) return error.EndOfStream; + try w.writeByte(self.str[self.i]); + self.i += 1; + return 1; + } + }; + const str = "This is a test"; + var one_byte_stream: OneByteReader = .{ + .str = str, + .i = 0, + .reader = .{ + .buffer = &.{}, + .vtable = &.{ .stream = OneByteReader.stream }, + .seek = 0, + .end = 0, + }, + }; + const res = try one_byte_stream.reader.allocRemaining(std.testing.allocator, .unlimited); + defer std.testing.allocator.free(res); + try std.testing.expectEqualStrings(str, res); +} + +test "takeDelimiterInclusive when it rebases" { + const written_line = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"; + var buffer: [128]u8 = undefined; + var tr: std.testing.Reader = .init(&buffer, &.{ + .{ .buffer = written_line }, + .{ .buffer = written_line }, + .{ .buffer = written_line }, + .{ .buffer = written_line }, + .{ .buffer = written_line }, + .{ .buffer = written_line }, + }); + const r = &tr.interface; + for (0..6) |_| { + try std.testing.expectEqualStrings(written_line, try r.takeDelimiterInclusive('\n')); + } +} + +/// 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 0000000000000000000000000000000000000000..9476b97804ec87cf6c469c1da8ba8835be1b708a --- /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/Reader/test.zig b/lib/std/Io/Reader/test.zig new file mode 100644 index 0000000000000000000000000000000000000000..30f0e1269c321988a9c8016597883634b8142783 --- /dev/null +++ b/lib/std/Io/Reader/test.zig @@ -0,0 +1,372 @@ +const builtin = @import("builtin"); +const std = @import("../../std.zig"); +const testing = std.testing; + +test "Reader" { + var buf = "a\x02".*; + var fis = std.io.fixedBufferStream(&buf); + const reader = fis.reader(); + try testing.expect((try reader.readByte()) == 'a'); + try testing.expect((try reader.readEnum(enum(u8) { + a = 0, + b = 99, + c = 2, + d = 3, + }, builtin.cpu.arch.endian())) == .c); + try testing.expectError(error.EndOfStream, reader.readByte()); +} + +test "isBytes" { + var fis = std.io.fixedBufferStream("foobar"); + const reader = fis.reader(); + try testing.expectEqual(true, try reader.isBytes("foo")); + try testing.expectEqual(false, try reader.isBytes("qux")); +} + +test "skipBytes" { + var fis = std.io.fixedBufferStream("foobar"); + const reader = fis.reader(); + try reader.skipBytes(3, .{}); + try testing.expect(try reader.isBytes("bar")); + try reader.skipBytes(0, .{}); + try testing.expectError(error.EndOfStream, reader.skipBytes(1, .{})); +} + +test "readUntilDelimiterArrayList returns ArrayLists with bytes read until the delimiter, then EndOfStream" { + const a = std.testing.allocator; + var list = std.ArrayList(u8).init(a); + defer list.deinit(); + + var fis = std.io.fixedBufferStream("0000\n1234\n"); + const reader = fis.reader(); + + try reader.readUntilDelimiterArrayList(&list, '\n', 5); + try std.testing.expectEqualStrings("0000", list.items); + try reader.readUntilDelimiterArrayList(&list, '\n', 5); + try std.testing.expectEqualStrings("1234", list.items); + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterArrayList(&list, '\n', 5)); +} + +test "readUntilDelimiterArrayList returns an empty ArrayList" { + const a = std.testing.allocator; + var list = std.ArrayList(u8).init(a); + defer list.deinit(); + + var fis = std.io.fixedBufferStream("\n"); + const reader = fis.reader(); + + try reader.readUntilDelimiterArrayList(&list, '\n', 5); + try std.testing.expectEqualStrings("", list.items); +} + +test "readUntilDelimiterArrayList returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { + const a = std.testing.allocator; + var list = std.ArrayList(u8).init(a); + defer list.deinit(); + + var fis = std.io.fixedBufferStream("1234567\n"); + const reader = fis.reader(); + + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterArrayList(&list, '\n', 5)); + try std.testing.expectEqualStrings("12345", list.items); + try reader.readUntilDelimiterArrayList(&list, '\n', 5); + try std.testing.expectEqualStrings("67", list.items); +} + +test "readUntilDelimiterArrayList returns EndOfStream" { + const a = std.testing.allocator; + var list = std.ArrayList(u8).init(a); + defer list.deinit(); + + var fis = std.io.fixedBufferStream("1234"); + const reader = fis.reader(); + + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterArrayList(&list, '\n', 5)); + try std.testing.expectEqualStrings("1234", list.items); +} + +test "readUntilDelimiterAlloc returns ArrayLists with bytes read until the delimiter, then EndOfStream" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("0000\n1234\n"); + const reader = fis.reader(); + + { + const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); + defer a.free(result); + try std.testing.expectEqualStrings("0000", result); + } + + { + const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); + defer a.free(result); + try std.testing.expectEqualStrings("1234", result); + } + + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterAlloc(a, '\n', 5)); +} + +test "readUntilDelimiterAlloc returns an empty ArrayList" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("\n"); + const reader = fis.reader(); + + { + const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); + defer a.free(result); + try std.testing.expectEqualStrings("", result); + } +} + +test "readUntilDelimiterAlloc returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("1234567\n"); + const reader = fis.reader(); + + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterAlloc(a, '\n', 5)); + + const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); + defer a.free(result); + try std.testing.expectEqualStrings("67", result); +} + +test "readUntilDelimiterAlloc returns EndOfStream" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("1234"); + const reader = fis.reader(); + + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterAlloc(a, '\n', 5)); +} + +test "readUntilDelimiter returns bytes read until the delimiter" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("0000\n1234\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("0000", try reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectEqualStrings("1234", try reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter returns an empty string" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("", try reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter returns StreamTooLong, then an empty string" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("12345\n"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectEqualStrings("", try reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter returns StreamTooLong, then bytes read until the delimiter" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234567\n"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectEqualStrings("67", try reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter returns EndOfStream" { + { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream(""); + const reader = fis.reader(); + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); + } + { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234"); + const reader = fis.reader(); + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); + } +} + +test "readUntilDelimiter returns bytes read until delimiter, then EndOfStream" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("1234", try reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter returns StreamTooLong, then EndOfStream" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("12345"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); +} + +test "readUntilDelimiter writes all bytes read to the output buffer" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("0000\n12345"); + const reader = fis.reader(); + _ = try reader.readUntilDelimiter(&buf, '\n'); + try std.testing.expectEqualStrings("0000\n", &buf); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); + try std.testing.expectEqualStrings("12345", &buf); +} + +test "readUntilDelimiterOrEofAlloc returns ArrayLists with bytes read until the delimiter, then EndOfStream" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("0000\n1234\n"); + const reader = fis.reader(); + + { + const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; + defer a.free(result); + try std.testing.expectEqualStrings("0000", result); + } + + { + const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; + defer a.free(result); + try std.testing.expectEqualStrings("1234", result); + } + + try std.testing.expect((try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)) == null); +} + +test "readUntilDelimiterOrEofAlloc returns an empty ArrayList" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("\n"); + const reader = fis.reader(); + + { + const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; + defer a.free(result); + try std.testing.expectEqualStrings("", result); + } +} + +test "readUntilDelimiterOrEofAlloc returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { + const a = std.testing.allocator; + + var fis = std.io.fixedBufferStream("1234567\n"); + const reader = fis.reader(); + + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)); + + const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; + defer a.free(result); + try std.testing.expectEqualStrings("67", result); +} + +test "readUntilDelimiterOrEof returns bytes read until the delimiter" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("0000\n1234\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("0000", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); + try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof returns an empty string" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof returns StreamTooLong, then an empty string" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("12345\n"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); + try std.testing.expectEqualStrings("", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof returns StreamTooLong, then bytes read until the delimiter" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234567\n"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); + try std.testing.expectEqualStrings("67", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof returns null" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream(""); + const reader = fis.reader(); + try std.testing.expect((try reader.readUntilDelimiterOrEof(&buf, '\n')) == null); +} + +test "readUntilDelimiterOrEof returns bytes read until delimiter, then null" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234\n"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); + try std.testing.expect((try reader.readUntilDelimiterOrEof(&buf, '\n')) == null); +} + +test "readUntilDelimiterOrEof returns bytes read until end-of-stream" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234"); + const reader = fis.reader(); + try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof returns StreamTooLong, then bytes read until end-of-stream" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("1234567"); + const reader = fis.reader(); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); + try std.testing.expectEqualStrings("67", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); +} + +test "readUntilDelimiterOrEof writes all bytes read to the output buffer" { + var buf: [5]u8 = undefined; + var fis = std.io.fixedBufferStream("0000\n12345"); + const reader = fis.reader(); + _ = try reader.readUntilDelimiterOrEof(&buf, '\n'); + try std.testing.expectEqualStrings("0000\n", &buf); + try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); + try std.testing.expectEqualStrings("12345", &buf); +} + +test "streamUntilDelimiter writes all bytes without delimiter to the output" { + const input_string = "some_string_with_delimiter!"; + var input_fbs = std.io.fixedBufferStream(input_string); + const reader = input_fbs.reader(); + + var output: [input_string.len]u8 = undefined; + var output_fbs = std.io.fixedBufferStream(&output); + const writer = output_fbs.writer(); + + try reader.streamUntilDelimiter(writer, '!', input_fbs.buffer.len); + try std.testing.expectEqualStrings("some_string_with_delimiter", output_fbs.getWritten()); + try std.testing.expectError(error.EndOfStream, reader.streamUntilDelimiter(writer, '!', input_fbs.buffer.len)); + + input_fbs.reset(); + output_fbs.reset(); + + try std.testing.expectError(error.StreamTooLong, reader.streamUntilDelimiter(writer, '!', 5)); +} + +test "readBoundedBytes correctly reads into a new bounded array" { + const test_string = "abcdefg"; + var fis = std.io.fixedBufferStream(test_string); + const reader = fis.reader(); + + var array = try reader.readBoundedBytes(10000); + try testing.expectEqualStrings(array.slice(), test_string); +} + +test "readIntoBoundedBytes correctly reads into a provided bounded array" { + const test_string = "abcdefg"; + var fis = std.io.fixedBufferStream(test_string); + const reader = fis.reader(); + + var bounded_array = std.BoundedArray(u8, 10000){}; + + // compile time error if the size is not the same at the provided `bounded.capacity()` + try reader.readIntoBoundedBytes(10000, &bounded_array); + try testing.expectEqualStrings(bounded_array.slice(), test_string); +} diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..d79959dcb15647da04df897a112b2c758cd2cb43 --- /dev/null +++ b/lib/std/Io/Writer.zig @@ -0,0 +1,2486 @@ +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 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, + +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. + /// + /// This function may not be called if the data to be written could have + /// been stored in `buffer` instead, including when the amount of data to + /// be written is zero and the buffer capacity is 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, +}; + +pub const FileAllError = error{ + /// Detailed diagnostics are found on the `File.Reader` struct. + ReadFailed, + /// See the `Writer` implementation for detailed diagnostics. + WriteFailed, +}; + +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 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. +pub fn fixed(buffer: []u8) Writer { + return .{ + .vtable = &.{ .drain = fixedDrain }, + .buffer = buffer, + }; +} + +pub fn hashed(w: *Writer, hasher: anytype, buffer: []u8) Hashed(@TypeOf(hasher)) { + return .initHasher(w, hasher, buffer); +} + +pub const failing: Writer = .{ + .vtable = &.{ + .drain = failingDrain, + .sendFile = failingSendFile, + }, +}; + +/// 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) return w.vtable.drain(w, data, splat); + for (data[0 .. data.len - 1]) |bytes| { + @memcpy(buffer[w.end..][0..bytes.len], bytes); + w.end += bytes.len; + } + const pattern = data[data.len - 1]; + switch (pattern.len) { + 0 => {}, + 1 => { + @memset(buffer[w.end..][0..splat], pattern[0]); + w.end += splat; + }, + else => for (0..splat) |_| { + @memcpy(buffer[w.end..][0..pattern.len], pattern); + w.end += pattern.len; + }, + } + return count; +} + +/// 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; + 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[0 .. data.len - 1]) |buf| { + if (buf.len == 0) continue; + vecs[i] = buf; + i += 1; + if (vecs.len - i == 0) break; + } + const pattern = data[data.len - 1]; + const new_splat = s: { + if (pattern.len == 0 or vecs.len - i == 0) break :s 1; + vecs[i] = pattern; + i += 1; + break :s splat; + }; + return w.vtable.drain(w, vecs[0..i], new_splat); +} + +test "writeSplatHeader splatting avoids buffer aliasing temptation" { + const initial_buf = try testing.allocator.alloc(u8, 8); + var aw: std.io.Writer.Allocating = .initOwnedSlice(testing.allocator, initial_buf); + defer aw.deinit(); + // This test assumes 8 vector buffer in this function. + const n = try aw.writer.writeSplatHeader("header which is longer than buf ", &.{ + "1", "2", "3", "4", "5", "6", "foo", "bar", "foo", + }, 3); + try testing.expectEqual(41, n); + try testing.expectEqualStrings( + "header which is longer than buf 123456foo", + aw.writer.buffered(), + ); +} + +/// 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..]; + } +} + +/// 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, + /// Tracks whether the "writable vector" API was used. + used: bool = false, + pub const vtable: *const VTable = &unique_vtable_allocation; + /// This is intended to be constant but it must be a unique address for + /// `@fieldParentPtr` to work. + var unique_vtable_allocation: VTable = .{ .drain = fixedDrain }; +}; + +pub fn writableVectorIterator(w: *Writer) Error!WritableVectorIterator { + if (w.vtable == VectorWrapper.vtable) { + const wrapper: *VectorWrapper = @fieldParentPtr("writer", w); + wrapper.used = true; + 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; + 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; +} + +/// 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; +} + +/// After calling `writableVector`, this function tracks how many bytes were +/// written to it. +pub fn advanceVector(w: *Writer, n: usize) usize { + 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; + } + } +} + +/// 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; + return bytes.len; + } + return w.vtable.drain(w, &.{bytes}, 1); +} + +/// 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; + 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); + 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..]); +} + +/// Renders fmt string with args, calling `writer` with slices of bytes. +/// If `writer` returns an error, the error is returned from `format` and +/// `writer` is not called again. +/// +/// The format string must be comptime-known and may contain placeholders following +/// this format: +/// `{[argument][specifier]:[fill][alignment][width].[precision]}` +/// +/// Above, each word including its surrounding [ and ] is a parameter which you have to replace with something: +/// +/// - *argument* is either the numeric index or the field name of the argument that should be inserted +/// - when using a field name, you are required to enclose the field name (an identifier) in square +/// brackets, e.g. {[score]...} as opposed to the numeric index form which can be written e.g. {2...} +/// - *specifier* is a type-dependent formatting option that determines how a type should formatted (see below) +/// - *fill* is a single byte which is used to pad formatted numbers. +/// - *alignment* is one of the three bytes '<', '^', or '>' to make numbers +/// left, center, or right-aligned, respectively. +/// - Not all specifiers support alignment. +/// - Alignment is not Unicode-aware; appropriate only when used with raw bytes or ASCII. +/// - *width* is the total width of the field in bytes. This only applies to number formatting. +/// - *precision* specifies how many decimals a formatted number should have. +/// +/// Note that most of the parameters are optional and may be omitted. Also you +/// can leave out separators like `:` and `.` when all parameters after the +/// separator are omitted. +/// +/// Only exception is the *fill* parameter. If a non-zero *fill* character is +/// required at the same time as *width* is specified, one has to specify +/// *alignment* as well, as otherwise the digit following `:` is interpreted as +/// *width*, not *fill*. +/// +/// The *specifier* has several options for types: +/// - `x` and `X`: output numeric value in hexadecimal notation, or string in hexadecimal bytes +/// - `s`: +/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination +/// - for slices of u8, print the entire slice as a string without zero-termination +/// - `t`: +/// - for enums and tagged unions: prints the tag name +/// - for error sets: prints the error name +/// - `b64`: output string as standard base64 +/// - `e`: output floating point value in scientific notation +/// - `d`: output numeric value in decimal notation +/// - `b`: output integer value in binary notation +/// - `o`: output integer value in octal notation +/// - `c`: output integer as an ASCII character. Integer type must have 8 bits at max. +/// - `u`: output integer as an UTF-8 sequence. Integer type must have 21 bits at max. +/// - `D`: output nanoseconds as duration +/// - `B`: output bytes in SI units (decimal) +/// - `Bi`: output bytes in IEC units (binary) +/// - `?`: output optional value as either the unwrapped value, or `null`; may be followed by a format specifier for the underlying value. +/// - `!`: output error union value as either the unwrapped value, or the formatted error value; may be followed by a format specifier for the underlying value. +/// - `*`: output the address of the value instead of the value itself. +/// - `any`: output a value of any type using its default format. +/// - `f`: delegates to a method on the type named "format" with the signature `fn (*Writer, args: anytype) Writer.Error!void`. +/// +/// A user type may be a `struct`, `vector`, `union` or `enum` type. +/// +/// To print literal curly braces, escape them by writing them twice, e.g. `{{` or `}}`. +pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void { + const ArgsType = @TypeOf(args); + const args_type_info = @typeInfo(ArgsType); + if (args_type_info != .@"struct") { + @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); + } + + const fields_info = args_type_info.@"struct".fields; + const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits; + if (fields_info.len > max_format_args) { + @compileError("32 arguments max are supported per format call"); + } + + @setEvalBranchQuota(fmt.len * 1000); + comptime var arg_state: std.fmt.ArgState = .{ .args_len = fields_info.len }; + comptime var i = 0; + comptime var literal: []const u8 = ""; + inline while (true) { + const start_index = i; + + inline while (i < fmt.len) : (i += 1) { + switch (fmt[i]) { + '{', '}' => break, + else => {}, + } + } + + comptime var end_index = i; + comptime var unescape_brace = false; + + // Handle {{ and }}, those are un-escaped as single braces + if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) { + unescape_brace = true; + // Make the first brace part of the literal... + end_index += 1; + // ...and skip both + i += 2; + } + + literal = literal ++ fmt[start_index..end_index]; + + // We've already skipped the other brace, restart the loop + if (unescape_brace) continue; + + // Write out the literal + if (literal.len != 0) { + try w.writeAll(literal); + literal = ""; + } + + if (i >= fmt.len) break; + + if (fmt[i] == '}') { + @compileError("missing opening {"); + } + + // Get past the { + comptime assert(fmt[i] == '{'); + i += 1; + + const fmt_begin = i; + // Find the closing brace + inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {} + const fmt_end = i; + + if (i >= fmt.len) { + @compileError("missing closing }"); + } + + // Get past the } + comptime assert(fmt[i] == '}'); + i += 1; + + const placeholder_array = fmt[fmt_begin..fmt_end].*; + const placeholder = comptime std.fmt.Placeholder.parse(&placeholder_array); + const arg_pos = comptime switch (placeholder.arg) { + .none => null, + .number => |pos| pos, + .named => |arg_name| std.meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("no argument with name '" ++ arg_name ++ "'"), + }; + + const width = switch (placeholder.width) { + .none => null, + .number => |v| v, + .named => |arg_name| blk: { + const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("no argument with name '" ++ arg_name ++ "'"); + _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); + break :blk @field(args, arg_name); + }, + }; + + const precision = switch (placeholder.precision) { + .none => null, + .number => |v| v, + .named => |arg_name| blk: { + const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse + @compileError("no argument with name '" ++ arg_name ++ "'"); + _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); + break :blk @field(args, arg_name); + }, + }; + + const arg_to_print = comptime arg_state.nextArg(arg_pos) orelse + @compileError("too few arguments"); + + try w.printValue( + placeholder.specifier_arg, + .{ + .fill = placeholder.fill, + .alignment = placeholder.alignment, + .width = width, + .precision = precision, + }, + @field(args, fields_info[arg_to_print].name), + std.options.fmt_max_depth, + ); + } + + if (comptime arg_state.hasUnusedArgs()) { + const missing_count = arg_state.args_len - @popCount(arg_state.used_args); + switch (missing_count) { + 0 => unreachable, + 1 => @compileError("unused argument in '" ++ fmt ++ "'"), + else => @compileError(std.fmt.comptimePrint("{d}", .{missing_count}) ++ " unused arguments in '" ++ fmt ++ "'"), + } + } +} + +/// 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) return; + } else { + @branchHint(.likely); + w.buffer[w.end] = byte; + w.end += 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; + } +} + +/// 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; + std.mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian); + return w.writeAll(&bytes); +} + +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 w.writeAll(std.mem.asBytes(&value)); +} + +/// 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 { + switch (@typeInfo(@TypeOf(value))) { + .@"struct" => |info| switch (info.layout) { + .auto => @compileError("ill-defined memory layout"), + .@"extern" => { + if (native_endian == endian) { + return w.writeStruct(value); + } else { + var copy = value; + std.mem.byteSwapAllFields(@TypeOf(value), ©); + return w.writeStruct(copy); + } + }, + .@"packed" => { + return writeInt(w, info.backing_integer.?, @bitCast(value), endian); + }, + }, + else => @compileError("not a struct"), + } +} + +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); + } +} + +/// 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; + 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); + 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; +} + +/// Number of bytes logically written is returned. This excludes bytes from +/// `buffer` because they have already been logically written. +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 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) ++ "@"); + const int = if (info.size == .slice) @intFromPtr(value.ptr) else @intFromPtr(value); + return w.printInt(int, 16, .lower, .{}); + }, + .optional => |info| { + if (@typeInfo(info.child) == .pointer) { + try w.writeAll(@typeName(info.child) ++ "@"); + try w.printInt(@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); + + switch (fmt.len) { + 1 => switch (fmt[0]) { + '*' => return w.printAddress(value), + 'f' => return value.format(w), + 'd' => switch (@typeInfo(T)) { + .float, .comptime_float => return printFloat(w, value, options.toNumber(.decimal, .lower)), + .int, .comptime_int => return printInt(w, value, 10, .lower, options), + .@"struct" => return value.formatNumber(w, options.toNumber(.decimal, .lower)), + .@"enum" => return printInt(w, @intFromEnum(value), 10, .lower, options), + .vector => return printVector(w, fmt, options, value, max_depth), + else => invalidFmtError(fmt, value), + }, + 'c' => return w.printAsciiChar(value, options), + 'u' => return w.printUnicodeCodepoint(value), + 'b' => switch (@typeInfo(T)) { + .int, .comptime_int => return printInt(w, value, 2, .lower, options), + .@"enum" => return printInt(w, @intFromEnum(value), 2, .lower, options), + .@"struct" => return value.formatNumber(w, options.toNumber(.binary, .lower)), + .vector => return printVector(w, fmt, options, value, max_depth), + else => invalidFmtError(fmt, value), + }, + 'o' => switch (@typeInfo(T)) { + .int, .comptime_int => return printInt(w, value, 8, .lower, options), + .@"enum" => return printInt(w, @intFromEnum(value), 8, .lower, options), + .@"struct" => return value.formatNumber(w, options.toNumber(.octal, .lower)), + .vector => return printVector(w, fmt, options, value, max_depth), + else => invalidFmtError(fmt, value), + }, + 'x' => switch (@typeInfo(T)) { + .float, .comptime_float => return printFloatHexOptions(w, value, options.toNumber(.hex, .lower)), + .int, .comptime_int => return printInt(w, value, 16, .lower, options), + .@"enum" => return printInt(w, @intFromEnum(value), 16, .lower, options), + .@"struct" => return value.formatNumber(w, options.toNumber(.hex, .lower)), + .pointer => |info| switch (info.size) { + .one, .slice => { + const slice: []const u8 = value; + optionsForbidden(options); + return printHex(w, slice, .lower); + }, + .many, .c => { + const slice: [:0]const u8 = std.mem.span(value); + optionsForbidden(options); + return printHex(w, slice, .lower); + }, + }, + .array => { + const slice: []const u8 = &value; + optionsForbidden(options); + return printHex(w, slice, .lower); + }, + .vector => return printVector(w, fmt, options, value, max_depth), + else => invalidFmtError(fmt, value), + }, + 'X' => switch (@typeInfo(T)) { + .float, .comptime_float => return printFloatHexOptions(w, value, options.toNumber(.hex, .lower)), + .int, .comptime_int => return printInt(w, value, 16, .upper, options), + .@"enum" => return printInt(w, @intFromEnum(value), 16, .upper, options), + .@"struct" => return value.formatNumber(w, options.toNumber(.hex, .upper)), + .pointer => |info| switch (info.size) { + .one, .slice => { + const slice: []const u8 = value; + optionsForbidden(options); + return printHex(w, slice, .upper); + }, + .many, .c => { + const slice: [:0]const u8 = std.mem.span(value); + optionsForbidden(options); + return printHex(w, slice, .upper); + }, + }, + .array => { + const slice: []const u8 = &value; + optionsForbidden(options); + return printHex(w, slice, .upper); + }, + .vector => return printVector(w, fmt, options, value, max_depth), + else => invalidFmtError(fmt, value), + }, + 's' => switch (@typeInfo(T)) { + .pointer => |info| switch (info.size) { + .one, .slice => { + const slice: []const u8 = value; + return w.alignBufferOptions(slice, options); + }, + .many, .c => { + const slice: [:0]const u8 = std.mem.span(value); + return w.alignBufferOptions(slice, options); + }, + }, + .array => { + const slice: []const u8 = &value; + return w.alignBufferOptions(slice, options); + }, + else => invalidFmtError(fmt, value), + }, + 'B' => switch (@typeInfo(T)) { + .int, .comptime_int => return w.printByteSize(value, .decimal, options), + .@"struct" => return value.formatByteSize(w, .decimal), + else => invalidFmtError(fmt, value), + }, + 'D' => switch (@typeInfo(T)) { + .int, .comptime_int => return w.printDuration(value, options), + .@"struct" => return value.formatDuration(w), + else => invalidFmtError(fmt, value), + }, + 'e' => switch (@typeInfo(T)) { + .float, .comptime_float => return printFloat(w, value, options.toNumber(.scientific, .lower)), + .@"struct" => return value.formatNumber(w, options.toNumber(.scientific, .lower)), + else => invalidFmtError(fmt, value), + }, + 'E' => switch (@typeInfo(T)) { + .float, .comptime_float => return printFloat(w, value, options.toNumber(.scientific, .upper)), + .@"struct" => return value.formatNumber(w, options.toNumber(.scientific, .upper)), + else => invalidFmtError(fmt, value), + }, + 't' => switch (@typeInfo(T)) { + .error_set => return w.writeAll(@errorName(value)), + .@"enum", .@"union" => return w.writeAll(@tagName(value)), + else => invalidFmtError(fmt, value), + }, + else => {}, + }, + 2 => switch (fmt[0]) { + 'B' => switch (fmt[1]) { + 'i' => switch (@typeInfo(T)) { + .int, .comptime_int => return w.printByteSize(value, .binary, options), + .@"struct" => return value.formatByteSize(w, .binary), + else => invalidFmtError(fmt, value), + }, + else => {}, + }, + else => {}, + }, + 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) { + .pointer => |info| switch (info.size) { + .one, .slice => { + const slice: []const u8 = value; + optionsForbidden(options); + return w.printBase64(slice); + }, + .many, .c => { + const slice: [:0]const u8 = std.mem.span(value); + optionsForbidden(options); + return w.printBase64(slice); + }, + }, + .array => { + const slice: []const u8 = &value; + optionsForbidden(options); + return w.printBase64(slice); + }, + else => invalidFmtError(fmt, value), + }, + else => {}, + } + + const is_any = comptime std.mem.eql(u8, fmt, ANY); + if (!is_any and std.meta.hasMethod(T, "format") and fmt.len == 0) { + // after 0.15.0 is tagged, delete this compile error and its condition + @compileError("ambiguous format string; specify {f} to call format method, or {any} to skip it"); + } + + switch (@typeInfo(T)) { + .float, .comptime_float => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return printFloat(w, value, options.toNumber(.decimal, .lower)); + }, + .int, .comptime_int => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return printInt(w, value, 10, .lower, options); + }, + .bool => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + const string: []const u8 = if (value) "true" else "false"; + return w.alignBufferOptions(string, 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 (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + optionsForbidden(options); + return printErrorSet(w, value); + }, + .@"enum" => |info| { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + optionsForbidden(options); + if (info.is_exhaustive) { + return printEnumExhaustive(w, value); + } else { + return printEnumNonexhaustive(w, value); + } + }, + .@"union" => |info| { + if (!is_any) { + if (fmt.len != 0) invalidFmtError(fmt, value); + return printValue(w, ANY, options, value, max_depth); + } + 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 switch (info.layout) { + .auto => { + return w.writeAll(".{ ... }"); + }, + .@"extern", .@"packed" => { + if (info.fields.len == 0) return w.writeAll(".{}"); + try w.writeAll(".{ "); + inline for (info.fields) |field| { + try w.writeByte('.'); + try w.writeAll(field.name); + try w.writeAll(" = "); + try w.printValue(ANY, options, @field(value, field.name), max_depth - 1); + (try w.writableArray(2)).* = ", ".*; + } + w.buffer[w.end - 2 ..][0..2].* = " }".*; + }, + } + }, + .@"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; + } + 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 => |array_info| return w.printValue(fmt, options, @as([]const array_info.child, value), max_depth), + .@"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.printInt(@intFromPtr(value), 16, .lower, options); + return; + }, + }, + .many, .c => { + if (!is_any) @compileError("cannot format pointer without a specifier (i.e. {s} or {*})"); + optionsForbidden(options); + try w.printAddress(value); + }, + .slice => { + if (!is_any) + @compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})"); + if (max_depth == 0) return w.writeAll("{ ... }"); + 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 => { + if (!is_any) @compileError("cannot format array without a specifier (i.e. {s} or {any})"); + if (max_depth == 0) return w.writeAll("{ ... }"); + 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 => { + if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); + return printVector(w, fmt, options, value, max_depth); + }, + .@"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); + optionsForbidden(options); + var vecs: [2][]const u8 = .{ ".", @tagName(value) }; + return w.writeVecAll(&vecs); + }, + .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 optionsForbidden(options: std.fmt.Options) void { + assert(options.precision == null); + assert(options.width == null); +} + +fn printErrorSet(w: *Writer, error_set: anyerror) Error!void { + var vecs: [2][]const u8 = .{ "error.", @errorName(error_set) }; + try w.writeVecAll(&vecs); +} + +fn printEnumExhaustive(w: *Writer, value: anytype) Error!void { + var vecs: [2][]const u8 = .{ ".", @tagName(value) }; + try w.writeVecAll(&vecs); +} + +fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void { + if (std.enums.tagName(@TypeOf(value), value)) |tag_name| { + var vecs: [2][]const u8 = .{ ".", tag_name }; + try w.writeVecAll(&vecs); + return; + } + try w.writeAll("@enumFromInt("); + try w.printInt(@intFromEnum(value), 10, .lower, .{}); + try w.writeByte(')'); +} + +pub fn printVector( + w: *Writer, + comptime fmt: []const u8, + options: std.fmt.Options, + value: anytype, + max_depth: usize, +) Error!void { + const len = @typeInfo(@TypeOf(value)).vector.len; + if (max_depth == 0) return w.writeAll("{ ... }"); + try w.writeAll("{ "); + inline for (0..len) |i| { + try w.printValue(fmt, options, value[i], max_depth - 1); + if (i < len - 1) try w.writeAll(", "); + } + try w.writeAll(" }"); +} + +// A wrapper around `printIntAny` to avoid the generic explosion of this +// function by funneling smaller integer types through `isize` and `usize`. +pub inline fn printInt( + w: *Writer, + value: anytype, + base: u8, + case: std.fmt.Case, + options: std.fmt.Options, +) Error!void { + switch (@TypeOf(value)) { + isize, usize => {}, + comptime_int => { + if (comptime std.math.cast(usize, value)) |x| return printIntAny(w, x, base, case, options); + if (comptime std.math.cast(isize, value)) |x| return printIntAny(w, x, base, case, options); + const Int = std.math.IntFittingRange(value, value); + return printIntAny(w, @as(Int, value), base, case, options); + }, + else => switch (@typeInfo(@TypeOf(value)).int.signedness) { + .signed => if (std.math.cast(isize, value)) |x| return printIntAny(w, x, base, case, options), + .unsigned => if (std.math.cast(usize, value)) |x| return printIntAny(w, x, base, case, options), + }, + } + return printIntAny(w, value, base, case, options); +} + +/// In general, prefer `printInt` to avoid generic explosion. However this +/// function may be used when optimal codegen for a particular integer type is +/// desired. +pub fn printIntAny( + w: *Writer, + value: anytype, + base: u8, + case: std.fmt.Case, + options: std.fmt.Options, +) Error!void { + assert(base >= 2); + const value_info = @typeInfo(@TypeOf(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(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 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) Error!void { + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(c, &buf) catch |err| switch (err) { + error.Utf8CannotEncodeSurrogateHalf, error.CodepointTooLarge => l: { + buf[0..3].* = std.unicode.replacement_character_utf8; + break :l 3; + }, + }; + return w.writeAll(buf[0..len]); +} + +/// Uses a larger stack buffer; asserts mode is decimal or scientific. +pub fn printFloat(w: *Writer, value: anytype, options: std.fmt.Number) Error!void { + const mode: std.fmt.float.Mode = switch (options.mode) { + .decimal => .decimal, + .scientific => .scientific, + .binary, .octal, .hex => unreachable, + }; + var buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined; + const s = std.fmt.float.render(&buf, value, .{ + .mode = mode, + .precision = options.precision, + }) catch |err| switch (err) { + error.BufferTooSmall => "(float)", + }; + return w.alignBuffer(s, options.width orelse s.len, options.alignment, options.fill); +} + +/// Uses a smaller stack buffer; asserts mode is not decimal or scientific. +pub fn printFloatHexOptions(w: *Writer, value: anytype, options: std.fmt.Number) Error!void { + var buf: [50]u8 = undefined; // for aligning + var sub_writer: Writer = .fixed(&buf); + switch (options.mode) { + .decimal => unreachable, + .scientific => unreachable, + .binary => @panic("TODO"), + .octal => @panic("TODO"), + .hex => {}, + } + printFloatHex(&sub_writer, value, options.case, options.precision) catch unreachable; // buf is large enough + + const printed = sub_writer.buffered(); + return w.alignBuffer(printed, options.width orelse printed.len, options.alignment, options.fill); +} + +pub fn printFloatHex(w: *Writer, value: anytype, case: std.fmt.Case, opt_precision: ?usize) Error!void { + if (std.math.signbit(value)) try w.writeByte('-'); + if (std.math.isNan(value)) return w.writeAll(switch (case) { + .lower => "nan", + .upper => "NAN", + }); + if (std.math.isInf(value)) return w.writeAll(switch (case) { + .lower => "inf", + .upper => "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, case, .{ .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.printInt(exponent - exponent_bias, 10, case, .{}); +} + +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.printInt(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.printInt(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.printInt(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.printInt(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_writer: Writer = .fixed(&buf); + if (@TypeOf(nanoseconds) == comptime_int) { + if (nanoseconds >= 0) { + sub_writer.printDurationUnsigned(nanoseconds) catch unreachable; + } else { + sub_writer.printDurationSigned(nanoseconds) catch unreachable; + } + } else switch (@typeInfo(@TypeOf(nanoseconds)).int.signedness) { + .signed => sub_writer.printDurationSigned(nanoseconds) catch unreachable, + .unsigned => sub_writer.printDurationUnsigned(nanoseconds) catch unreachable, + } + return w.alignBufferOptions(sub_writer.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 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 "printValue max_depth" { + const Vec2 = struct { + const SelfType = @This(); + x: f32, + y: f32, + + pub fn format(self: SelfType, w: *Writer) Error!void { + return w.print("({d:.3},{d:.3})", .{ self.x, self.y }); + } + }; + 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(".{ ... }", w.buffered()); + + w = .fixed(&buf); + try w.printValue("", .{}, inst, 1); + try testing.expectEqualStrings(".{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }", w.buffered()); + + w = .fixed(&buf); + try w.printValue("", .{}, inst, 2); + try testing.expectEqualStrings(".{ .a = .{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }, .tu = .{ .ptr = .{ ... } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }", w.buffered()); + + w = .fixed(&buf); + try w.printValue("", .{}, inst, 3); + try testing.expectEqualStrings(".{ .a = .{ .a = .{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }, .tu = .{ .ptr = .{ ... } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }, .tu = .{ .ptr = .{ .ptr = .{ ... } } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }", w.buffered()); + + const vec: @Vector(4, i32) = .{ 1, 2, 3, 4 }; + w = .fixed(&buf); + try w.printValue("", .{}, vec, 0); + try testing.expectEqualStrings("{ ... }", w.buffered()); + + w = .fixed(&buf); + try w.printValue("", .{}, vec, 1); + try testing.expectEqualStrings("{ 1, 2, 3, 4 }", w.buffered()); +} + +test printDuration { + try testDurationCase("0ns", 0); + try testDurationCase("1ns", 1); + try testDurationCase("999ns", std.time.ns_per_us - 1); + try testDurationCase("1us", std.time.ns_per_us); + try testDurationCase("1.45us", 1450); + try testDurationCase("1.5us", 3 * std.time.ns_per_us / 2); + try testDurationCase("14.5us", 14500); + try testDurationCase("145us", 145000); + try testDurationCase("999.999us", std.time.ns_per_ms - 1); + try testDurationCase("1ms", std.time.ns_per_ms + 1); + try testDurationCase("1.5ms", 3 * std.time.ns_per_ms / 2); + try testDurationCase("1.11ms", 1110000); + try testDurationCase("1.111ms", 1111000); + try testDurationCase("1.111ms", 1111100); + try testDurationCase("999.999ms", std.time.ns_per_s - 1); + try testDurationCase("1s", std.time.ns_per_s); + try testDurationCase("59.999s", std.time.ns_per_min - 1); + try testDurationCase("1m", std.time.ns_per_min); + try testDurationCase("1h", std.time.ns_per_hour); + try testDurationCase("1d", std.time.ns_per_day); + try testDurationCase("1w", std.time.ns_per_week); + try testDurationCase("1y", 365 * std.time.ns_per_day); + try testDurationCase("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1 + try testDurationCase("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); + try testDurationCase("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); + try testDurationCase("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); + try testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); + try testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); + try testDurationCase("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); + try testDurationCase("584y49w23h34m33.709s", std.math.maxInt(u64)); + + try testing.expectFmt("=======0ns", "{D:=>10}", .{0}); + try testing.expectFmt("1ns=======", "{D:=<10}", .{1}); + try testing.expectFmt(" 999ns ", "{D:^10}", .{std.time.ns_per_us - 1}); +} + +test printDurationSigned { + try testDurationCaseSigned("0ns", 0); + try testDurationCaseSigned("1ns", 1); + try testDurationCaseSigned("-1ns", -(1)); + try testDurationCaseSigned("999ns", std.time.ns_per_us - 1); + try testDurationCaseSigned("-999ns", -(std.time.ns_per_us - 1)); + try testDurationCaseSigned("1us", std.time.ns_per_us); + try testDurationCaseSigned("-1us", -(std.time.ns_per_us)); + try testDurationCaseSigned("1.45us", 1450); + try testDurationCaseSigned("-1.45us", -(1450)); + try testDurationCaseSigned("1.5us", 3 * std.time.ns_per_us / 2); + try testDurationCaseSigned("-1.5us", -(3 * std.time.ns_per_us / 2)); + try testDurationCaseSigned("14.5us", 14500); + try testDurationCaseSigned("-14.5us", -(14500)); + try testDurationCaseSigned("145us", 145000); + try testDurationCaseSigned("-145us", -(145000)); + try testDurationCaseSigned("999.999us", std.time.ns_per_ms - 1); + try testDurationCaseSigned("-999.999us", -(std.time.ns_per_ms - 1)); + try testDurationCaseSigned("1ms", std.time.ns_per_ms + 1); + try testDurationCaseSigned("-1ms", -(std.time.ns_per_ms + 1)); + try testDurationCaseSigned("1.5ms", 3 * std.time.ns_per_ms / 2); + try testDurationCaseSigned("-1.5ms", -(3 * std.time.ns_per_ms / 2)); + try testDurationCaseSigned("1.11ms", 1110000); + try testDurationCaseSigned("-1.11ms", -(1110000)); + try testDurationCaseSigned("1.111ms", 1111000); + try testDurationCaseSigned("-1.111ms", -(1111000)); + try testDurationCaseSigned("1.111ms", 1111100); + try testDurationCaseSigned("-1.111ms", -(1111100)); + try testDurationCaseSigned("999.999ms", std.time.ns_per_s - 1); + try testDurationCaseSigned("-999.999ms", -(std.time.ns_per_s - 1)); + try testDurationCaseSigned("1s", std.time.ns_per_s); + try testDurationCaseSigned("-1s", -(std.time.ns_per_s)); + try testDurationCaseSigned("59.999s", std.time.ns_per_min - 1); + try testDurationCaseSigned("-59.999s", -(std.time.ns_per_min - 1)); + try testDurationCaseSigned("1m", std.time.ns_per_min); + try testDurationCaseSigned("-1m", -(std.time.ns_per_min)); + try testDurationCaseSigned("1h", std.time.ns_per_hour); + try testDurationCaseSigned("-1h", -(std.time.ns_per_hour)); + try testDurationCaseSigned("1d", std.time.ns_per_day); + try testDurationCaseSigned("-1d", -(std.time.ns_per_day)); + try testDurationCaseSigned("1w", std.time.ns_per_week); + try testDurationCaseSigned("-1w", -(std.time.ns_per_week)); + try testDurationCaseSigned("1y", 365 * std.time.ns_per_day); + try testDurationCaseSigned("-1y", -(365 * std.time.ns_per_day)); + try testDurationCaseSigned("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1d + try testDurationCaseSigned("-1y52w23h59m59.999s", -(730 * std.time.ns_per_day - 1)); // 365d = 52w1d + try testDurationCaseSigned("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); + try testDurationCaseSigned("-1y1h1.001s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms)); + try testDurationCaseSigned("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); + try testDurationCaseSigned("-1y1h1s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us)); + try testDurationCaseSigned("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); + try testDurationCaseSigned("-1y1h999.999us", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1)); + try testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); + try testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms)); + try testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); + try testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1)); + try testDurationCaseSigned("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); + try testDurationCaseSigned("-1y1m999ns", -(365 * std.time.ns_per_day + std.time.ns_per_min + 999)); + try testDurationCaseSigned("292y24w3d23h47m16.854s", std.math.maxInt(i64)); + try testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64) + 1); + try testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64)); + + try testing.expectFmt("=======0ns", "{D:=>10}", .{0}); + try testing.expectFmt("1ns=======", "{D:=<10}", .{1}); + try testing.expectFmt("-1ns======", "{D:=<10}", .{-(1)}); + try testing.expectFmt(" -999ns ", "{D:^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 printInt { + 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 }); + + try testPrintIntCase("123456789123456789", @as(comptime_int, 123456789123456789), 10, .lower, .{}); +} + +test "printFloat with comptime_float" { + var buf: [20]u8 = undefined; + var w: Writer = .fixed(&buf); + try w.printFloat(@as(comptime_float, 1.0), std.fmt.Options.toNumber(.{}, .scientific, .lower)); + try testing.expectEqualStrings(w.buffered(), "1e0"); + try testing.expectFmt("1", "{}", .{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); + try w.printInt(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 testing.expectFmt("lowercase: cafebabe\n", "lowercase: {x}\n", .{some_bytes}); + try testing.expectFmt("uppercase: CAFEBABE\n", "uppercase: {X}\n", .{some_bytes}); + try testing.expectFmt("uppercase: CAFE\n", "uppercase: {X}\n", .{some_bytes[0..2]}); + try testing.expectFmt("lowercase: babe\n", "lowercase: {x}\n", .{some_bytes[2..]}); + const bytes_with_zeros = "\x00\x0E\xBA\xBE"; + try 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.WriteFailed, w.writeAll("!")); + try testing.expect(std.mem.eql(u8, w.buffered(), "Helloworld")); + + w = .fixed(&buffer); + + try testing.expect(w.buffered().len == 0); + + try testing.expectError(error.WriteFailed, w.writeAll("Hello world!")); + try testing.expect(std.mem.eql(u8, w.buffered(), "Hello worl")); +} + +test "writeSplat 0 len splat larger than capacity" { + var buf: [8]u8 = undefined; + var w: std.io.Writer = .fixed(&buf); + const n = try w.writeSplat(&.{"something that overflows buf"}, 0); + try testing.expectEqual(0, n); +} + +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 const Discarding = struct { + count: u64, + writer: Writer, + + pub fn init(buffer: []u8) Discarding { + return .{ + .count = 0, + .writer = .{ + .vtable = &.{ + .drain = Discarding.drain, + .sendFile = Discarding.sendFile, + }, + .buffer = buffer, + }, + }; + } + + pub fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + const d: *Discarding = @alignCast(@fieldParentPtr("writer", w)); + const slice = data[0 .. data.len - 1]; + const pattern = data[slice.len..]; + var written: usize = pattern.len * splat; + for (slice) |bytes| written += bytes.len; + d.count += w.end + written; + w.end = 0; + return written; + } + + pub fn sendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { + if (File.Handle == void) return error.Unimplemented; + const d: *Discarding = @alignCast(@fieldParentPtr("writer", w)); + d.count += w.end; + w.end = 0; + if (file_reader.getSize()) |size| { + const n = limit.minInt64(size - file_reader.pos); + file_reader.seekBy(@intCast(n)) catch return error.Unimplemented; + w.end = 0; + d.count += n; + 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, + writer: Writer, + + pub fn init(out: *Writer, buffer: []u8) @This() { + return .initHasher(out, .{}, buffer); + } + + pub fn initHasher(out: *Writer, hasher: Hasher, buffer: []u8) @This() { + return .{ + .out = out, + .hasher = hasher, + .writer = .{ + .buffer = buffer, + .vtable = &.{ .drain = @This().drain }, + }, + }; + } + + fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { + const this: *@This() = @alignCast(@fieldParentPtr("writer", w)); + const aux = w.buffered(); + const aux_n = try this.out.writeSplatHeader(aux, 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(aux); + const n = aux_n - w.end; + w.end = 0; + var remaining: usize = n; + for (data[0 .. data.len - 1]) |slice| { + if (remaining <= slice.len) { + this.hasher.update(slice[0..remaining]); + return n; + } + remaining -= slice.len; + this.hasher.update(slice); + } + const pattern = data[data.len - 1]; + assert(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, + writer: Writer, + + pub fn init(allocator: Allocator) Allocating { + return .{ + .allocator = allocator, + .writer = .{ + .buffer = &.{}, + .vtable = &vtable, + }, + }; + } + + pub fn initCapacity(allocator: Allocator, capacity: usize) error{OutOfMemory}!Allocating { + return .{ + .allocator = allocator, + .writer = .{ + .buffer = try allocator.alloc(u8, capacity), + .vtable = &vtable, + }, + }; + } + + pub fn initOwnedSlice(allocator: Allocator, slice: []u8) Allocating { + return .{ + .allocator = allocator, + .writer = .{ + .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, + .writer = .{ + .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.writer.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.writer; + 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.writer.buffered(); + } + + pub fn shrinkRetainingCapacity(a: *Allocating, new_len: usize) void { + a.writer.end = new_len; + } + + 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("writer", 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; + // Even if we append no data, this function needs to ensure there is more + // capacity in the buffer to avoid infinite loop, hence the +1 in this loop. + assert(data.len != 0); + for (data) |bytes| { + list.ensureUnusedCapacity(gpa, bytes.len + splat_len + 1) 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("writer", 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.minInt64(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.writer.buffer = list.allocatedSlice(); + a.writer.end = list.items.len; + } + + test Allocating { + var a: Allocating = .init(testing.allocator); + defer a.deinit(); + const w = &a.writer; + + 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/bit_reader.zig b/lib/std/Io/bit_reader.zig new file mode 100644 index 0000000000000000000000000000000000000000..7823e47d43fcc5f8eb51416ad0df9265299a180c --- /dev/null +++ b/lib/std/Io/bit_reader.zig @@ -0,0 +1,238 @@ +const std = @import("../std.zig"); + +//General note on endianess: +//Big endian is packed starting in the most significant part of the byte and subsequent +// bytes contain less significant bits. Thus we always take bits from the high +// end and place them below existing bits in our output. +//Little endian is packed starting in the least significant part of the byte and +// subsequent bytes contain more significant bits. Thus we always take bits from +// the low end and place them above existing bits in our output. +//Regardless of endianess, within any given byte the bits are always in most +// to least significant order. +//Also regardless of endianess, the buffer always aligns bits to the low end +// of the byte. + +/// Creates a bit reader which allows for reading bits from an underlying standard reader +pub fn BitReader(comptime endian: std.builtin.Endian, comptime Reader: type) type { + return struct { + reader: Reader, + bits: u8 = 0, + count: u4 = 0, + + const low_bit_mask = [9]u8{ + 0b00000000, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, + }; + + fn Bits(comptime T: type) type { + return struct { + T, + u16, + }; + } + + fn initBits(comptime T: type, out: anytype, num: u16) Bits(T) { + const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); + return .{ + @bitCast(@as(UT, @intCast(out))), + num, + }; + } + + /// Reads `bits` bits from the reader and returns a specified type + /// containing them in the least significant end, returning an error if the + /// specified number of bits could not be read. + pub fn readBitsNoEof(self: *@This(), comptime T: type, num: u16) !T { + const b, const c = try self.readBitsTuple(T, num); + if (c < num) return error.EndOfStream; + return b; + } + + /// Reads `bits` bits from the reader and returns a specified type + /// containing them in the least significant end. The number of bits successfully + /// read is placed in `out_bits`, as reaching the end of the stream is not an error. + pub fn readBits(self: *@This(), comptime T: type, num: u16, out_bits: *u16) !T { + const b, const c = try self.readBitsTuple(T, num); + out_bits.* = c; + return b; + } + + /// Reads `bits` bits from the reader and returns a tuple of the specified type + /// containing them in the least significant end, and the number of bits successfully + /// read. Reaching the end of the stream is not an error. + pub fn readBitsTuple(self: *@This(), comptime T: type, num: u16) !Bits(T) { + const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); + const U = if (@bitSizeOf(T) < 8) u8 else UT; //it is a pain to work with return initBits(T, out, out_count), + else => |e| return e, + }; + + switch (endian) { + .big => { + if (U == u8) out = 0 else out <<= 8; //shifting u8 by 8 is illegal in Zig + out |= byte; + }, + .little => { + const pos = @as(U, byte) << @intCast(out_count); + out |= pos; + }, + } + out_count += 8; + } + + const bits_left = num - out_count; + const keep = 8 - bits_left; + + if (bits_left == 0) return initBits(T, out, out_count); + + const final_byte = self.reader.readByte() catch |err| switch (err) { + error.EndOfStream => return initBits(T, out, out_count), + else => |e| return e, + }; + + switch (endian) { + .big => { + out <<= @intCast(bits_left); + out |= final_byte >> @intCast(keep); + self.bits = final_byte & low_bit_mask[keep]; + }, + .little => { + const pos = @as(U, final_byte & low_bit_mask[bits_left]) << @intCast(out_count); + out |= pos; + self.bits = final_byte >> @intCast(bits_left); + }, + } + + self.count = @intCast(keep); + return initBits(T, out, num); + } + + //convenience function for removing bits from + //the appropriate part of the buffer based on + //endianess. + fn removeBits(self: *@This(), num: u4) u8 { + if (num == 8) { + self.count = 0; + return self.bits; + } + + const keep = self.count - num; + const bits = switch (endian) { + .big => self.bits >> @intCast(keep), + .little => self.bits & low_bit_mask[num], + }; + switch (endian) { + .big => self.bits &= low_bit_mask[keep], + .little => self.bits >>= @intCast(num), + } + + self.count = keep; + return bits; + } + + pub fn alignToByte(self: *@This()) void { + self.bits = 0; + self.count = 0; + } + }; +} + +pub fn bitReader(comptime endian: std.builtin.Endian, reader: anytype) BitReader(endian, @TypeOf(reader)) { + return .{ .reader = reader }; +} + +/////////////////////////////// + +test "api coverage" { + const mem_be = [_]u8{ 0b11001101, 0b00001011 }; + const mem_le = [_]u8{ 0b00011101, 0b10010101 }; + + var mem_in_be = std.io.fixedBufferStream(&mem_be); + var bit_stream_be = bitReader(.big, mem_in_be.reader()); + + var out_bits: u16 = undefined; + + const expect = std.testing.expect; + const expectError = std.testing.expectError; + + try expect(1 == try bit_stream_be.readBits(u2, 1, &out_bits)); + try expect(out_bits == 1); + try expect(2 == try bit_stream_be.readBits(u5, 2, &out_bits)); + try expect(out_bits == 2); + try expect(3 == try bit_stream_be.readBits(u128, 3, &out_bits)); + try expect(out_bits == 3); + try expect(4 == try bit_stream_be.readBits(u8, 4, &out_bits)); + try expect(out_bits == 4); + try expect(5 == try bit_stream_be.readBits(u9, 5, &out_bits)); + try expect(out_bits == 5); + try expect(1 == try bit_stream_be.readBits(u1, 1, &out_bits)); + try expect(out_bits == 1); + + mem_in_be.pos = 0; + bit_stream_be.count = 0; + try expect(0b110011010000101 == try bit_stream_be.readBits(u15, 15, &out_bits)); + try expect(out_bits == 15); + + mem_in_be.pos = 0; + bit_stream_be.count = 0; + try expect(0b1100110100001011 == try bit_stream_be.readBits(u16, 16, &out_bits)); + try expect(out_bits == 16); + + _ = try bit_stream_be.readBits(u0, 0, &out_bits); + + try expect(0 == try bit_stream_be.readBits(u1, 1, &out_bits)); + try expect(out_bits == 0); + try expectError(error.EndOfStream, bit_stream_be.readBitsNoEof(u1, 1)); + + var mem_in_le = std.io.fixedBufferStream(&mem_le); + var bit_stream_le = bitReader(.little, mem_in_le.reader()); + + try expect(1 == try bit_stream_le.readBits(u2, 1, &out_bits)); + try expect(out_bits == 1); + try expect(2 == try bit_stream_le.readBits(u5, 2, &out_bits)); + try expect(out_bits == 2); + try expect(3 == try bit_stream_le.readBits(u128, 3, &out_bits)); + try expect(out_bits == 3); + try expect(4 == try bit_stream_le.readBits(u8, 4, &out_bits)); + try expect(out_bits == 4); + try expect(5 == try bit_stream_le.readBits(u9, 5, &out_bits)); + try expect(out_bits == 5); + try expect(1 == try bit_stream_le.readBits(u1, 1, &out_bits)); + try expect(out_bits == 1); + + mem_in_le.pos = 0; + bit_stream_le.count = 0; + try expect(0b001010100011101 == try bit_stream_le.readBits(u15, 15, &out_bits)); + try expect(out_bits == 15); + + mem_in_le.pos = 0; + bit_stream_le.count = 0; + try expect(0b1001010100011101 == try bit_stream_le.readBits(u16, 16, &out_bits)); + try expect(out_bits == 16); + + _ = try bit_stream_le.readBits(u0, 0, &out_bits); + + try expect(0 == try bit_stream_le.readBits(u1, 1, &out_bits)); + try expect(out_bits == 0); + try expectError(error.EndOfStream, bit_stream_le.readBitsNoEof(u1, 1)); +} diff --git a/lib/std/Io/bit_writer.zig b/lib/std/Io/bit_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..eef0ece81b437e244b9815bc79e4c80ceeb69ed7 --- /dev/null +++ b/lib/std/Io/bit_writer.zig @@ -0,0 +1,179 @@ +const std = @import("../std.zig"); + +//General note on endianess: +//Big endian is packed starting in the most significant part of the byte and subsequent +// bytes contain less significant bits. Thus we write out bits from the high end +// of our input first. +//Little endian is packed starting in the least significant part of the byte and +// subsequent bytes contain more significant bits. Thus we write out bits from +// the low end of our input first. +//Regardless of endianess, within any given byte the bits are always in most +// to least significant order. +//Also regardless of endianess, the buffer always aligns bits to the low end +// of the byte. + +/// Creates a bit writer which allows for writing bits to an underlying standard writer +pub fn BitWriter(comptime endian: std.builtin.Endian, comptime Writer: type) type { + return struct { + writer: Writer, + bits: u8 = 0, + count: u4 = 0, + + const low_bit_mask = [9]u8{ + 0b00000000, + 0b00000001, + 0b00000011, + 0b00000111, + 0b00001111, + 0b00011111, + 0b00111111, + 0b01111111, + 0b11111111, + }; + + /// Write the specified number of bits to the writer from the least significant bits of + /// the specified value. Bits will only be written to the writer when there + /// are enough to fill a byte. + pub fn writeBits(self: *@This(), value: anytype, num: u16) !void { + const T = @TypeOf(value); + const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); + const U = if (@bitSizeOf(T) < 8) u8 else UT; // 0) { + //if we can't fill the buffer, add what we have + const bits_free = 8 - self.count; + if (num < bits_free) { + self.addBits(@truncate(in), @intCast(num)); + return; + } + + //finish filling the buffer and flush it + if (num == bits_free) { + self.addBits(@truncate(in), @intCast(num)); + return self.flushBits(); + } + + switch (endian) { + .big => { + const bits = in >> @intCast(in_count - bits_free); + self.addBits(@truncate(bits), bits_free); + }, + .little => { + self.addBits(@truncate(in), bits_free); + in >>= @intCast(bits_free); + }, + } + in_count -= bits_free; + try self.flushBits(); + } + + //write full bytes while we can + const full_bytes_left = in_count / 8; + for (0..full_bytes_left) |_| { + switch (endian) { + .big => { + const bits = in >> @intCast(in_count - 8); + try self.writer.writeByte(@truncate(bits)); + }, + .little => { + try self.writer.writeByte(@truncate(in)); + if (U == u8) in = 0 else in >>= 8; + }, + } + in_count -= 8; + } + + //save the remaining bits in the buffer + self.addBits(@truncate(in), @intCast(in_count)); + } + + //convenience funciton for adding bits to the buffer + //in the appropriate position based on endianess + fn addBits(self: *@This(), bits: u8, num: u4) void { + if (num == 8) self.bits = bits else switch (endian) { + .big => { + self.bits <<= @intCast(num); + self.bits |= bits & low_bit_mask[num]; + }, + .little => { + const pos = bits << @intCast(self.count); + self.bits |= pos; + }, + } + self.count += num; + } + + /// Flush any remaining bits to the writer, filling + /// unused bits with 0s. + pub fn flushBits(self: *@This()) !void { + if (self.count == 0) return; + if (endian == .big) self.bits <<= @intCast(8 - self.count); + try self.writer.writeByte(self.bits); + self.bits = 0; + self.count = 0; + } + }; +} + +pub fn bitWriter(comptime endian: std.builtin.Endian, writer: anytype) BitWriter(endian, @TypeOf(writer)) { + return .{ .writer = writer }; +} + +/////////////////////////////// + +test "api coverage" { + var mem_be = [_]u8{0} ** 2; + var mem_le = [_]u8{0} ** 2; + + var mem_out_be = std.io.fixedBufferStream(&mem_be); + var bit_stream_be = bitWriter(.big, mem_out_be.writer()); + + const testing = std.testing; + + try bit_stream_be.writeBits(@as(u2, 1), 1); + try bit_stream_be.writeBits(@as(u5, 2), 2); + try bit_stream_be.writeBits(@as(u128, 3), 3); + try bit_stream_be.writeBits(@as(u8, 4), 4); + try bit_stream_be.writeBits(@as(u9, 5), 5); + try bit_stream_be.writeBits(@as(u1, 1), 1); + + try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001011); + + mem_out_be.pos = 0; + + try bit_stream_be.writeBits(@as(u15, 0b110011010000101), 15); + try bit_stream_be.flushBits(); + try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001010); + + mem_out_be.pos = 0; + try bit_stream_be.writeBits(@as(u32, 0b110011010000101), 16); + try testing.expect(mem_be[0] == 0b01100110 and mem_be[1] == 0b10000101); + + try bit_stream_be.writeBits(@as(u0, 0), 0); + + var mem_out_le = std.io.fixedBufferStream(&mem_le); + var bit_stream_le = bitWriter(.little, mem_out_le.writer()); + + try bit_stream_le.writeBits(@as(u2, 1), 1); + try bit_stream_le.writeBits(@as(u5, 2), 2); + try bit_stream_le.writeBits(@as(u128, 3), 3); + try bit_stream_le.writeBits(@as(u8, 4), 4); + try bit_stream_le.writeBits(@as(u9, 5), 5); + try bit_stream_le.writeBits(@as(u1, 1), 1); + + try testing.expect(mem_le[0] == 0b00011101 and mem_le[1] == 0b10010101); + + mem_out_le.pos = 0; + try bit_stream_le.writeBits(@as(u15, 0b110011010000101), 15); + try bit_stream_le.flushBits(); + try testing.expect(mem_le[0] == 0b10000101 and mem_le[1] == 0b01100110); + + mem_out_le.pos = 0; + try bit_stream_le.writeBits(@as(u32, 0b1100110100001011), 16); + try testing.expect(mem_le[0] == 0b00001011 and mem_le[1] == 0b11001101); + + try bit_stream_le.writeBits(@as(u0, 0), 0); +} diff --git a/lib/std/Io/buffered_atomic_file.zig b/lib/std/Io/buffered_atomic_file.zig new file mode 100644 index 0000000000000000000000000000000000000000..48510bde52a2b097677316c4f287f56d53d88393 --- /dev/null +++ b/lib/std/Io/buffered_atomic_file.zig @@ -0,0 +1,55 @@ +const std = @import("../std.zig"); +const mem = std.mem; +const fs = std.fs; +const File = std.fs.File; + +pub const BufferedAtomicFile = struct { + atomic_file: fs.AtomicFile, + file_writer: File.Writer, + buffered_writer: BufferedWriter, + allocator: mem.Allocator, + + pub const buffer_size = 4096; + pub const BufferedWriter = std.io.BufferedWriter(buffer_size, File.Writer); + 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 + pub fn create( + allocator: mem.Allocator, + dir: fs.Dir, + dest_path: []const u8, + atomic_file_options: fs.Dir.AtomicFileOptions, + ) !*BufferedAtomicFile { + var self = try allocator.create(BufferedAtomicFile); + self.* = BufferedAtomicFile{ + .atomic_file = undefined, + .file_writer = undefined, + .buffered_writer = undefined, + .allocator = allocator, + }; + errdefer allocator.destroy(self); + + self.atomic_file = try dir.atomicFile(dest_path, atomic_file_options); + errdefer self.atomic_file.deinit(); + + self.file_writer = self.atomic_file.file.deprecatedWriter(); + self.buffered_writer = .{ .unbuffered_writer = self.file_writer }; + return self; + } + + /// always call destroy, even after successful finish() + pub fn destroy(self: *BufferedAtomicFile) void { + self.atomic_file.deinit(); + self.allocator.destroy(self); + } + + pub fn finish(self: *BufferedAtomicFile) !void { + try self.buffered_writer.flush(); + try self.atomic_file.finish(); + } + + pub fn writer(self: *BufferedAtomicFile) Writer { + return .{ .context = &self.buffered_writer }; + } +}; diff --git a/lib/std/Io/buffered_reader.zig b/lib/std/Io/buffered_reader.zig new file mode 100644 index 0000000000000000000000000000000000000000..548dd92f736238be712549aadd065dd6d36c8cba --- /dev/null +++ b/lib/std/Io/buffered_reader.zig @@ -0,0 +1,201 @@ +const std = @import("../std.zig"); +const io = std.io; +const mem = std.mem; +const assert = std.debug.assert; +const testing = std.testing; + +pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) type { + return struct { + unbuffered_reader: ReaderType, + buf: [buffer_size]u8 = undefined, + start: usize = 0, + end: usize = 0, + + pub const Error = ReaderType.Error; + pub const Reader = io.GenericReader(*Self, Error, read); + + const Self = @This(); + + pub fn read(self: *Self, dest: []u8) Error!usize { + // First try reading from the already buffered data onto the destination. + const current = self.buf[self.start..self.end]; + if (current.len != 0) { + const to_transfer = @min(current.len, dest.len); + @memcpy(dest[0..to_transfer], current[0..to_transfer]); + self.start += to_transfer; + return to_transfer; + } + + // If dest is large, read from the unbuffered reader directly into the destination. + if (dest.len >= buffer_size) { + return self.unbuffered_reader.read(dest); + } + + // If dest is small, read from the unbuffered reader into our own internal buffer, + // and then transfer to destination. + self.end = try self.unbuffered_reader.read(&self.buf); + const to_transfer = @min(self.end, dest.len); + @memcpy(dest[0..to_transfer], self.buf[0..to_transfer]); + self.start = to_transfer; + return to_transfer; + } + + pub fn reader(self: *Self) Reader { + return .{ .context = self }; + } + }; +} + +pub fn bufferedReader(reader: anytype) BufferedReader(4096, @TypeOf(reader)) { + return .{ .unbuffered_reader = reader }; +} + +pub fn bufferedReaderSize(comptime size: usize, reader: anytype) BufferedReader(size, @TypeOf(reader)) { + return .{ .unbuffered_reader = reader }; +} + +test "OneByte" { + const OneByteReadReader = struct { + str: []const u8, + curr: usize, + + const Error = error{NoError}; + const Self = @This(); + const Reader = io.GenericReader(*Self, Error, read); + + fn init(str: []const u8) Self { + return Self{ + .str = str, + .curr = 0, + }; + } + + fn read(self: *Self, dest: []u8) Error!usize { + if (self.str.len <= self.curr or dest.len == 0) + return 0; + + dest[0] = self.str[self.curr]; + self.curr += 1; + return 1; + } + + fn reader(self: *Self) Reader { + return .{ .context = self }; + } + }; + + const str = "This is a test"; + var one_byte_stream = OneByteReadReader.init(str); + var buf_reader = bufferedReader(one_byte_stream.reader()); + const stream = buf_reader.reader(); + + const res = try stream.readAllAlloc(testing.allocator, str.len + 1); + defer testing.allocator.free(res); + try testing.expectEqualSlices(u8, str, res); +} + +fn smallBufferedReader(underlying_stream: anytype) BufferedReader(8, @TypeOf(underlying_stream)) { + return .{ .unbuffered_reader = underlying_stream }; +} +test "Block" { + const BlockReader = struct { + block: []const u8, + reads_allowed: usize, + curr_read: usize, + + const Error = error{NoError}; + const Self = @This(); + const Reader = io.GenericReader(*Self, Error, read); + + fn init(block: []const u8, reads_allowed: usize) Self { + return Self{ + .block = block, + .reads_allowed = reads_allowed, + .curr_read = 0, + }; + } + + fn read(self: *Self, dest: []u8) Error!usize { + if (self.curr_read >= self.reads_allowed) return 0; + @memcpy(dest[0..self.block.len], self.block); + + self.curr_read += 1; + return self.block.len; + } + + fn reader(self: *Self) Reader { + return .{ .context = self }; + } + }; + + const block = "0123"; + + // len out == block + { + var test_buf_reader: BufferedReader(4, BlockReader) = .{ + .unbuffered_reader = BlockReader.init(block, 2), + }; + const reader = test_buf_reader.reader(); + var out_buf: [4]u8 = undefined; + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, block); + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, block); + try testing.expectEqual(try reader.readAll(&out_buf), 0); + } + + // len out < block + { + var test_buf_reader: BufferedReader(4, BlockReader) = .{ + .unbuffered_reader = BlockReader.init(block, 2), + }; + const reader = test_buf_reader.reader(); + var out_buf: [3]u8 = undefined; + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, "012"); + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, "301"); + const n = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, out_buf[0..n], "23"); + try testing.expectEqual(try reader.readAll(&out_buf), 0); + } + + // len out > block + { + var test_buf_reader: BufferedReader(4, BlockReader) = .{ + .unbuffered_reader = BlockReader.init(block, 2), + }; + const reader = test_buf_reader.reader(); + var out_buf: [5]u8 = undefined; + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, "01230"); + const n = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, out_buf[0..n], "123"); + try testing.expectEqual(try reader.readAll(&out_buf), 0); + } + + // len out == 0 + { + var test_buf_reader: BufferedReader(4, BlockReader) = .{ + .unbuffered_reader = BlockReader.init(block, 2), + }; + const reader = test_buf_reader.reader(); + var out_buf: [0]u8 = undefined; + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, ""); + } + + // len bufreader buf > block + { + var test_buf_reader: BufferedReader(5, BlockReader) = .{ + .unbuffered_reader = BlockReader.init(block, 2), + }; + const reader = test_buf_reader.reader(); + var out_buf: [4]u8 = undefined; + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, block); + _ = try reader.readAll(&out_buf); + try testing.expectEqualSlices(u8, &out_buf, block); + try testing.expectEqual(try reader.readAll(&out_buf), 0); + } +} diff --git a/lib/std/Io/buffered_writer.zig b/lib/std/Io/buffered_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..ef95de0f0ce83d831ace24f433f95c349bc19fdb --- /dev/null +++ b/lib/std/Io/buffered_writer.zig @@ -0,0 +1,43 @@ +const std = @import("../std.zig"); + +const io = std.io; +const mem = std.mem; + +pub fn BufferedWriter(comptime buffer_size: usize, comptime WriterType: type) type { + return struct { + unbuffered_writer: WriterType, + buf: [buffer_size]u8 = undefined, + end: usize = 0, + + pub const Error = WriterType.Error; + pub const Writer = io.GenericWriter(*Self, Error, write); + + const Self = @This(); + + pub fn flush(self: *Self) !void { + try self.unbuffered_writer.writeAll(self.buf[0..self.end]); + self.end = 0; + } + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + pub fn write(self: *Self, bytes: []const u8) Error!usize { + if (self.end + bytes.len > self.buf.len) { + try self.flush(); + if (bytes.len > self.buf.len) + return self.unbuffered_writer.write(bytes); + } + + const new_end = self.end + bytes.len; + @memcpy(self.buf[self.end..new_end], bytes); + self.end = new_end; + return bytes.len; + } + }; +} + +pub fn bufferedWriter(underlying_stream: anytype) BufferedWriter(4096, @TypeOf(underlying_stream)) { + return .{ .unbuffered_writer = underlying_stream }; +} diff --git a/lib/std/Io/c_writer.zig b/lib/std/Io/c_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..30d0cabcf5145b692eb77ac9ad9c2dcf8b4158c1 --- /dev/null +++ b/lib/std/Io/c_writer.zig @@ -0,0 +1,44 @@ +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const io = std.io; +const testing = std.testing; + +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 }; +} + +fn cWriterWrite(c_file: *std.c.FILE, bytes: []const u8) std.fs.File.WriteError!usize { + const amt_written = std.c.fwrite(bytes.ptr, 1, bytes.len, c_file); + if (amt_written >= 0) return amt_written; + switch (@as(std.c.E, @enumFromInt(std.c._errno().*))) { + .SUCCESS => unreachable, + .INVAL => unreachable, + .FAULT => unreachable, + .AGAIN => unreachable, // this is a blocking API + .BADF => unreachable, // always a race condition + .DESTADDRREQ => unreachable, // connect was never called + .DQUOT => return error.DiskQuota, + .FBIG => return error.FileTooBig, + .IO => return error.InputOutput, + .NOSPC => return error.NoSpaceLeft, + .PERM => return error.PermissionDenied, + .PIPE => return error.BrokenPipe, + else => |err| return std.posix.unexpectedErrno(err), + } +} + +test cWriter { + if (!builtin.link_libc or builtin.os.tag == .wasi) return error.SkipZigTest; + + const filename = "tmp_io_test_file.txt"; + const out_file = std.c.fopen(filename, "w") orelse return error.UnableToOpenTestFile; + defer { + _ = std.c.fclose(out_file); + std.fs.cwd().deleteFileZ(filename) catch {}; + } + + const writer = cWriter(out_file); + try writer.print("hi: {}\n", .{@as(i32, 123)}); +} diff --git a/lib/std/Io/change_detection_stream.zig b/lib/std/Io/change_detection_stream.zig new file mode 100644 index 0000000000000000000000000000000000000000..d9da1c4a0eb0d0a934a00cf882cccf19b88c35a7 --- /dev/null +++ b/lib/std/Io/change_detection_stream.zig @@ -0,0 +1,55 @@ +const std = @import("../std.zig"); +const io = std.io; +const mem = std.mem; +const assert = std.debug.assert; + +/// Used to detect if the data written to a stream differs from a source buffer +pub fn ChangeDetectionStream(comptime WriterType: type) type { + return struct { + const Self = @This(); + pub const Error = WriterType.Error; + pub const Writer = io.GenericWriter(*Self, Error, write); + + anything_changed: bool, + underlying_writer: WriterType, + source_index: usize, + source: []const u8, + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + fn write(self: *Self, bytes: []const u8) Error!usize { + if (!self.anything_changed) { + const end = self.source_index + bytes.len; + if (end > self.source.len) { + self.anything_changed = true; + } else { + const src_slice = self.source[self.source_index..end]; + self.source_index += bytes.len; + if (!mem.eql(u8, bytes, src_slice)) { + self.anything_changed = true; + } + } + } + + return self.underlying_writer.write(bytes); + } + + pub fn changeDetected(self: *Self) bool { + return self.anything_changed or (self.source_index != self.source.len); + } + }; +} + +pub fn changeDetectionStream( + source: []const u8, + underlying_writer: anytype, +) ChangeDetectionStream(@TypeOf(underlying_writer)) { + return ChangeDetectionStream(@TypeOf(underlying_writer)){ + .anything_changed = false, + .underlying_writer = underlying_writer, + .source_index = 0, + .source = source, + }; +} diff --git a/lib/std/Io/counting_reader.zig b/lib/std/Io/counting_reader.zig new file mode 100644 index 0000000000000000000000000000000000000000..bc1e1b6ec72433a4c19ba8ae618864831b1648c9 --- /dev/null +++ b/lib/std/Io/counting_reader.zig @@ -0,0 +1,43 @@ +const std = @import("../std.zig"); +const io = std.io; +const testing = std.testing; + +/// A Reader that counts how many bytes has been read from it. +pub fn CountingReader(comptime ReaderType: anytype) type { + return struct { + child_reader: ReaderType, + bytes_read: u64 = 0, + + pub const Error = ReaderType.Error; + 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); + self.bytes_read += amt; + return amt; + } + + pub fn reader(self: *@This()) Reader { + return .{ .context = self }; + } + }; +} + +pub fn countingReader(reader: anytype) CountingReader(@TypeOf(reader)) { + return .{ .child_reader = reader }; +} + +test CountingReader { + const bytes = "yay" ** 100; + var fbs = io.fixedBufferStream(bytes); + + var counting_stream = countingReader(fbs.reader()); + const stream = counting_stream.reader(); + + //read and discard all bytes + while (stream.readByte()) |_| {} else |err| { + try testing.expect(err == error.EndOfStream); + } + + try testing.expect(counting_stream.bytes_read == bytes.len); +} diff --git a/lib/std/Io/counting_writer.zig b/lib/std/Io/counting_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..32c3ed930fcaaa69709f6abc37f3523b325a85aa --- /dev/null +++ b/lib/std/Io/counting_writer.zig @@ -0,0 +1,39 @@ +const std = @import("../std.zig"); +const io = std.io; +const testing = std.testing; + +/// A Writer that counts how many bytes has been written to it. +pub fn CountingWriter(comptime WriterType: type) type { + return struct { + bytes_written: u64, + child_stream: WriterType, + + pub const Error = WriterType.Error; + pub const Writer = io.GenericWriter(*Self, Error, write); + + const Self = @This(); + + pub fn write(self: *Self, bytes: []const u8) Error!usize { + const amt = try self.child_stream.write(bytes); + self.bytes_written += amt; + return amt; + } + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + }; +} + +pub fn countingWriter(child_stream: anytype) CountingWriter(@TypeOf(child_stream)) { + return .{ .bytes_written = 0, .child_stream = child_stream }; +} + +test CountingWriter { + var counting_stream = countingWriter(std.io.null_writer); + const stream = counting_stream.writer(); + + const bytes = "yay" ** 100; + stream.writeAll(bytes) catch unreachable; + try testing.expect(counting_stream.bytes_written == bytes.len); +} diff --git a/lib/std/Io/find_byte_writer.zig b/lib/std/Io/find_byte_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..fe6836f6037bb1fcb8381c0cfbb0cebc15573a93 --- /dev/null +++ b/lib/std/Io/find_byte_writer.zig @@ -0,0 +1,40 @@ +const std = @import("../std.zig"); +const io = std.io; +const assert = std.debug.assert; + +/// A Writer that returns whether the given character has been written to it. +/// The contents are not written to anything. +pub fn FindByteWriter(comptime UnderlyingWriter: type) type { + return struct { + const Self = @This(); + pub const Error = UnderlyingWriter.Error; + pub const Writer = io.GenericWriter(*Self, Error, write); + + underlying_writer: UnderlyingWriter, + byte_found: bool, + byte: u8, + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + fn write(self: *Self, bytes: []const u8) Error!usize { + if (!self.byte_found) { + self.byte_found = blk: { + for (bytes) |b| + if (b == self.byte) break :blk true; + break :blk false; + }; + } + return self.underlying_writer.write(bytes); + } + }; +} + +pub fn findByteWriter(byte: u8, underlying_writer: anytype) FindByteWriter(@TypeOf(underlying_writer)) { + return FindByteWriter(@TypeOf(underlying_writer)){ + .underlying_writer = underlying_writer, + .byte = byte, + .byte_found = false, + }; +} diff --git a/lib/std/Io/fixed_buffer_stream.zig b/lib/std/Io/fixed_buffer_stream.zig new file mode 100644 index 0000000000000000000000000000000000000000..67d6f3d286381db34eb92c80358523e184a500c7 --- /dev/null +++ b/lib/std/Io/fixed_buffer_stream.zig @@ -0,0 +1,198 @@ +const std = @import("../std.zig"); +const io = std.io; +const testing = std.testing; +const mem = std.mem; +const assert = std.debug.assert; + +/// 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`. + buffer: Buffer, + pos: usize, + + pub const ReadError = error{}; + pub const WriteError = error{NoSpaceLeft}; + pub const SeekError = error{}; + pub const GetSeekPosError = error{}; + + pub const Reader = io.GenericReader(*Self, ReadError, read); + pub const Writer = io.GenericWriter(*Self, WriteError, write); + + pub const SeekableStream = io.SeekableStream( + *Self, + SeekError, + GetSeekPosError, + seekTo, + seekBy, + getPos, + getEndPos, + ); + + const Self = @This(); + + pub fn reader(self: *Self) Reader { + return .{ .context = self }; + } + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + pub fn seekableStream(self: *Self) SeekableStream { + return .{ .context = self }; + } + + pub fn read(self: *Self, dest: []u8) ReadError!usize { + const size = @min(dest.len, self.buffer.len - self.pos); + const end = self.pos + size; + + @memcpy(dest[0..size], self.buffer[self.pos..end]); + self.pos = end; + + return size; + } + + /// If the returned number of bytes written is less than requested, the + /// buffer is full. Returns `error.NoSpaceLeft` when no bytes would be written. + /// Note: `error.NoSpaceLeft` matches the corresponding error from + /// `std.fs.File.WriteError`. + pub fn write(self: *Self, bytes: []const u8) WriteError!usize { + if (bytes.len == 0) return 0; + if (self.pos >= self.buffer.len) return error.NoSpaceLeft; + + const n = @min(self.buffer.len - self.pos, bytes.len); + @memcpy(self.buffer[self.pos..][0..n], bytes[0..n]); + self.pos += n; + + if (n == 0) return error.NoSpaceLeft; + + return n; + } + + pub fn seekTo(self: *Self, pos: u64) SeekError!void { + self.pos = @min(std.math.lossyCast(usize, pos), self.buffer.len); + } + + pub fn seekBy(self: *Self, amt: i64) SeekError!void { + if (amt < 0) { + const abs_amt = @abs(amt); + const abs_amt_usize = std.math.cast(usize, abs_amt) orelse std.math.maxInt(usize); + if (abs_amt_usize > self.pos) { + self.pos = 0; + } else { + self.pos -= abs_amt_usize; + } + } else { + const amt_usize = std.math.cast(usize, amt) orelse std.math.maxInt(usize); + const new_pos = std.math.add(usize, self.pos, amt_usize) catch std.math.maxInt(usize); + self.pos = @min(self.buffer.len, new_pos); + } + } + + pub fn getEndPos(self: *Self) GetSeekPosError!u64 { + return self.buffer.len; + } + + pub fn getPos(self: *Self) GetSeekPosError!u64 { + return self.pos; + } + + pub fn getWritten(self: Self) Buffer { + return self.buffer[0..self.pos]; + } + + pub fn reset(self: *Self) void { + self.pos = 0; + } + }; +} + +pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) { + return .{ .buffer = buffer, .pos = 0 }; +} + +fn Slice(comptime T: type) type { + switch (@typeInfo(T)) { + .pointer => |ptr_info| { + var new_ptr_info = ptr_info; + switch (ptr_info.size) { + .slice => {}, + .one => switch (@typeInfo(ptr_info.child)) { + .array => |info| new_ptr_info.child = info.child, + else => @compileError("invalid type given to fixedBufferStream"), + }, + else => @compileError("invalid type given to fixedBufferStream"), + } + new_ptr_info.size = .slice; + return @Type(.{ .pointer = new_ptr_info }); + }, + else => @compileError("invalid type given to fixedBufferStream"), + } +} + +test "output" { + var buf: [255]u8 = undefined; + var fbs = fixedBufferStream(&buf); + const stream = fbs.writer(); + + try stream.print("{s}{s}!", .{ "Hello", "World" }); + try testing.expectEqualSlices(u8, "HelloWorld!", fbs.getWritten()); +} + +test "output at comptime" { + comptime { + var buf: [255]u8 = undefined; + var fbs = fixedBufferStream(&buf); + const stream = fbs.writer(); + + try stream.print("{s}{s}!", .{ "Hello", "World" }); + try testing.expectEqualSlices(u8, "HelloWorld!", fbs.getWritten()); + } +} + +test "output 2" { + var buffer: [10]u8 = undefined; + var fbs = fixedBufferStream(&buffer); + + try fbs.writer().writeAll("Hello"); + try testing.expect(mem.eql(u8, fbs.getWritten(), "Hello")); + + try fbs.writer().writeAll("world"); + try testing.expect(mem.eql(u8, fbs.getWritten(), "Helloworld")); + + try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("!")); + try testing.expect(mem.eql(u8, fbs.getWritten(), "Helloworld")); + + fbs.reset(); + try testing.expect(fbs.getWritten().len == 0); + + try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("Hello world!")); + try testing.expect(mem.eql(u8, fbs.getWritten(), "Hello worl")); + + try fbs.seekTo((try fbs.getEndPos()) + 1); + try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("H")); +} + +test "input" { + const bytes = [_]u8{ 1, 2, 3, 4, 5, 6, 7 }; + var fbs = fixedBufferStream(&bytes); + + var dest: [4]u8 = undefined; + + var read = try fbs.reader().read(&dest); + try testing.expect(read == 4); + try testing.expect(mem.eql(u8, dest[0..4], bytes[0..4])); + + read = try fbs.reader().read(&dest); + try testing.expect(read == 3); + try testing.expect(mem.eql(u8, dest[0..3], bytes[4..7])); + + read = try fbs.reader().read(&dest); + try testing.expect(read == 0); + + try fbs.seekTo((try fbs.getEndPos()) + 1); + read = try fbs.reader().read(&dest); + try testing.expect(read == 0); +} diff --git a/lib/std/Io/limited_reader.zig b/lib/std/Io/limited_reader.zig new file mode 100644 index 0000000000000000000000000000000000000000..b6b555f76deca749d492879f00af9f80b579b372 --- /dev/null +++ b/lib/std/Io/limited_reader.zig @@ -0,0 +1,45 @@ +const std = @import("../std.zig"); +const io = std.io; +const assert = std.debug.assert; +const testing = std.testing; + +pub fn LimitedReader(comptime ReaderType: type) type { + return struct { + inner_reader: ReaderType, + bytes_left: u64, + + pub const Error = ReaderType.Error; + pub const Reader = io.GenericReader(*Self, Error, read); + + const Self = @This(); + + pub fn read(self: *Self, dest: []u8) Error!usize { + const max_read = @min(self.bytes_left, dest.len); + const n = try self.inner_reader.read(dest[0..max_read]); + self.bytes_left -= n; + return n; + } + + pub fn reader(self: *Self) Reader { + return .{ .context = self }; + } + }; +} + +/// Returns an initialised `LimitedReader`. +/// `bytes_left` is a `u64` to be able to take 64 bit file offsets +pub fn limitedReader(inner_reader: anytype, bytes_left: u64) LimitedReader(@TypeOf(inner_reader)) { + return .{ .inner_reader = inner_reader, .bytes_left = bytes_left }; +} + +test "basic usage" { + const data = "hello world"; + var fbs = std.io.fixedBufferStream(data); + var early_stream = limitedReader(fbs.reader(), 3); + + var buf: [5]u8 = undefined; + try testing.expectEqual(@as(usize, 3), try early_stream.reader().read(&buf)); + try testing.expectEqualSlices(u8, data[0..3], buf[0..3]); + try testing.expectEqual(@as(usize, 0), try early_stream.reader().read(&buf)); + try testing.expectError(error.EndOfStream, early_stream.reader().skipBytes(10, .{})); +} diff --git a/lib/std/Io/multi_writer.zig b/lib/std/Io/multi_writer.zig new file mode 100644 index 0000000000000000000000000000000000000000..20e9e782de9d02999fe889c659985d70fe1d1508 --- /dev/null +++ b/lib/std/Io/multi_writer.zig @@ -0,0 +1,53 @@ +const std = @import("../std.zig"); +const io = std.io; + +/// Takes a tuple of streams, and constructs a new stream that writes to all of them +pub fn MultiWriter(comptime Writers: type) type { + comptime var ErrSet = error{}; + inline for (@typeInfo(Writers).@"struct".fields) |field| { + const StreamType = field.type; + ErrSet = ErrSet || StreamType.Error; + } + + return struct { + const Self = @This(); + + streams: Writers, + + pub const Error = ErrSet; + pub const Writer = io.GenericWriter(*Self, Error, write); + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + pub fn write(self: *Self, bytes: []const u8) Error!usize { + inline for (self.streams) |stream| + try stream.writeAll(bytes); + return bytes.len; + } + }; +} + +pub fn multiWriter(streams: anytype) MultiWriter(@TypeOf(streams)) { + return .{ .streams = streams }; +} + +const testing = std.testing; + +test "MultiWriter" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var f = try tmp.dir.createFile("t.txt", .{}); + + var buf1: [255]u8 = undefined; + var fbs1 = io.fixedBufferStream(&buf1); + var buf2: [255]u8 = undefined; + var stream = multiWriter(.{ fbs1.writer(), f.writer() }); + + try stream.writer().print("HI", .{}); + f.close(); + + try testing.expectEqualSlices(u8, "HI", fbs1.getWritten()); + try testing.expectEqualSlices(u8, "HI", try tmp.dir.readFile("t.txt", &buf2)); +} diff --git a/lib/std/Io/seekable_stream.zig b/lib/std/Io/seekable_stream.zig new file mode 100644 index 0000000000000000000000000000000000000000..1aa653dbe52cc6297c94b0422c35c74b46515b6c --- /dev/null +++ b/lib/std/Io/seekable_stream.zig @@ -0,0 +1,35 @@ +const std = @import("../std.zig"); + +pub fn SeekableStream( + comptime Context: type, + comptime SeekErrorType: type, + comptime GetSeekPosErrorType: type, + comptime seekToFn: fn (context: Context, pos: u64) SeekErrorType!void, + comptime seekByFn: fn (context: Context, pos: i64) SeekErrorType!void, + comptime getPosFn: fn (context: Context) GetSeekPosErrorType!u64, + comptime getEndPosFn: fn (context: Context) GetSeekPosErrorType!u64, +) type { + return struct { + context: Context, + + const Self = @This(); + pub const SeekError = SeekErrorType; + pub const GetSeekPosError = GetSeekPosErrorType; + + pub fn seekTo(self: Self, pos: u64) SeekError!void { + return seekToFn(self.context, pos); + } + + pub fn seekBy(self: Self, amt: i64) SeekError!void { + return seekByFn(self.context, amt); + } + + pub fn getEndPos(self: Self) GetSeekPosError!u64 { + return getEndPosFn(self.context); + } + + pub fn getPos(self: Self) GetSeekPosError!u64 { + return getPosFn(self.context); + } + }; +} diff --git a/lib/std/Io/stream_source.zig b/lib/std/Io/stream_source.zig new file mode 100644 index 0000000000000000000000000000000000000000..2a3527e47934873dc22cc5997286801c4601ffe7 --- /dev/null +++ b/lib/std/Io/stream_source.zig @@ -0,0 +1,127 @@ +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const io = std.io; + +/// 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.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 + const has_file = (builtin.os.tag != .freestanding and builtin.os.tag != .uefi); + + /// The stream access is redirected to this buffer. + buffer: io.FixedBufferStream([]u8), + + /// The stream access is redirected to this buffer. + /// Writing to the source will always yield `error.AccessDenied`. + const_buffer: io.FixedBufferStream([]const u8), + + /// The stream access is redirected to this file. + /// On freestanding, this must never be initialized! + file: if (has_file) std.fs.File else void, + + pub const ReadError = io.FixedBufferStream([]u8).ReadError || (if (has_file) std.fs.File.ReadError else error{}); + pub const WriteError = error{AccessDenied} || io.FixedBufferStream([]u8).WriteError || (if (has_file) std.fs.File.WriteError else error{}); + 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.GenericReader(*StreamSource, ReadError, read); + pub const Writer = io.GenericWriter(*StreamSource, WriteError, write); + pub const SeekableStream = io.SeekableStream( + *StreamSource, + SeekError, + GetSeekPosError, + seekTo, + seekBy, + getPos, + getEndPos, + ); + + pub fn read(self: *StreamSource, dest: []u8) ReadError!usize { + switch (self.*) { + .buffer => |*x| return x.read(dest), + .const_buffer => |*x| return x.read(dest), + .file => |x| if (!has_file) unreachable else return x.read(dest), + } + } + + pub fn write(self: *StreamSource, bytes: []const u8) WriteError!usize { + switch (self.*) { + .buffer => |*x| return x.write(bytes), + .const_buffer => return error.AccessDenied, + .file => |x| if (!has_file) unreachable else return x.write(bytes), + } + } + + pub fn seekTo(self: *StreamSource, pos: u64) SeekError!void { + switch (self.*) { + .buffer => |*x| return x.seekTo(pos), + .const_buffer => |*x| return x.seekTo(pos), + .file => |x| if (!has_file) unreachable else return x.seekTo(pos), + } + } + + pub fn seekBy(self: *StreamSource, amt: i64) SeekError!void { + switch (self.*) { + .buffer => |*x| return x.seekBy(amt), + .const_buffer => |*x| return x.seekBy(amt), + .file => |x| if (!has_file) unreachable else return x.seekBy(amt), + } + } + + pub fn getEndPos(self: *StreamSource) GetSeekPosError!u64 { + switch (self.*) { + .buffer => |*x| return x.getEndPos(), + .const_buffer => |*x| return x.getEndPos(), + .file => |x| if (!has_file) unreachable else return x.getEndPos(), + } + } + + pub fn getPos(self: *StreamSource) GetSeekPosError!u64 { + switch (self.*) { + .buffer => |*x| return x.getPos(), + .const_buffer => |*x| return x.getPos(), + .file => |x| if (!has_file) unreachable else return x.getPos(), + } + } + + pub fn reader(self: *StreamSource) Reader { + return .{ .context = self }; + } + + pub fn writer(self: *StreamSource) Writer { + return .{ .context = self }; + } + + pub fn seekableStream(self: *StreamSource) SeekableStream { + return .{ .context = self }; + } +}; + +test "refs" { + std.testing.refAllDecls(StreamSource); +} + +test "mutable buffer" { + var buffer: [64]u8 = undefined; + var source = StreamSource{ .buffer = std.io.fixedBufferStream(&buffer) }; + + var writer = source.writer(); + + try writer.writeAll("Hello, World!"); + + try std.testing.expectEqualStrings("Hello, World!", source.buffer.getWritten()); +} + +test "const buffer" { + const buffer: [64]u8 = "Hello, World!".* ++ ([1]u8{0xAA} ** 51); + var source = StreamSource{ .const_buffer = std.io.fixedBufferStream(&buffer) }; + + var reader = source.reader(); + + var dst_buffer: [13]u8 = undefined; + try reader.readNoEof(&dst_buffer); + + try std.testing.expectEqualStrings("Hello, World!", &dst_buffer); +} diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig new file mode 100644 index 0000000000000000000000000000000000000000..bf14f0c24ca15447283e531db5f125680a9767d0 --- /dev/null +++ b/lib/std/Io/test.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const io = std.io; +const DefaultPrng = std.Random.DefaultPrng; +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const mem = std.mem; +const fs = std.fs; +const File = std.fs.File; +const native_endian = @import("builtin").target.cpu.arch.endian(); + +const tmpDir = std.testing.tmpDir; + +test "write a file, read it, then delete it" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + var data: [1024]u8 = undefined; + var prng = DefaultPrng.init(std.testing.random_seed); + const random = prng.random(); + random.bytes(data[0..]); + const tmp_file_name = "temp_test_file.txt"; + { + var file = try tmp.dir.createFile(tmp_file_name, .{}); + defer file.close(); + + var buf_stream = io.bufferedWriter(file.deprecatedWriter()); + const st = buf_stream.writer(); + try st.print("begin", .{}); + try st.writeAll(data[0..]); + try st.print("end", .{}); + try buf_stream.flush(); + } + + { + // Make sure the exclusive flag is honored. + try expectError(File.OpenError.PathAlreadyExists, tmp.dir.createFile(tmp_file_name, .{ .exclusive = true })); + } + + { + var file = try tmp.dir.openFile(tmp_file_name, .{}); + defer file.close(); + + const file_size = try file.getEndPos(); + const expected_file_size: u64 = "begin".len + data.len + "end".len; + try expectEqual(expected_file_size, file_size); + + var buf_stream = io.bufferedReader(file.deprecatedReader()); + const st = buf_stream.reader(); + const contents = try st.readAllAlloc(std.testing.allocator, 2 * 1024); + defer std.testing.allocator.free(contents); + + try expect(mem.eql(u8, contents[0.."begin".len], "begin")); + try expect(mem.eql(u8, contents["begin".len .. contents.len - "end".len], &data)); + try expect(mem.eql(u8, contents[contents.len - "end".len ..], "end")); + } + try tmp.dir.deleteFile(tmp_file_name); +} + +test "BitStreams with File Stream" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_file_name = "temp_test_file.txt"; + { + var file = try tmp.dir.createFile(tmp_file_name, .{}); + defer file.close(); + + var bit_stream = io.bitWriter(native_endian, file.deprecatedWriter()); + + try bit_stream.writeBits(@as(u2, 1), 1); + try bit_stream.writeBits(@as(u5, 2), 2); + try bit_stream.writeBits(@as(u128, 3), 3); + try bit_stream.writeBits(@as(u8, 4), 4); + try bit_stream.writeBits(@as(u9, 5), 5); + try bit_stream.writeBits(@as(u1, 1), 1); + try bit_stream.flushBits(); + } + { + var file = try tmp.dir.openFile(tmp_file_name, .{}); + defer file.close(); + + var bit_stream = io.bitReader(native_endian, file.deprecatedReader()); + + var out_bits: u16 = undefined; + + try expect(1 == try bit_stream.readBits(u2, 1, &out_bits)); + try expect(out_bits == 1); + try expect(2 == try bit_stream.readBits(u5, 2, &out_bits)); + try expect(out_bits == 2); + try expect(3 == try bit_stream.readBits(u128, 3, &out_bits)); + try expect(out_bits == 3); + try expect(4 == try bit_stream.readBits(u8, 4, &out_bits)); + try expect(out_bits == 4); + try expect(5 == try bit_stream.readBits(u9, 5, &out_bits)); + try expect(out_bits == 5); + try expect(1 == try bit_stream.readBits(u1, 1, &out_bits)); + try expect(out_bits == 1); + + try expectError(error.EndOfStream, bit_stream.readBitsNoEof(u1, 1)); + } + try tmp.dir.deleteFile(tmp_file_name); +} + +test "File seek ops" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_file_name = "temp_test_file.txt"; + var file = try tmp.dir.createFile(tmp_file_name, .{}); + defer file.close(); + + try file.writeAll(&([_]u8{0x55} ** 8192)); + + // Seek to the end + try file.seekFromEnd(0); + try expect((try file.getPos()) == try file.getEndPos()); + // Negative delta + try file.seekBy(-4096); + try expect((try file.getPos()) == 4096); + // Positive delta + try file.seekBy(10); + try expect((try file.getPos()) == 4106); + // Absolute position + try file.seekTo(1234); + try expect((try file.getPos()) == 1234); +} + +test "setEndPos" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_file_name = "temp_test_file.txt"; + var file = try tmp.dir.createFile(tmp_file_name, .{}); + defer file.close(); + + // Verify that the file size changes and the file offset is not moved + try std.testing.expect((try file.getEndPos()) == 0); + try std.testing.expect((try file.getPos()) == 0); + try file.setEndPos(8192); + try std.testing.expect((try file.getEndPos()) == 8192); + try std.testing.expect((try file.getPos()) == 0); + try file.seekTo(100); + try file.setEndPos(4096); + try std.testing.expect((try file.getEndPos()) == 4096); + try std.testing.expect((try file.getPos()) == 100); + try file.setEndPos(0); + try std.testing.expect((try file.getEndPos()) == 0); + try std.testing.expect((try file.getPos()) == 100); +} + +test "updateTimes" { + var tmp = tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_file_name = "just_a_temporary_file.txt"; + var file = try tmp.dir.createFile(tmp_file_name, .{ .read = true }); + defer file.close(); + + const stat_old = try file.stat(); + // Set atime and mtime to 5s before + try file.updateTimes( + stat_old.atime - 5 * std.time.ns_per_s, + stat_old.mtime - 5 * std.time.ns_per_s, + ); + const stat_new = try file.stat(); + try expect(stat_new.atime < stat_old.atime); + try expect(stat_new.mtime < stat_old.mtime); +} + +test "GenericReader methods can return error.EndOfStream" { + // https://github.com/ziglang/zig/issues/17733 + var fbs = std.io.fixedBufferStream(""); + try std.testing.expectError( + error.EndOfStream, + fbs.reader().readEnum(enum(u8) { a, b }, .little), + ); + try std.testing.expectError( + error.EndOfStream, + fbs.reader().isBytes("foo"), + ); +} diff --git a/lib/std/Io/tty.zig b/lib/std/Io/tty.zig new file mode 100644 index 0000000000000000000000000000000000000000..fa17d9a16def9331c778c9de74abc8a41721639a --- /dev/null +++ b/lib/std/Io/tty.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const File = std.fs.File; +const process = std.process; +const windows = std.os.windows; +const native_os = builtin.os.tag; + +/// Deprecated in favor of `Config.detect`. +pub fn detectConfig(file: File) Config { + return .detect(file); +} + +pub const Color = enum { + black, + red, + green, + yellow, + blue, + magenta, + cyan, + white, + bright_black, + bright_red, + bright_green, + bright_yellow, + bright_blue, + bright_magenta, + bright_cyan, + bright_white, + dim, + bold, + reset, +}; + +/// Provides simple functionality for manipulating the terminal in some way, +/// such as coloring text, etc. +pub const Config = union(enum) { + no_color, + escape_codes, + windows_api: if (native_os == .windows) WindowsContext else void, + + /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr). + /// This includes feature checks for ANSI escape codes and the Windows console API, as well as + /// respecting the `NO_COLOR` and `CLICOLOR_FORCE` environment variables to override the default. + /// Will attempt to enable ANSI escape code support if necessary/possible. + pub fn detect(file: File) Config { + const force_color: ?bool = if (builtin.os.tag == .wasi) + null // wasi does not support environment variables + else if (process.hasNonEmptyEnvVarConstant("NO_COLOR")) + false + else if (process.hasNonEmptyEnvVarConstant("CLICOLOR_FORCE")) + true + else + null; + + if (force_color == false) return .no_color; + + if (file.getOrEnableAnsiEscapeSupport()) return .escape_codes; + + if (native_os == .windows and file.isTty()) { + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) == windows.FALSE) { + return if (force_color == true) .escape_codes else .no_color; + } + return .{ .windows_api = .{ + .handle = file.handle, + .reset_attributes = info.wAttributes, + } }; + } + + return if (force_color == true) .escape_codes else .no_color; + } + + pub const WindowsContext = struct { + handle: File.Handle, + reset_attributes: u16, + }; + + pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || std.io.Writer.Error; + + pub fn setColor(conf: Config, w: *std.io.Writer, color: Color) SetColorError!void { + nosuspend switch (conf) { + .no_color => return, + .escape_codes => { + const color_string = switch (color) { + .black => "\x1b[30m", + .red => "\x1b[31m", + .green => "\x1b[32m", + .yellow => "\x1b[33m", + .blue => "\x1b[34m", + .magenta => "\x1b[35m", + .cyan => "\x1b[36m", + .white => "\x1b[37m", + .bright_black => "\x1b[90m", + .bright_red => "\x1b[91m", + .bright_green => "\x1b[92m", + .bright_yellow => "\x1b[93m", + .bright_blue => "\x1b[94m", + .bright_magenta => "\x1b[95m", + .bright_cyan => "\x1b[96m", + .bright_white => "\x1b[97m", + .bold => "\x1b[1m", + .dim => "\x1b[2m", + .reset => "\x1b[0m", + }; + try w.writeAll(color_string); + }, + .windows_api => |ctx| if (native_os == .windows) { + const attributes = switch (color) { + .black => 0, + .red => windows.FOREGROUND_RED, + .green => windows.FOREGROUND_GREEN, + .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, + .blue => windows.FOREGROUND_BLUE, + .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, + .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, + .bright_black => windows.FOREGROUND_INTENSITY, + .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, + .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, + .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, + // "dim" is not supported using basic character attributes, but let's still make it do *something*. + // This matches the old behavior of TTY.Color before the bright variants were added. + .dim => windows.FOREGROUND_INTENSITY, + .reset => ctx.reset_attributes, + }; + try w.flush(); + try windows.SetConsoleTextAttribute(ctx.handle, attributes); + } else { + unreachable; + }, + }; + } +}; diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index 1cf4dc3c64101b589ba95e659d7489bb490a9182..542166b78e557c8dbe78755c6b724c0f150d2844 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -227,8 +227,8 @@ test join { try testJoinMaybeZWindows(&[_][]const u8{ "c:\\a\\", "b\\", "c" }, "c:\\a\\b\\c", zero); try testJoinMaybeZWindows( - &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "io.zig" }, - "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\io.zig", + &[_][]const u8{ "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std", "ab.zig" }, + "c:\\home\\andy\\dev\\zig\\build\\lib\\zig\\std\\ab.zig", zero, ); @@ -252,8 +252,8 @@ test join { try testJoinMaybeZPosix(&[_][]const u8{ "/a/", "b/", "c" }, "/a/b/c", zero); try testJoinMaybeZPosix( - &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "io.zig" }, - "/home/andy/dev/zig/build/lib/zig/std/io.zig", + &[_][]const u8{ "/home/andy/dev/zig/build/lib/zig/std", "ab.zig" }, + "/home/andy/dev/zig/build/lib/zig/std/ab.zig", zero, ); diff --git a/lib/std/io.zig b/lib/std/io.zig deleted file mode 100644 index 5339318cd1cfef9d02f94c0f4ae9702c04beb8eb..0000000000000000000000000000000000000000 --- a/lib/std/io.zig +++ /dev/null @@ -1,884 +0,0 @@ -const std = @import("std.zig"); -const builtin = @import("builtin"); -const root = @import("root"); -const c = std.c; -const is_windows = builtin.os.tag == .windows; -const windows = std.os.windows; -const posix = std.posix; -const math = std.math; -const assert = std.debug.assert; -const fs = std.fs; -const mem = std.mem; -const meta = std.meta; -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); - } - - /// Any value grater than `std.math.maxInt(usize)` is interpreted to mean - /// `.unlimited`. - pub fn limited64(n: u64) Limit { - return @enumFromInt(@min(n, std.math.maxInt(usize))); - } - - 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 minInt64(l: Limit, n: u64) 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"); - -/// Deprecated in favor of `Reader`. -pub fn GenericReader( - comptime Context: type, - comptime ReadError: type, - /// 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. - comptime readFn: fn (context: Context, buffer: []u8) ReadError!usize, -) type { - return struct { - context: Context, - - pub const Error = ReadError; - pub const NoEofError = ReadError || error{ - EndOfStream, - }; - - pub inline fn read(self: Self, buffer: []u8) Error!usize { - return readFn(self.context, buffer); - } - - pub inline fn readAll(self: Self, buffer: []u8) Error!usize { - return @errorCast(self.any().readAll(buffer)); - } - - pub inline fn readAtLeast(self: Self, buffer: []u8, len: usize) Error!usize { - return @errorCast(self.any().readAtLeast(buffer, len)); - } - - pub inline fn readNoEof(self: Self, buf: []u8) NoEofError!void { - return @errorCast(self.any().readNoEof(buf)); - } - - pub inline fn readAllArrayList( - self: Self, - array_list: *std.ArrayList(u8), - max_append_size: usize, - ) (error{StreamTooLong} || Allocator.Error || Error)!void { - return @errorCast(self.any().readAllArrayList(array_list, max_append_size)); - } - - pub inline fn readAllArrayListAligned( - self: Self, - comptime alignment: ?Alignment, - array_list: *std.ArrayListAligned(u8, alignment), - max_append_size: usize, - ) (error{StreamTooLong} || Allocator.Error || Error)!void { - return @errorCast(self.any().readAllArrayListAligned( - alignment, - array_list, - max_append_size, - )); - } - - pub inline fn readAllAlloc( - self: Self, - allocator: Allocator, - max_size: usize, - ) (Error || Allocator.Error || error{StreamTooLong})![]u8 { - return @errorCast(self.any().readAllAlloc(allocator, max_size)); - } - - pub inline fn readUntilDelimiterArrayList( - self: Self, - array_list: *std.ArrayList(u8), - delimiter: u8, - max_size: usize, - ) (NoEofError || Allocator.Error || error{StreamTooLong})!void { - return @errorCast(self.any().readUntilDelimiterArrayList( - array_list, - delimiter, - max_size, - )); - } - - pub inline fn readUntilDelimiterAlloc( - self: Self, - allocator: Allocator, - delimiter: u8, - max_size: usize, - ) (NoEofError || Allocator.Error || error{StreamTooLong})![]u8 { - return @errorCast(self.any().readUntilDelimiterAlloc( - allocator, - delimiter, - max_size, - )); - } - - pub inline fn readUntilDelimiter( - self: Self, - buf: []u8, - delimiter: u8, - ) (NoEofError || error{StreamTooLong})![]u8 { - return @errorCast(self.any().readUntilDelimiter(buf, delimiter)); - } - - pub inline fn readUntilDelimiterOrEofAlloc( - self: Self, - allocator: Allocator, - delimiter: u8, - max_size: usize, - ) (Error || Allocator.Error || error{StreamTooLong})!?[]u8 { - return @errorCast(self.any().readUntilDelimiterOrEofAlloc( - allocator, - delimiter, - max_size, - )); - } - - pub inline fn readUntilDelimiterOrEof( - self: Self, - buf: []u8, - delimiter: u8, - ) (Error || error{StreamTooLong})!?[]u8 { - return @errorCast(self.any().readUntilDelimiterOrEof(buf, delimiter)); - } - - pub inline fn streamUntilDelimiter( - self: Self, - writer: anytype, - delimiter: u8, - optional_max_size: ?usize, - ) (NoEofError || error{StreamTooLong} || @TypeOf(writer).Error)!void { - return @errorCast(self.any().streamUntilDelimiter( - writer, - delimiter, - optional_max_size, - )); - } - - pub inline fn skipUntilDelimiterOrEof(self: Self, delimiter: u8) Error!void { - return @errorCast(self.any().skipUntilDelimiterOrEof(delimiter)); - } - - pub inline fn readByte(self: Self) NoEofError!u8 { - return @errorCast(self.any().readByte()); - } - - pub inline fn readByteSigned(self: Self) NoEofError!i8 { - return @errorCast(self.any().readByteSigned()); - } - - pub inline fn readBytesNoEof( - self: Self, - comptime num_bytes: usize, - ) NoEofError![num_bytes]u8 { - return @errorCast(self.any().readBytesNoEof(num_bytes)); - } - - pub inline fn readIntoBoundedBytes( - self: Self, - comptime num_bytes: usize, - bounded: *std.BoundedArray(u8, num_bytes), - ) Error!void { - return @errorCast(self.any().readIntoBoundedBytes(num_bytes, bounded)); - } - - pub inline fn readBoundedBytes( - self: Self, - comptime num_bytes: usize, - ) Error!std.BoundedArray(u8, num_bytes) { - return @errorCast(self.any().readBoundedBytes(num_bytes)); - } - - pub inline fn readInt(self: Self, comptime T: type, endian: std.builtin.Endian) NoEofError!T { - return @errorCast(self.any().readInt(T, endian)); - } - - pub inline fn readVarInt( - self: Self, - comptime ReturnType: type, - endian: std.builtin.Endian, - size: usize, - ) NoEofError!ReturnType { - return @errorCast(self.any().readVarInt(ReturnType, endian, size)); - } - - pub const SkipBytesOptions = AnyReader.SkipBytesOptions; - - pub inline fn skipBytes( - self: Self, - num_bytes: u64, - comptime options: SkipBytesOptions, - ) NoEofError!void { - return @errorCast(self.any().skipBytes(num_bytes, options)); - } - - pub inline fn isBytes(self: Self, slice: []const u8) NoEofError!bool { - return @errorCast(self.any().isBytes(slice)); - } - - pub inline fn readStruct(self: Self, comptime T: type) NoEofError!T { - return @errorCast(self.any().readStruct(T)); - } - - pub inline fn readStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian) NoEofError!T { - return @errorCast(self.any().readStructEndian(T, endian)); - } - - pub const ReadEnumError = NoEofError || error{ - /// An integer was read, but it did not match any of the tags in the supplied enum. - InvalidValue, - }; - - pub inline fn readEnum( - self: Self, - comptime Enum: type, - endian: std.builtin.Endian, - ) ReadEnumError!Enum { - return @errorCast(self.any().readEnum(Enum, endian)); - } - - pub inline fn any(self: *const Self) AnyReader { - return .{ - .context = @ptrCast(&self.context), - .readFn = typeErasedReadFn, - }; - } - - const Self = @This(); - - fn typeErasedReadFn(context: *const anyopaque, buffer: []u8) anyerror!usize { - const ptr: *const Context = @alignCast(@ptrCast(context)); - return readFn(ptr.*, buffer); - } - }; -} - -/// Deprecated in favor of `Writer`. -pub fn GenericWriter( - comptime Context: type, - comptime WriteError: type, - comptime writeFn: fn (context: Context, bytes: []const u8) WriteError!usize, -) type { - return struct { - context: Context, - - const Self = @This(); - pub const Error = WriteError; - - pub inline fn write(self: Self, bytes: []const u8) Error!usize { - return writeFn(self.context, bytes); - } - - pub inline fn writeAll(self: Self, bytes: []const u8) Error!void { - return @errorCast(self.any().writeAll(bytes)); - } - - pub inline fn print(self: Self, comptime format: []const u8, args: anytype) Error!void { - return @errorCast(self.any().print(format, args)); - } - - pub inline fn writeByte(self: Self, byte: u8) Error!void { - return @errorCast(self.any().writeByte(byte)); - } - - pub inline fn writeByteNTimes(self: Self, byte: u8, n: usize) Error!void { - return @errorCast(self.any().writeByteNTimes(byte, n)); - } - - pub inline fn writeBytesNTimes(self: Self, bytes: []const u8, n: usize) Error!void { - return @errorCast(self.any().writeBytesNTimes(bytes, n)); - } - - pub inline fn writeInt(self: Self, comptime T: type, value: T, endian: std.builtin.Endian) Error!void { - return @errorCast(self.any().writeInt(T, value, endian)); - } - - pub inline fn writeStruct(self: Self, value: anytype) Error!void { - return @errorCast(self.any().writeStruct(value)); - } - - pub inline fn writeStructEndian(self: Self, value: anytype, endian: std.builtin.Endian) Error!void { - return @errorCast(self.any().writeStructEndian(value, endian)); - } - - pub inline fn any(self: *const Self) AnyWriter { - return .{ - .context = @ptrCast(&self.context), - .writeFn = typeErasedWriteFn, - }; - } - - fn typeErasedWriteFn(context: *const anyopaque, bytes: []const u8) anyerror!usize { - const ptr: *const Context = @alignCast(@ptrCast(context)); - return writeFn(ptr.*, bytes); - } - - /// Helper for bridging to the new `Writer` API while upgrading. - pub fn adaptToNewApi(self: *const Self) Adapter { - return .{ - .derp_writer = self.*, - .new_interface = .{ - .buffer = &.{}, - .vtable = &.{ .drain = Adapter.drain }, - }, - }; - } - - pub const Adapter = struct { - derp_writer: Self, - new_interface: Writer, - err: ?Error = null, - - fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { - _ = splat; - const a: *@This() = @fieldParentPtr("new_interface", w); - return a.derp_writer.write(data[0]) catch |err| { - a.err = err; - return error.WriteFailed; - }; - } - }; - }; -} - -/// 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; - -pub const BufferedWriter = @import("io/buffered_writer.zig").BufferedWriter; -pub const bufferedWriter = @import("io/buffered_writer.zig").bufferedWriter; - -pub const BufferedReader = @import("io/buffered_reader.zig").BufferedReader; -pub const bufferedReader = @import("io/buffered_reader.zig").bufferedReader; -pub const bufferedReaderSize = @import("io/buffered_reader.zig").bufferedReaderSize; - -pub const FixedBufferStream = @import("io/fixed_buffer_stream.zig").FixedBufferStream; -pub const fixedBufferStream = @import("io/fixed_buffer_stream.zig").fixedBufferStream; - -pub const CWriter = @import("io/c_writer.zig").CWriter; -pub const cWriter = @import("io/c_writer.zig").cWriter; - -pub const LimitedReader = @import("io/limited_reader.zig").LimitedReader; -pub const limitedReader = @import("io/limited_reader.zig").limitedReader; - -pub const CountingWriter = @import("io/counting_writer.zig").CountingWriter; -pub const countingWriter = @import("io/counting_writer.zig").countingWriter; -pub const CountingReader = @import("io/counting_reader.zig").CountingReader; -pub const countingReader = @import("io/counting_reader.zig").countingReader; - -pub const MultiWriter = @import("io/multi_writer.zig").MultiWriter; -pub const multiWriter = @import("io/multi_writer.zig").multiWriter; - -pub const BitReader = @import("io/bit_reader.zig").BitReader; -pub const bitReader = @import("io/bit_reader.zig").bitReader; - -pub const BitWriter = @import("io/bit_writer.zig").BitWriter; -pub const bitWriter = @import("io/bit_writer.zig").bitWriter; - -pub const ChangeDetectionStream = @import("io/change_detection_stream.zig").ChangeDetectionStream; -pub const changeDetectionStream = @import("io/change_detection_stream.zig").changeDetectionStream; - -pub const FindByteWriter = @import("io/find_byte_writer.zig").FindByteWriter; -pub const findByteWriter = @import("io/find_byte_writer.zig").findByteWriter; - -pub const BufferedAtomicFile = @import("io/buffered_atomic_file.zig").BufferedAtomicFile; - -pub const StreamSource = @import("io/stream_source.zig").StreamSource; - -pub const tty = @import("io/tty.zig"); - -/// A Writer that doesn't write to anything. -pub const null_writer: NullWriter = .{ .context = {} }; - -pub const NullWriter = GenericWriter(void, error{}, dummyWrite); -fn dummyWrite(context: void, data: []const u8) error{}!usize { - _ = context; - return data.len; -} - -test null_writer { - null_writer.writeAll("yay" ** 10) catch |err| switch (err) {}; -} - -pub fn poll( - allocator: Allocator, - comptime StreamEnum: type, - files: PollFiles(StreamEnum), -) Poller(StreamEnum) { - const enum_fields = @typeInfo(StreamEnum).@"enum".fields; - var result: Poller(StreamEnum) = undefined; - - if (is_windows) result.windows = .{ - .first_read_done = false, - .overlapped = [1]windows.OVERLAPPED{ - mem.zeroes(windows.OVERLAPPED), - } ** enum_fields.len, - .small_bufs = undefined, - .active = .{ - .count = 0, - .handles_buf = undefined, - .stream_map = undefined, - }, - }; - - inline for (0..enum_fields.len) |i| { - result.fifos[i] = .{ - .allocator = allocator, - .buf = &.{}, - .head = 0, - .count = 0, - }; - if (is_windows) { - result.windows.active.handles_buf[i] = @field(files, enum_fields[i].name).handle; - } else { - result.poll_fds[i] = .{ - .fd = @field(files, enum_fields[i].name).handle, - .events = posix.POLL.IN, - .revents = undefined, - }; - } - } - return result; -} - -pub const PollFifo = std.fifo.LinearFifo(u8, .Dynamic); - -pub fn Poller(comptime StreamEnum: type) type { - return struct { - const enum_fields = @typeInfo(StreamEnum).@"enum".fields; - const PollFd = if (is_windows) void else posix.pollfd; - - fifos: [enum_fields.len]PollFifo, - poll_fds: [enum_fields.len]PollFd, - windows: if (is_windows) struct { - first_read_done: bool, - overlapped: [enum_fields.len]windows.OVERLAPPED, - small_bufs: [enum_fields.len][128]u8, - active: struct { - count: math.IntFittingRange(0, enum_fields.len), - handles_buf: [enum_fields.len]windows.HANDLE, - stream_map: [enum_fields.len]StreamEnum, - - pub fn removeAt(self: *@This(), index: u32) void { - std.debug.assert(index < self.count); - for (index + 1..self.count) |i| { - self.handles_buf[i - 1] = self.handles_buf[i]; - self.stream_map[i - 1] = self.stream_map[i]; - } - self.count -= 1; - } - }, - } else void, - - const Self = @This(); - - pub fn deinit(self: *Self) void { - if (is_windows) { - // cancel any pending IO to prevent clobbering OVERLAPPED value - for (self.windows.active.handles_buf[0..self.windows.active.count]) |h| { - _ = windows.kernel32.CancelIo(h); - } - } - inline for (&self.fifos) |*q| q.deinit(); - self.* = undefined; - } - - pub fn poll(self: *Self) !bool { - if (is_windows) { - return pollWindows(self, null); - } else { - return pollPosix(self, null); - } - } - - pub fn pollTimeout(self: *Self, nanoseconds: u64) !bool { - if (is_windows) { - return pollWindows(self, nanoseconds); - } else { - return pollPosix(self, nanoseconds); - } - } - - pub inline fn fifo(self: *Self, comptime which: StreamEnum) *PollFifo { - return &self.fifos[@intFromEnum(which)]; - } - - fn pollWindows(self: *Self, nanoseconds: ?u64) !bool { - const bump_amt = 512; - - if (!self.windows.first_read_done) { - var already_read_data = false; - for (0..enum_fields.len) |i| { - const handle = self.windows.active.handles_buf[i]; - switch (try windowsAsyncReadToFifoAndQueueSmallRead( - handle, - &self.windows.overlapped[i], - &self.fifos[i], - &self.windows.small_bufs[i], - bump_amt, - )) { - .populated, .empty => |state| { - if (state == .populated) already_read_data = true; - self.windows.active.handles_buf[self.windows.active.count] = handle; - self.windows.active.stream_map[self.windows.active.count] = @as(StreamEnum, @enumFromInt(i)); - self.windows.active.count += 1; - }, - .closed => {}, // don't add to the wait_objects list - .closed_populated => { - // don't add to the wait_objects list, but we did already get data - already_read_data = true; - }, - } - } - self.windows.first_read_done = true; - if (already_read_data) return true; - } - - while (true) { - if (self.windows.active.count == 0) return false; - - const status = windows.kernel32.WaitForMultipleObjects( - self.windows.active.count, - &self.windows.active.handles_buf, - 0, - if (nanoseconds) |ns| - @min(std.math.cast(u32, ns / std.time.ns_per_ms) orelse (windows.INFINITE - 1), windows.INFINITE - 1) - else - windows.INFINITE, - ); - if (status == windows.WAIT_FAILED) - return windows.unexpectedError(windows.GetLastError()); - if (status == windows.WAIT_TIMEOUT) - return true; - - if (status < windows.WAIT_OBJECT_0 or status > windows.WAIT_OBJECT_0 + enum_fields.len - 1) - unreachable; - - const active_idx = status - windows.WAIT_OBJECT_0; - - const stream_idx = @intFromEnum(self.windows.active.stream_map[active_idx]); - const handle = self.windows.active.handles_buf[active_idx]; - - const overlapped = &self.windows.overlapped[stream_idx]; - const stream_fifo = &self.fifos[stream_idx]; - const small_buf = &self.windows.small_bufs[stream_idx]; - - const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { - .success => |n| n, - .closed => { - self.windows.active.removeAt(active_idx); - continue; - }, - .aborted => unreachable, - }; - try stream_fifo.write(small_buf[0..num_bytes_read]); - - switch (try windowsAsyncReadToFifoAndQueueSmallRead( - handle, - overlapped, - stream_fifo, - small_buf, - bump_amt, - )) { - .empty => {}, // irrelevant, we already got data from the small buffer - .populated => {}, - .closed, - .closed_populated, // identical, since we already got data from the small buffer - => self.windows.active.removeAt(active_idx), - } - return true; - } - } - - fn pollPosix(self: *Self, nanoseconds: ?u64) !bool { - // We ask for ensureUnusedCapacity with this much extra space. This - // has more of an effect on small reads because once the reads - // start to get larger the amount of space an ArrayList will - // allocate grows exponentially. - const bump_amt = 512; - - const err_mask = posix.POLL.ERR | posix.POLL.NVAL | posix.POLL.HUP; - - const events_len = try posix.poll(&self.poll_fds, if (nanoseconds) |ns| - std.math.cast(i32, ns / std.time.ns_per_ms) orelse std.math.maxInt(i32) - else - -1); - if (events_len == 0) { - for (self.poll_fds) |poll_fd| { - if (poll_fd.fd != -1) return true; - } else return false; - } - - var keep_polling = false; - inline for (&self.poll_fds, &self.fifos) |*poll_fd, *q| { - // Try reading whatever is available before checking the error - // conditions. - // It's still possible to read after a POLL.HUP is received, - // always check if there's some data waiting to be read first. - if (poll_fd.revents & posix.POLL.IN != 0) { - const buf = try q.writableWithSize(bump_amt); - const amt = posix.read(poll_fd.fd, buf) catch |err| switch (err) { - error.BrokenPipe => 0, // Handle the same as EOF. - else => |e| return e, - }; - q.update(amt); - if (amt == 0) { - // Remove the fd when the EOF condition is met. - poll_fd.fd = -1; - } else { - keep_polling = true; - } - } else if (poll_fd.revents & err_mask != 0) { - // Exclude the fds that signaled an error. - poll_fd.fd = -1; - } else if (poll_fd.fd != -1) { - keep_polling = true; - } - } - return keep_polling; - } - }; -} - -/// The `ReadFile` docuementation states that `lpNumberOfBytesRead` does not have a meaningful -/// result when using overlapped I/O, but also that it cannot be `null` on Windows 7. For -/// compatibility, we point it to this dummy variables, which we never otherwise access. -/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile -var win_dummy_bytes_read: u32 = undefined; - -/// Read as much data as possible from `handle` with `overlapped`, and write it to the FIFO. Before -/// returning, queue a read into `small_buf` so that `WaitForMultipleObjects` returns when more data -/// is available. `handle` must have no pending asynchronous operation. -fn windowsAsyncReadToFifoAndQueueSmallRead( - handle: windows.HANDLE, - overlapped: *windows.OVERLAPPED, - fifo: *PollFifo, - small_buf: *[128]u8, - bump_amt: usize, -) !enum { empty, populated, closed_populated, closed } { - var read_any_data = false; - while (true) { - const fifo_read_pending = while (true) { - const buf = try fifo.writableWithSize(bump_amt); - const buf_len = math.cast(u32, buf.len) orelse math.maxInt(u32); - - if (0 == windows.kernel32.ReadFile( - handle, - buf.ptr, - buf_len, - &win_dummy_bytes_read, - overlapped, - )) switch (windows.GetLastError()) { - .IO_PENDING => break true, - .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, - else => |err| return windows.unexpectedError(err), - }; - - const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { - .success => |n| n, - .closed => return if (read_any_data) .closed_populated else .closed, - .aborted => unreachable, - }; - - read_any_data = true; - fifo.update(num_bytes_read); - - if (num_bytes_read == buf_len) { - // We filled the buffer, so there's probably more data available. - continue; - } else { - // We didn't fill the buffer, so assume we're out of data. - // There is no pending read. - break false; - } - }; - - if (fifo_read_pending) cancel_read: { - // Cancel the pending read into the FIFO. - _ = windows.kernel32.CancelIo(handle); - - // We have to wait for the handle to be signalled, i.e. for the cancellation to complete. - switch (windows.kernel32.WaitForSingleObject(handle, windows.INFINITE)) { - windows.WAIT_OBJECT_0 => {}, - windows.WAIT_FAILED => return windows.unexpectedError(windows.GetLastError()), - else => unreachable, - } - - // If it completed before we canceled, make sure to tell the FIFO! - const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, true)) { - .success => |n| n, - .closed => return if (read_any_data) .closed_populated else .closed, - .aborted => break :cancel_read, - }; - read_any_data = true; - fifo.update(num_bytes_read); - } - - // Try to queue the 1-byte read. - if (0 == windows.kernel32.ReadFile( - handle, - small_buf, - small_buf.len, - &win_dummy_bytes_read, - overlapped, - )) switch (windows.GetLastError()) { - .IO_PENDING => { - // 1-byte read pending as intended - return if (read_any_data) .populated else .empty; - }, - .BROKEN_PIPE => return if (read_any_data) .closed_populated else .closed, - else => |err| return windows.unexpectedError(err), - }; - - // We got data back this time. Write it to the FIFO and run the main loop again. - const num_bytes_read = switch (try windowsGetReadResult(handle, overlapped, false)) { - .success => |n| n, - .closed => return if (read_any_data) .closed_populated else .closed, - .aborted => unreachable, - }; - try fifo.write(small_buf[0..num_bytes_read]); - read_any_data = true; - } -} - -/// Simple wrapper around `GetOverlappedResult` to determine the result of a `ReadFile` operation. -/// If `!allow_aborted`, then `aborted` is never returned (`OPERATION_ABORTED` is considered unexpected). -/// -/// The `ReadFile` documentation states that the number of bytes read by an overlapped `ReadFile` must be determined using `GetOverlappedResult`, even if the -/// operation immediately returns data: -/// "Use NULL for [lpNumberOfBytesRead] if this is an asynchronous operation to avoid potentially -/// erroneous results." -/// "If `hFile` was opened with `FILE_FLAG_OVERLAPPED`, the following conditions are in effect: [...] -/// The lpNumberOfBytesRead parameter should be set to NULL. Use the GetOverlappedResult function to -/// get the actual number of bytes read." -/// See: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile -fn windowsGetReadResult( - handle: windows.HANDLE, - overlapped: *windows.OVERLAPPED, - allow_aborted: bool, -) !union(enum) { - success: u32, - closed, - aborted, -} { - var num_bytes_read: u32 = undefined; - if (0 == windows.kernel32.GetOverlappedResult( - handle, - overlapped, - &num_bytes_read, - 0, - )) switch (windows.GetLastError()) { - .BROKEN_PIPE => return .closed, - .OPERATION_ABORTED => |err| if (allow_aborted) { - return .aborted; - } else { - return windows.unexpectedError(err); - }, - else => |err| return windows.unexpectedError(err), - }; - return .{ .success = num_bytes_read }; -} - -/// Given an enum, returns a struct with fields of that enum, each field -/// representing an I/O stream for polling. -pub fn PollFiles(comptime StreamEnum: type) type { - const enum_fields = @typeInfo(StreamEnum).@"enum".fields; - var struct_fields: [enum_fields.len]std.builtin.Type.StructField = undefined; - for (&struct_fields, enum_fields) |*struct_field, enum_field| { - struct_field.* = .{ - .name = enum_field.name, - .type = fs.File, - .default_value_ptr = null, - .is_comptime = false, - .alignment = @alignOf(fs.File), - }; - } - return @Type(.{ .@"struct" = .{ - .layout = .auto, - .fields = &struct_fields, - .decls = &.{}, - .is_tuple = false, - } }); -} - -test { - _ = Reader; - _ = Writer; - _ = @import("io/bit_reader.zig"); - _ = @import("io/bit_writer.zig"); - _ = @import("io/buffered_atomic_file.zig"); - _ = @import("io/buffered_reader.zig"); - _ = @import("io/buffered_writer.zig"); - _ = @import("io/c_writer.zig"); - _ = @import("io/counting_writer.zig"); - _ = @import("io/counting_reader.zig"); - _ = @import("io/fixed_buffer_stream.zig"); - _ = @import("io/seekable_stream.zig"); - _ = @import("io/stream_source.zig"); - _ = @import("io/test.zig"); -} diff --git a/lib/std/io/DeprecatedReader.zig b/lib/std/io/DeprecatedReader.zig deleted file mode 100644 index 3f2429c3aead2a048179dde86991f87b2f59cba8..0000000000000000000000000000000000000000 --- a/lib/std/io/DeprecatedReader.zig +++ /dev/null @@ -1,386 +0,0 @@ -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 deleted file mode 100644 index 391b9853570100e5adecc1e2b0d562372f911aa7..0000000000000000000000000000000000000000 --- a/lib/std/io/DeprecatedWriter.zig +++ /dev/null @@ -1,109 +0,0 @@ -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; - } -} - -/// Helper for bridging to the new `Writer` API while upgrading. -pub fn adaptToNewApi(self: *const Self) Adapter { - return .{ - .derp_writer = self.*, - .new_interface = .{ - .buffer = &.{}, - .vtable = &.{ .drain = Adapter.drain }, - }, - }; -} - -pub const Adapter = struct { - derp_writer: Self, - new_interface: std.io.Writer, - err: ?Error = null, - - fn drain(w: *std.io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { - _ = splat; - const a: *@This() = @fieldParentPtr("new_interface", w); - return a.derp_writer.write(data[0]) catch |err| { - a.err = err; - return error.WriteFailed; - }; - } -}; diff --git a/lib/std/io/Reader.zig b/lib/std/io/Reader.zig deleted file mode 100644 index c2f0b25017c290436c8dc194a5962fa606807608..0000000000000000000000000000000000000000 --- a/lib/std/io/Reader.zig +++ /dev/null @@ -1,1731 +0,0 @@ -const Reader = @This(); - -const builtin = @import("builtin"); -const native_endian = builtin.target.cpu.arch.endian(); - -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`, a value whose minimum size is determined by the - /// stream implementation. - /// - /// 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. - /// - /// Although this function is usually called when `buffer` is empty, it is - /// also called when it needs to be filled more due to the API user - /// requesting contiguous memory. In either case, the existing buffer data - /// should be ignored; new data written to `w`. - /// - /// In addition to, or instead of writing to `w`, the implementation may - /// choose to store data in `buffer`, modifying `seek` and `end` - /// accordingly. Stream implementations are encouraged to take advantage of - /// this if simplifies the logic. - 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. - /// - /// This function is only called when `buffer` is empty. - 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 .init(r, limit, buffer); -} - -/// 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, - }; -} - -pub fn stream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { - const buffer = limit.slice(r.buffer[r.seek..r.end]); - if (buffer.len > 0) { - @branchHint(.likely); - const n = try w.write(buffer); - r.seek += n; - return n; - } - const n = try r.vtable.stream(r, w, limit); - assert(n <= @intFromEnum(limit)); - return n; -} - -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 dw: Writer.Discarding = .init(r.buffer); - const n = r.stream(&dw.writer, limit) catch |err| switch (err) { - error.WriteFailed => unreachable, - error.ReadFailed => return error.ReadFailed, - error.EndOfStream => return error.EndOfStream, - }; - assert(n <= @intFromEnum(limit)); - 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) { - 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; - 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); -} - -/// 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; - 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); -} - -/// 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 => { - assert(!wrapper.used); - if (wrapper.writer.buffer.ptr == first.ptr) { - remaining -= wrapper.writer.end; - } else { - assert(wrapper.writer.end <= r.buffer.len); - r.end = wrapper.writer.end; - } - break; - }, - else => |e| return e, - }; - if (!wrapper.used) { - if (wrapper.writer.buffer.ptr == first.ptr) { - remaining -= n; - } else { - assert(n <= r.buffer.len); - 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; - } - assert(n <= r.buffer.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 }; -} - -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; - } - } -} - -/// 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 tossBuffered(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 readSliceAll(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: -/// * `readSliceAll` -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.used) { - assert(r.seek == 0); - r.seek = remaining.len; - r.end = wrapper.writer.end; - @memcpy(remaining, r.buffer[0..remaining.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: -/// * `readSliceAll` -/// * `readSliceEndianAlloc` -pub inline fn readSliceEndian( - r: *Reader, - comptime Elem: type, - buffer: []Elem, - endian: std.builtin.Endian, -) Error!void { - try readSliceAll(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 readSliceAll(r, @ptrCast(dest)); - if (native_endian != endian) for (dest) |*elem| std.mem.byteSwapAllFields(Elem, elem); - return dest; -} - -/// Shortcut for calling `readSliceAll` with a buffer provided by `allocator`. -pub fn readAlloc(r: *Reader, allocator: Allocator, len: usize) ReadAllocError![]u8 { - const dest = try allocator.alloc(u8, len); - errdefer allocator.free(dest); - try readSliceAll(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; -} - -/// Returns a slice of the next bytes of buffered data from the stream until -/// `sentinel` is found, without advancing the seek position. -/// -/// Returned slice has a sentinel; end of stream does not count as a delimiter. -/// -/// Invalidates previously returned values from `peek`. -/// -/// See also: -/// * `takeSentinel` -/// * `peekDelimiterExclusive` -/// * `peekDelimiterInclusive` -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 (r.vtable.stream == &endingStream) { - // Protect the `@constCast` of `fixed`. - return error.EndOfStream; - } - r.rebase(); - while (r.buffer.len - r.end != 0) { - const end_cap = r.buffer[r.end..]; - var writer: Writer = .fixed(end_cap); - const n = r.vtable.stream(r, &writer, .limited(end_cap.len)) catch |err| switch (err) { - error.WriteFailed => unreachable, - else => |e| return e, - }; - r.end += n; - if (std.mem.indexOfScalarPos(u8, end_cap[0..n], 0, delimiter)) |end| { - return r.buffer[0 .. r.end - n + 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 => { - const remaining = r.buffer[r.seek..r.end]; - if (remaining.len == 0) return error.EndOfStream; - r.toss(remaining.len); - return remaining; - }, - else => |e| return e, - }; - 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 => { - const remaining = r.buffer[r.seek..r.end]; - if (remaining.len == 0) return error.EndOfStream; - r.toss(remaining.len); - return remaining; - }, - else => |e| return e, - }; - return result[0 .. result.len - 1]; -} - -/// Appends to `w` contents by reading from the stream until `delimiter` is -/// found. Does not write the delimiter itself. -/// -/// Returns number of bytes streamed, which may be zero, or error.EndOfStream -/// if the delimiter was not found. -/// -/// See also: -/// * `streamDelimiterEnding` -/// * `streamDelimiterLimit` -pub fn streamDelimiter(r: *Reader, w: *Writer, delimiter: u8) StreamError!usize { - const n = streamDelimiterLimit(r, w, delimiter, .unlimited) catch |err| switch (err) { - error.StreamTooLong => unreachable, // unlimited is passed - else => |e| return e, - }; - if (r.seek == r.end) return error.EndOfStream; - return n; -} - -/// Appends to `w` contents by reading from the stream until `delimiter` is found. -/// Does not write the delimiter itself. -/// -/// Returns number of bytes streamed, which may be zero. End of stream can be -/// detected by checking if the next byte in the stream is the delimiter. -/// -/// See also: -/// * `streamDelimiter` -/// * `streamDelimiterLimit` -pub fn streamDelimiterEnding( - r: *Reader, - w: *Writer, - delimiter: u8, -) StreamRemainingError!usize { - return streamDelimiterLimit(r, w, delimiter, .unlimited) catch |err| switch (err) { - error.StreamTooLong => unreachable, // unlimited is passed - else => |e| return e, - }; -} - -pub const StreamDelimiterLimitError = error{ - ReadFailed, - WriteFailed, - /// 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, which may be zero. End of stream can be -/// detected by checking if the next byte in the stream is the delimiter. -pub fn streamDelimiterLimit( - r: *Reader, - w: *Writer, - delimiter: u8, - limit: Limit, -) StreamDelimiterLimitError!usize { - var remaining = @intFromEnum(limit); - while (remaining != 0) { - const available = Limit.limited(remaining).slice(r.peekGreedy(1) catch |err| switch (err) { - error.ReadFailed => return error.ReadFailed, - error.EndOfStream => return @intFromEnum(limit) - remaining, - }); - if (std.mem.indexOfScalar(u8, available, delimiter)) |delimiter_index| { - try w.writeAll(available[0..delimiter_index]); - r.toss(delimiter_index); - remaining -= delimiter_index; - return @intFromEnum(limit) - remaining; - } - try w.writeAll(available); - r.toss(available.len); - remaining -= available.len; - } - return error.StreamTooLong; -} - -/// Reads from the stream until specified byte is found, discarding all data, -/// including the delimiter. -/// -/// Returns number of bytes discarded, or `error.EndOfStream` if the delimiter -/// is not found. -/// -/// See also: -/// * `discardDelimiterExclusive` -/// * `discardDelimiterLimit` -pub fn discardDelimiterInclusive(r: *Reader, delimiter: u8) Error!usize { - const n = discardDelimiterLimit(r, delimiter, .unlimited) catch |err| switch (err) { - error.StreamTooLong => unreachable, // unlimited is passed - else => |e| return e, - }; - if (r.seek == r.end) return error.EndOfStream; - assert(r.buffer[r.seek] == delimiter); - toss(r, 1); - return n + 1; -} - -/// Reads from the stream until specified byte is found, discarding all data, -/// excluding the delimiter. -/// -/// Returns the number of bytes discarded. -/// -/// Succeeds if stream ends before delimiter found. End of stream can be -/// detected by checking if the delimiter is buffered. -/// -/// See also: -/// * `discardDelimiterInclusive` -/// * `discardDelimiterLimit` -pub fn discardDelimiterExclusive(r: *Reader, delimiter: u8) ShortError!usize { - return discardDelimiterLimit(r, delimiter, .unlimited) catch |err| switch (err) { - error.StreamTooLong => unreachable, // unlimited is passed - else => |e| return e, - }; -} - -pub const DiscardDelimiterLimitError = error{ - ReadFailed, - /// The delimiter was not found within the limit. - StreamTooLong, -}; - -/// Reads from the stream until specified byte is found, discarding all data, -/// excluding the delimiter. -/// -/// Returns the number of bytes discarded. -/// -/// Succeeds if stream ends before delimiter found. End of stream can be -/// detected by checking if the delimiter is buffered. -pub fn discardDelimiterLimit(r: *Reader, delimiter: u8, limit: Limit) DiscardDelimiterLimitError!usize { - var remaining = @intFromEnum(limit); - while (remaining != 0) { - const available = Limit.limited(remaining).slice(r.peekGreedy(1) catch |err| switch (err) { - error.ReadFailed => return error.ReadFailed, - error.EndOfStream => return @intFromEnum(limit) - remaining, - }); - if (std.mem.indexOfScalar(u8, available, delimiter)) |delimiter_index| { - r.toss(delimiter_index); - remaining -= delimiter_index; - return @intFromEnum(limit) - remaining; - } - r.toss(available.len); - remaining -= available.len; - } - return error.StreamTooLong; -} - -/// 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; - } - if (r.seek + n <= r.buffer.len) while (true) { - const end_cap = r.buffer[r.end..]; - var writer: Writer = .fixed(end_cap); - r.end += r.vtable.stream(r, &writer, .limited(end_cap.len)) catch |err| switch (err) { - error.WriteFailed => unreachable, - else => |e| return e, - }; - if (r.seek + n <= r.end) return; - }; - if (r.vtable.stream == &endingStream) { - // Protect the `@constCast` of `fixed`. - return error.EndOfStream; - } - 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, - }; - } -} - -/// 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(.likely); - return buffer[seek]; - } - try fill(r, 1); - return r.buffer[r.seek]; -} - -/// 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; -} - -/// Same as `takeByte` except the returned byte is signed. -pub fn takeByteSigned(r: *Reader) Error!i8 { - return @bitCast(try r.takeByte()); -} - -/// 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); -} - -/// 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); -} - -/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. -/// -/// Advances the seek position. -/// -/// See also: -/// * `peekStruct` -/// * `takeStructEndian` -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))); -} - -/// Asserts the buffer was initialized with a capacity at least `@sizeOf(T)`. -/// -/// Does not advance the seek position. -/// -/// See also: -/// * `takeStruct` -/// * `peekStructEndian` -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); - return @ptrCast(try r.peekArray(@sizeOf(T))); -} - -/// 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. -/// -/// See also: -/// * `takeStruct` -/// * `peekStructEndian` -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. -/// -/// See also: -/// * `takeStructEndian` -/// * `peekStruct` -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; -} - -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, - }; -} - -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; -} - -/// 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; -} - -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 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); - } -} - -/// 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 { - var r: Reader = .fixed("abc"); - try testing.expectEqualStrings("ab", try r.peek(2)); - try testing.expectEqualStrings("a", try r.peek(1)); -} - -test peekGreedy { - var r: Reader = .fixed("abc"); - try testing.expectEqualStrings("abc", try r.peekGreedy(1)); -} - -test toss { - var r: Reader = .fixed("abc"); - r.toss(1); - try testing.expectEqualStrings("bc", r.buffered()); -} - -test take { - var r: Reader = .fixed("abc"); - try testing.expectEqualStrings("ab", try r.take(2)); - try testing.expectEqualStrings("c", try r.take(1)); -} - -test takeArray { - var r: Reader = .fixed("abc"); - try testing.expectEqualStrings("ab", try r.takeArray(2)); - try testing.expectEqualStrings("c", try r.takeArray(1)); -} - -test peekArray { - var r: Reader = .fixed("abc"); - try testing.expectEqualStrings("ab", try r.peekArray(2)); - try testing.expectEqualStrings("a", try r.peekArray(1)); -} - -test discardAll { - var r: Reader = .fixed("foobar"); - try r.discardAll(3); - try testing.expectEqualStrings("bar", try r.take(3)); - try r.discardAll(0); - try testing.expectError(error.EndOfStream, r.discardAll(1)); -} - -test discardRemaining { - var r: Reader = .fixed("foobar"); - r.toss(1); - try testing.expectEqual(5, try r.discardRemaining()); - try testing.expectEqual(0, try r.discardRemaining()); -} - -test stream { - var out_buffer: [10]u8 = undefined; - var r: Reader = .fixed("foobar"); - var w: Writer = .fixed(&out_buffer); - // Short streams are possible with this function but not with fixed. - try testing.expectEqual(2, try r.stream(&w, .limited(2))); - try testing.expectEqualStrings("fo", w.buffered()); - try testing.expectEqual(4, try r.stream(&w, .unlimited)); - try testing.expectEqualStrings("foobar", w.buffered()); -} - -test takeSentinel { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab", try r.takeSentinel('\n')); - try testing.expectError(error.EndOfStream, r.takeSentinel('\n')); - try testing.expectEqualStrings("c", try r.peek(1)); -} - -test peekSentinel { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab", try r.peekSentinel('\n')); - try testing.expectEqualStrings("ab", try r.peekSentinel('\n')); -} - -test takeDelimiterInclusive { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab\n", try r.takeDelimiterInclusive('\n')); - try testing.expectError(error.EndOfStream, r.takeDelimiterInclusive('\n')); -} - -test peekDelimiterInclusive { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n')); - try testing.expectEqualStrings("ab\n", try r.peekDelimiterInclusive('\n')); - r.toss(3); - try testing.expectError(error.EndOfStream, r.peekDelimiterInclusive('\n')); -} - -test takeDelimiterExclusive { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab", try r.takeDelimiterExclusive('\n')); - try testing.expectEqualStrings("c", try r.takeDelimiterExclusive('\n')); - try testing.expectError(error.EndOfStream, r.takeDelimiterExclusive('\n')); -} - -test peekDelimiterExclusive { - var r: Reader = .fixed("ab\nc"); - try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n')); - try testing.expectEqualStrings("ab", try r.peekDelimiterExclusive('\n')); - r.toss(3); - try testing.expectEqualStrings("c", try r.peekDelimiterExclusive('\n')); -} - -test streamDelimiter { - var out_buffer: [10]u8 = undefined; - var r: Reader = .fixed("foo\nbars"); - var w: Writer = .fixed(&out_buffer); - try testing.expectEqual(3, try r.streamDelimiter(&w, '\n')); - try testing.expectEqualStrings("foo", w.buffered()); - try testing.expectEqual(0, try r.streamDelimiter(&w, '\n')); - r.toss(1); - try testing.expectError(error.EndOfStream, r.streamDelimiter(&w, '\n')); -} - -test streamDelimiterEnding { - var out_buffer: [10]u8 = undefined; - var r: Reader = .fixed("foo\nbars"); - var w: Writer = .fixed(&out_buffer); - try testing.expectEqual(3, try r.streamDelimiterEnding(&w, '\n')); - try testing.expectEqualStrings("foo", w.buffered()); - r.toss(1); - try testing.expectEqual(4, try r.streamDelimiterEnding(&w, '\n')); - try testing.expectEqualStrings("foobars", w.buffered()); - try testing.expectEqual(0, try r.streamDelimiterEnding(&w, '\n')); - try testing.expectEqual(0, try r.streamDelimiterEnding(&w, '\n')); -} - -test streamDelimiterLimit { - var out_buffer: [10]u8 = undefined; - var r: Reader = .fixed("foo\nbars"); - var w: Writer = .fixed(&out_buffer); - try testing.expectError(error.StreamTooLong, r.streamDelimiterLimit(&w, '\n', .limited(2))); - try testing.expectEqual(1, try r.streamDelimiterLimit(&w, '\n', .limited(3))); - try testing.expectEqualStrings("\n", try r.take(1)); - try testing.expectEqual(4, try r.streamDelimiterLimit(&w, '\n', .unlimited)); - try testing.expectEqualStrings("foobars", w.buffered()); -} - -test discardDelimiterExclusive { - var r: Reader = .fixed("foob\nar"); - try testing.expectEqual(4, try r.discardDelimiterExclusive('\n')); - try testing.expectEqualStrings("\n", try r.take(1)); - try testing.expectEqual(2, try r.discardDelimiterExclusive('\n')); - try testing.expectEqual(0, try r.discardDelimiterExclusive('\n')); -} - -test discardDelimiterInclusive { - var r: Reader = .fixed("foob\nar"); - try testing.expectEqual(5, try r.discardDelimiterInclusive('\n')); - try testing.expectError(error.EndOfStream, r.discardDelimiterInclusive('\n')); -} - -test discardDelimiterLimit { - var r: Reader = .fixed("foob\nar"); - try testing.expectError(error.StreamTooLong, r.discardDelimiterLimit('\n', .limited(4))); - try testing.expectEqual(0, try r.discardDelimiterLimit('\n', .limited(2))); - try testing.expectEqualStrings("\n", try r.take(1)); - try testing.expectEqual(2, try r.discardDelimiterLimit('\n', .unlimited)); - try testing.expectEqual(0, try r.discardDelimiterLimit('\n', .unlimited)); -} - -test fill { - var r: Reader = .fixed("abc"); - try r.fill(1); - try r.fill(3); -} - -test takeByte { - var r: Reader = .fixed("ab"); - try testing.expectEqual('a', try r.takeByte()); - try testing.expectEqual('b', try r.takeByte()); - try testing.expectError(error.EndOfStream, r.takeByte()); -} - -test takeByteSigned { - var r: Reader = .fixed(&.{ 255, 5 }); - try testing.expectEqual(-1, try r.takeByteSigned()); - try testing.expectEqual(5, try r.takeByteSigned()); - try testing.expectError(error.EndOfStream, r.takeByteSigned()); -} - -test takeInt { - var r: Reader = .fixed(&.{ 0x12, 0x34, 0x56 }); - try testing.expectEqual(0x1234, try r.takeInt(u16, .big)); - try testing.expectError(error.EndOfStream, r.takeInt(u16, .little)); -} - -test takeVarInt { - var r: Reader = .fixed(&.{ 0x12, 0x34, 0x56 }); - try testing.expectEqual(0x123456, try r.takeVarInt(u64, .big, 3)); - try testing.expectError(error.EndOfStream, r.takeVarInt(u16, .little, 1)); -} - -test takeStruct { - var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); - const S = extern struct { a: u8, b: u16 }; - switch (native_endian) { - .little => try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.takeStruct(S)).*), - .big => try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.takeStruct(S)).*), - } - try testing.expectError(error.EndOfStream, r.takeStruct(S)); -} - -test peekStruct { - var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); - const S = extern struct { a: u8, b: u16 }; - switch (native_endian) { - .little => { - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.peekStruct(S)).*); - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), (try r.peekStruct(S)).*); - }, - .big => { - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.peekStruct(S)).*); - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), (try r.peekStruct(S)).*); - }, - } -} - -test takeStructEndian { - var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); - const S = extern struct { a: u8, b: u16 }; - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), try r.takeStructEndian(S, .big)); - try testing.expectError(error.EndOfStream, r.takeStructEndian(S, .little)); -} - -test peekStructEndian { - var r: Reader = .fixed(&.{ 0x12, 0x00, 0x34, 0x56 }); - const S = extern struct { a: u8, b: u16 }; - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x3456 }), try r.peekStructEndian(S, .big)); - try testing.expectEqual(@as(S, .{ .a = 0x12, .b = 0x5634 }), try r.peekStructEndian(S, .little)); -} - -test takeEnum { - var r: Reader = .fixed(&.{ 2, 0, 1 }); - const E1 = enum(u8) { a, b, c }; - const E2 = enum(u16) { _ }; - try testing.expectEqual(E1.c, try r.takeEnum(E1, .little)); - try testing.expectEqual(@as(E2, @enumFromInt(0x0001)), try r.takeEnum(E2, .big)); -} - -test takeLeb128 { - var r: Reader = .fixed("\xc7\x9f\x7f\x80"); - try testing.expectEqual(-12345, try r.takeLeb128(i64)); - try testing.expectEqual(0x80, try r.peekByte()); - try testing.expectError(error.EndOfStream, r.takeLeb128(i64)); -} - -test readSliceShort { - var r: Reader = .fixed("HelloFren"); - var buf: [5]u8 = undefined; - try testing.expectEqual(5, try r.readSliceShort(&buf)); - try testing.expectEqualStrings("Hello", buf[0..5]); - try testing.expectEqual(4, try r.readSliceShort(&buf)); - try testing.expectEqualStrings("Fren", buf[0..4]); - try testing.expectEqual(0, try r.readSliceShort(&buf)); -} - -test readVec { - var r: Reader = .fixed(std.ascii.letters); - var flat_buffer: [52]u8 = undefined; - var bufs: [2][]u8 = .{ - flat_buffer[0..26], - flat_buffer[26..], - }; - // Short reads are possible with this function but not with fixed. - try testing.expectEqual(26 * 2, try r.readVec(&bufs)); - try testing.expectEqualStrings(std.ascii.letters[0..26], bufs[0]); - try testing.expectEqualStrings(std.ascii.letters[26..], bufs[1]); -} - -test readVecLimit { - var r: Reader = .fixed(std.ascii.letters); - var flat_buffer: [52]u8 = undefined; - var bufs: [2][]u8 = .{ - flat_buffer[0..26], - flat_buffer[26..], - }; - // Short reads are possible with this function but not with fixed. - try testing.expectEqual(50, try r.readVecLimit(&bufs, .limited(50))); - try testing.expectEqualStrings(std.ascii.letters[0..26], bufs[0]); - try testing.expectEqualStrings(std.ascii.letters[26..50], bufs[1][0..24]); -} - -test "expected error.EndOfStream" { - // Unit test inspired by https://github.com/ziglang/zig/issues/17733 - var buffer: [3]u8 = undefined; - var r: std.io.Reader = .fixed(&buffer); - r.end = 0; // capacity 3, but empty - try std.testing.expectError(error.EndOfStream, r.takeEnum(enum(u8) { a, b }, .little)); - try std.testing.expectError(error.EndOfStream, r.take(3)); -} - -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, - i: usize, - reader: Reader, - - fn stream(r: *Reader, w: *Writer, limit: Limit) StreamError!usize { - assert(@intFromEnum(limit) >= 1); - const self: *@This() = @fieldParentPtr("reader", r); - if (self.str.len - self.i == 0) return error.EndOfStream; - try w.writeByte(self.str[self.i]); - self.i += 1; - return 1; - } - }; - const str = "This is a test"; - var one_byte_stream: OneByteReader = .{ - .str = str, - .i = 0, - .reader = .{ - .buffer = &.{}, - .vtable = &.{ .stream = OneByteReader.stream }, - .seek = 0, - .end = 0, - }, - }; - const res = try one_byte_stream.reader.allocRemaining(std.testing.allocator, .unlimited); - defer std.testing.allocator.free(res); - try std.testing.expectEqualStrings(str, res); -} - -test "takeDelimiterInclusive when it rebases" { - const written_line = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"; - var buffer: [128]u8 = undefined; - var tr: std.testing.Reader = .init(&buffer, &.{ - .{ .buffer = written_line }, - .{ .buffer = written_line }, - .{ .buffer = written_line }, - .{ .buffer = written_line }, - .{ .buffer = written_line }, - .{ .buffer = written_line }, - }); - const r = &tr.interface; - for (0..6) |_| { - try std.testing.expectEqualStrings(written_line, try r.takeDelimiterInclusive('\n')); - } -} - -/// 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 deleted file mode 100644 index 9476b97804ec87cf6c469c1da8ba8835be1b708a..0000000000000000000000000000000000000000 --- a/lib/std/io/Reader/Limited.zig +++ /dev/null @@ -1,42 +0,0 @@ -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/Reader/test.zig b/lib/std/io/Reader/test.zig deleted file mode 100644 index 30f0e1269c321988a9c8016597883634b8142783..0000000000000000000000000000000000000000 --- a/lib/std/io/Reader/test.zig +++ /dev/null @@ -1,372 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("../../std.zig"); -const testing = std.testing; - -test "Reader" { - var buf = "a\x02".*; - var fis = std.io.fixedBufferStream(&buf); - const reader = fis.reader(); - try testing.expect((try reader.readByte()) == 'a'); - try testing.expect((try reader.readEnum(enum(u8) { - a = 0, - b = 99, - c = 2, - d = 3, - }, builtin.cpu.arch.endian())) == .c); - try testing.expectError(error.EndOfStream, reader.readByte()); -} - -test "isBytes" { - var fis = std.io.fixedBufferStream("foobar"); - const reader = fis.reader(); - try testing.expectEqual(true, try reader.isBytes("foo")); - try testing.expectEqual(false, try reader.isBytes("qux")); -} - -test "skipBytes" { - var fis = std.io.fixedBufferStream("foobar"); - const reader = fis.reader(); - try reader.skipBytes(3, .{}); - try testing.expect(try reader.isBytes("bar")); - try reader.skipBytes(0, .{}); - try testing.expectError(error.EndOfStream, reader.skipBytes(1, .{})); -} - -test "readUntilDelimiterArrayList returns ArrayLists with bytes read until the delimiter, then EndOfStream" { - const a = std.testing.allocator; - var list = std.ArrayList(u8).init(a); - defer list.deinit(); - - var fis = std.io.fixedBufferStream("0000\n1234\n"); - const reader = fis.reader(); - - try reader.readUntilDelimiterArrayList(&list, '\n', 5); - try std.testing.expectEqualStrings("0000", list.items); - try reader.readUntilDelimiterArrayList(&list, '\n', 5); - try std.testing.expectEqualStrings("1234", list.items); - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterArrayList(&list, '\n', 5)); -} - -test "readUntilDelimiterArrayList returns an empty ArrayList" { - const a = std.testing.allocator; - var list = std.ArrayList(u8).init(a); - defer list.deinit(); - - var fis = std.io.fixedBufferStream("\n"); - const reader = fis.reader(); - - try reader.readUntilDelimiterArrayList(&list, '\n', 5); - try std.testing.expectEqualStrings("", list.items); -} - -test "readUntilDelimiterArrayList returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { - const a = std.testing.allocator; - var list = std.ArrayList(u8).init(a); - defer list.deinit(); - - var fis = std.io.fixedBufferStream("1234567\n"); - const reader = fis.reader(); - - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterArrayList(&list, '\n', 5)); - try std.testing.expectEqualStrings("12345", list.items); - try reader.readUntilDelimiterArrayList(&list, '\n', 5); - try std.testing.expectEqualStrings("67", list.items); -} - -test "readUntilDelimiterArrayList returns EndOfStream" { - const a = std.testing.allocator; - var list = std.ArrayList(u8).init(a); - defer list.deinit(); - - var fis = std.io.fixedBufferStream("1234"); - const reader = fis.reader(); - - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterArrayList(&list, '\n', 5)); - try std.testing.expectEqualStrings("1234", list.items); -} - -test "readUntilDelimiterAlloc returns ArrayLists with bytes read until the delimiter, then EndOfStream" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("0000\n1234\n"); - const reader = fis.reader(); - - { - const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); - defer a.free(result); - try std.testing.expectEqualStrings("0000", result); - } - - { - const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); - defer a.free(result); - try std.testing.expectEqualStrings("1234", result); - } - - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterAlloc(a, '\n', 5)); -} - -test "readUntilDelimiterAlloc returns an empty ArrayList" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("\n"); - const reader = fis.reader(); - - { - const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); - defer a.free(result); - try std.testing.expectEqualStrings("", result); - } -} - -test "readUntilDelimiterAlloc returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("1234567\n"); - const reader = fis.reader(); - - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterAlloc(a, '\n', 5)); - - const result = try reader.readUntilDelimiterAlloc(a, '\n', 5); - defer a.free(result); - try std.testing.expectEqualStrings("67", result); -} - -test "readUntilDelimiterAlloc returns EndOfStream" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("1234"); - const reader = fis.reader(); - - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiterAlloc(a, '\n', 5)); -} - -test "readUntilDelimiter returns bytes read until the delimiter" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("0000\n1234\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("0000", try reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectEqualStrings("1234", try reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter returns an empty string" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("", try reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter returns StreamTooLong, then an empty string" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("12345\n"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectEqualStrings("", try reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter returns StreamTooLong, then bytes read until the delimiter" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234567\n"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectEqualStrings("67", try reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter returns EndOfStream" { - { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream(""); - const reader = fis.reader(); - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); - } - { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234"); - const reader = fis.reader(); - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); - } -} - -test "readUntilDelimiter returns bytes read until delimiter, then EndOfStream" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("1234", try reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter returns StreamTooLong, then EndOfStream" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("12345"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectError(error.EndOfStream, reader.readUntilDelimiter(&buf, '\n')); -} - -test "readUntilDelimiter writes all bytes read to the output buffer" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("0000\n12345"); - const reader = fis.reader(); - _ = try reader.readUntilDelimiter(&buf, '\n'); - try std.testing.expectEqualStrings("0000\n", &buf); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiter(&buf, '\n')); - try std.testing.expectEqualStrings("12345", &buf); -} - -test "readUntilDelimiterOrEofAlloc returns ArrayLists with bytes read until the delimiter, then EndOfStream" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("0000\n1234\n"); - const reader = fis.reader(); - - { - const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; - defer a.free(result); - try std.testing.expectEqualStrings("0000", result); - } - - { - const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; - defer a.free(result); - try std.testing.expectEqualStrings("1234", result); - } - - try std.testing.expect((try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)) == null); -} - -test "readUntilDelimiterOrEofAlloc returns an empty ArrayList" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("\n"); - const reader = fis.reader(); - - { - const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; - defer a.free(result); - try std.testing.expectEqualStrings("", result); - } -} - -test "readUntilDelimiterOrEofAlloc returns StreamTooLong, then an ArrayList with bytes read until the delimiter" { - const a = std.testing.allocator; - - var fis = std.io.fixedBufferStream("1234567\n"); - const reader = fis.reader(); - - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)); - - const result = (try reader.readUntilDelimiterOrEofAlloc(a, '\n', 5)).?; - defer a.free(result); - try std.testing.expectEqualStrings("67", result); -} - -test "readUntilDelimiterOrEof returns bytes read until the delimiter" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("0000\n1234\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("0000", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); - try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof returns an empty string" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof returns StreamTooLong, then an empty string" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("12345\n"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); - try std.testing.expectEqualStrings("", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof returns StreamTooLong, then bytes read until the delimiter" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234567\n"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); - try std.testing.expectEqualStrings("67", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof returns null" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream(""); - const reader = fis.reader(); - try std.testing.expect((try reader.readUntilDelimiterOrEof(&buf, '\n')) == null); -} - -test "readUntilDelimiterOrEof returns bytes read until delimiter, then null" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234\n"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); - try std.testing.expect((try reader.readUntilDelimiterOrEof(&buf, '\n')) == null); -} - -test "readUntilDelimiterOrEof returns bytes read until end-of-stream" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234"); - const reader = fis.reader(); - try std.testing.expectEqualStrings("1234", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof returns StreamTooLong, then bytes read until end-of-stream" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("1234567"); - const reader = fis.reader(); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); - try std.testing.expectEqualStrings("67", (try reader.readUntilDelimiterOrEof(&buf, '\n')).?); -} - -test "readUntilDelimiterOrEof writes all bytes read to the output buffer" { - var buf: [5]u8 = undefined; - var fis = std.io.fixedBufferStream("0000\n12345"); - const reader = fis.reader(); - _ = try reader.readUntilDelimiterOrEof(&buf, '\n'); - try std.testing.expectEqualStrings("0000\n", &buf); - try std.testing.expectError(error.StreamTooLong, reader.readUntilDelimiterOrEof(&buf, '\n')); - try std.testing.expectEqualStrings("12345", &buf); -} - -test "streamUntilDelimiter writes all bytes without delimiter to the output" { - const input_string = "some_string_with_delimiter!"; - var input_fbs = std.io.fixedBufferStream(input_string); - const reader = input_fbs.reader(); - - var output: [input_string.len]u8 = undefined; - var output_fbs = std.io.fixedBufferStream(&output); - const writer = output_fbs.writer(); - - try reader.streamUntilDelimiter(writer, '!', input_fbs.buffer.len); - try std.testing.expectEqualStrings("some_string_with_delimiter", output_fbs.getWritten()); - try std.testing.expectError(error.EndOfStream, reader.streamUntilDelimiter(writer, '!', input_fbs.buffer.len)); - - input_fbs.reset(); - output_fbs.reset(); - - try std.testing.expectError(error.StreamTooLong, reader.streamUntilDelimiter(writer, '!', 5)); -} - -test "readBoundedBytes correctly reads into a new bounded array" { - const test_string = "abcdefg"; - var fis = std.io.fixedBufferStream(test_string); - const reader = fis.reader(); - - var array = try reader.readBoundedBytes(10000); - try testing.expectEqualStrings(array.slice(), test_string); -} - -test "readIntoBoundedBytes correctly reads into a provided bounded array" { - const test_string = "abcdefg"; - var fis = std.io.fixedBufferStream(test_string); - const reader = fis.reader(); - - var bounded_array = std.BoundedArray(u8, 10000){}; - - // compile time error if the size is not the same at the provided `bounded.capacity()` - try reader.readIntoBoundedBytes(10000, &bounded_array); - try testing.expectEqualStrings(bounded_array.slice(), test_string); -} diff --git a/lib/std/io/Writer.zig b/lib/std/io/Writer.zig deleted file mode 100644 index d79959dcb15647da04df897a112b2c758cd2cb43..0000000000000000000000000000000000000000 --- a/lib/std/io/Writer.zig +++ /dev/null @@ -1,2486 +0,0 @@ -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 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, - -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. - /// - /// This function may not be called if the data to be written could have - /// been stored in `buffer` instead, including when the amount of data to - /// be written is zero and the buffer capacity is 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, -}; - -pub const FileAllError = error{ - /// Detailed diagnostics are found on the `File.Reader` struct. - ReadFailed, - /// See the `Writer` implementation for detailed diagnostics. - WriteFailed, -}; - -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 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. -pub fn fixed(buffer: []u8) Writer { - return .{ - .vtable = &.{ .drain = fixedDrain }, - .buffer = buffer, - }; -} - -pub fn hashed(w: *Writer, hasher: anytype, buffer: []u8) Hashed(@TypeOf(hasher)) { - return .initHasher(w, hasher, buffer); -} - -pub const failing: Writer = .{ - .vtable = &.{ - .drain = failingDrain, - .sendFile = failingSendFile, - }, -}; - -/// 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) return w.vtable.drain(w, data, splat); - for (data[0 .. data.len - 1]) |bytes| { - @memcpy(buffer[w.end..][0..bytes.len], bytes); - w.end += bytes.len; - } - const pattern = data[data.len - 1]; - switch (pattern.len) { - 0 => {}, - 1 => { - @memset(buffer[w.end..][0..splat], pattern[0]); - w.end += splat; - }, - else => for (0..splat) |_| { - @memcpy(buffer[w.end..][0..pattern.len], pattern); - w.end += pattern.len; - }, - } - return count; -} - -/// 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; - 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[0 .. data.len - 1]) |buf| { - if (buf.len == 0) continue; - vecs[i] = buf; - i += 1; - if (vecs.len - i == 0) break; - } - const pattern = data[data.len - 1]; - const new_splat = s: { - if (pattern.len == 0 or vecs.len - i == 0) break :s 1; - vecs[i] = pattern; - i += 1; - break :s splat; - }; - return w.vtable.drain(w, vecs[0..i], new_splat); -} - -test "writeSplatHeader splatting avoids buffer aliasing temptation" { - const initial_buf = try testing.allocator.alloc(u8, 8); - var aw: std.io.Writer.Allocating = .initOwnedSlice(testing.allocator, initial_buf); - defer aw.deinit(); - // This test assumes 8 vector buffer in this function. - const n = try aw.writer.writeSplatHeader("header which is longer than buf ", &.{ - "1", "2", "3", "4", "5", "6", "foo", "bar", "foo", - }, 3); - try testing.expectEqual(41, n); - try testing.expectEqualStrings( - "header which is longer than buf 123456foo", - aw.writer.buffered(), - ); -} - -/// 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..]; - } -} - -/// 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, - /// Tracks whether the "writable vector" API was used. - used: bool = false, - pub const vtable: *const VTable = &unique_vtable_allocation; - /// This is intended to be constant but it must be a unique address for - /// `@fieldParentPtr` to work. - var unique_vtable_allocation: VTable = .{ .drain = fixedDrain }; -}; - -pub fn writableVectorIterator(w: *Writer) Error!WritableVectorIterator { - if (w.vtable == VectorWrapper.vtable) { - const wrapper: *VectorWrapper = @fieldParentPtr("writer", w); - wrapper.used = true; - 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; - 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; -} - -/// 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; -} - -/// After calling `writableVector`, this function tracks how many bytes were -/// written to it. -pub fn advanceVector(w: *Writer, n: usize) usize { - 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; - } - } -} - -/// 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; - return bytes.len; - } - return w.vtable.drain(w, &.{bytes}, 1); -} - -/// 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; - 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); - 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..]); -} - -/// Renders fmt string with args, calling `writer` with slices of bytes. -/// If `writer` returns an error, the error is returned from `format` and -/// `writer` is not called again. -/// -/// The format string must be comptime-known and may contain placeholders following -/// this format: -/// `{[argument][specifier]:[fill][alignment][width].[precision]}` -/// -/// Above, each word including its surrounding [ and ] is a parameter which you have to replace with something: -/// -/// - *argument* is either the numeric index or the field name of the argument that should be inserted -/// - when using a field name, you are required to enclose the field name (an identifier) in square -/// brackets, e.g. {[score]...} as opposed to the numeric index form which can be written e.g. {2...} -/// - *specifier* is a type-dependent formatting option that determines how a type should formatted (see below) -/// - *fill* is a single byte which is used to pad formatted numbers. -/// - *alignment* is one of the three bytes '<', '^', or '>' to make numbers -/// left, center, or right-aligned, respectively. -/// - Not all specifiers support alignment. -/// - Alignment is not Unicode-aware; appropriate only when used with raw bytes or ASCII. -/// - *width* is the total width of the field in bytes. This only applies to number formatting. -/// - *precision* specifies how many decimals a formatted number should have. -/// -/// Note that most of the parameters are optional and may be omitted. Also you -/// can leave out separators like `:` and `.` when all parameters after the -/// separator are omitted. -/// -/// Only exception is the *fill* parameter. If a non-zero *fill* character is -/// required at the same time as *width* is specified, one has to specify -/// *alignment* as well, as otherwise the digit following `:` is interpreted as -/// *width*, not *fill*. -/// -/// The *specifier* has several options for types: -/// - `x` and `X`: output numeric value in hexadecimal notation, or string in hexadecimal bytes -/// - `s`: -/// - for pointer-to-many and C pointers of u8, print as a C-string using zero-termination -/// - for slices of u8, print the entire slice as a string without zero-termination -/// - `t`: -/// - for enums and tagged unions: prints the tag name -/// - for error sets: prints the error name -/// - `b64`: output string as standard base64 -/// - `e`: output floating point value in scientific notation -/// - `d`: output numeric value in decimal notation -/// - `b`: output integer value in binary notation -/// - `o`: output integer value in octal notation -/// - `c`: output integer as an ASCII character. Integer type must have 8 bits at max. -/// - `u`: output integer as an UTF-8 sequence. Integer type must have 21 bits at max. -/// - `D`: output nanoseconds as duration -/// - `B`: output bytes in SI units (decimal) -/// - `Bi`: output bytes in IEC units (binary) -/// - `?`: output optional value as either the unwrapped value, or `null`; may be followed by a format specifier for the underlying value. -/// - `!`: output error union value as either the unwrapped value, or the formatted error value; may be followed by a format specifier for the underlying value. -/// - `*`: output the address of the value instead of the value itself. -/// - `any`: output a value of any type using its default format. -/// - `f`: delegates to a method on the type named "format" with the signature `fn (*Writer, args: anytype) Writer.Error!void`. -/// -/// A user type may be a `struct`, `vector`, `union` or `enum` type. -/// -/// To print literal curly braces, escape them by writing them twice, e.g. `{{` or `}}`. -pub fn print(w: *Writer, comptime fmt: []const u8, args: anytype) Error!void { - const ArgsType = @TypeOf(args); - const args_type_info = @typeInfo(ArgsType); - if (args_type_info != .@"struct") { - @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); - } - - const fields_info = args_type_info.@"struct".fields; - const max_format_args = @typeInfo(std.fmt.ArgSetType).int.bits; - if (fields_info.len > max_format_args) { - @compileError("32 arguments max are supported per format call"); - } - - @setEvalBranchQuota(fmt.len * 1000); - comptime var arg_state: std.fmt.ArgState = .{ .args_len = fields_info.len }; - comptime var i = 0; - comptime var literal: []const u8 = ""; - inline while (true) { - const start_index = i; - - inline while (i < fmt.len) : (i += 1) { - switch (fmt[i]) { - '{', '}' => break, - else => {}, - } - } - - comptime var end_index = i; - comptime var unescape_brace = false; - - // Handle {{ and }}, those are un-escaped as single braces - if (i + 1 < fmt.len and fmt[i + 1] == fmt[i]) { - unescape_brace = true; - // Make the first brace part of the literal... - end_index += 1; - // ...and skip both - i += 2; - } - - literal = literal ++ fmt[start_index..end_index]; - - // We've already skipped the other brace, restart the loop - if (unescape_brace) continue; - - // Write out the literal - if (literal.len != 0) { - try w.writeAll(literal); - literal = ""; - } - - if (i >= fmt.len) break; - - if (fmt[i] == '}') { - @compileError("missing opening {"); - } - - // Get past the { - comptime assert(fmt[i] == '{'); - i += 1; - - const fmt_begin = i; - // Find the closing brace - inline while (i < fmt.len and fmt[i] != '}') : (i += 1) {} - const fmt_end = i; - - if (i >= fmt.len) { - @compileError("missing closing }"); - } - - // Get past the } - comptime assert(fmt[i] == '}'); - i += 1; - - const placeholder_array = fmt[fmt_begin..fmt_end].*; - const placeholder = comptime std.fmt.Placeholder.parse(&placeholder_array); - const arg_pos = comptime switch (placeholder.arg) { - .none => null, - .number => |pos| pos, - .named => |arg_name| std.meta.fieldIndex(ArgsType, arg_name) orelse - @compileError("no argument with name '" ++ arg_name ++ "'"), - }; - - const width = switch (placeholder.width) { - .none => null, - .number => |v| v, - .named => |arg_name| blk: { - const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse - @compileError("no argument with name '" ++ arg_name ++ "'"); - _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); - break :blk @field(args, arg_name); - }, - }; - - const precision = switch (placeholder.precision) { - .none => null, - .number => |v| v, - .named => |arg_name| blk: { - const arg_i = comptime std.meta.fieldIndex(ArgsType, arg_name) orelse - @compileError("no argument with name '" ++ arg_name ++ "'"); - _ = comptime arg_state.nextArg(arg_i) orelse @compileError("too few arguments"); - break :blk @field(args, arg_name); - }, - }; - - const arg_to_print = comptime arg_state.nextArg(arg_pos) orelse - @compileError("too few arguments"); - - try w.printValue( - placeholder.specifier_arg, - .{ - .fill = placeholder.fill, - .alignment = placeholder.alignment, - .width = width, - .precision = precision, - }, - @field(args, fields_info[arg_to_print].name), - std.options.fmt_max_depth, - ); - } - - if (comptime arg_state.hasUnusedArgs()) { - const missing_count = arg_state.args_len - @popCount(arg_state.used_args); - switch (missing_count) { - 0 => unreachable, - 1 => @compileError("unused argument in '" ++ fmt ++ "'"), - else => @compileError(std.fmt.comptimePrint("{d}", .{missing_count}) ++ " unused arguments in '" ++ fmt ++ "'"), - } - } -} - -/// 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) return; - } else { - @branchHint(.likely); - w.buffer[w.end] = byte; - w.end += 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; - } -} - -/// 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; - std.mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian); - return w.writeAll(&bytes); -} - -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 w.writeAll(std.mem.asBytes(&value)); -} - -/// 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 { - switch (@typeInfo(@TypeOf(value))) { - .@"struct" => |info| switch (info.layout) { - .auto => @compileError("ill-defined memory layout"), - .@"extern" => { - if (native_endian == endian) { - return w.writeStruct(value); - } else { - var copy = value; - std.mem.byteSwapAllFields(@TypeOf(value), ©); - return w.writeStruct(copy); - } - }, - .@"packed" => { - return writeInt(w, info.backing_integer.?, @bitCast(value), endian); - }, - }, - else => @compileError("not a struct"), - } -} - -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); - } -} - -/// 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; - 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); - 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; -} - -/// Number of bytes logically written is returned. This excludes bytes from -/// `buffer` because they have already been logically written. -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 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) ++ "@"); - const int = if (info.size == .slice) @intFromPtr(value.ptr) else @intFromPtr(value); - return w.printInt(int, 16, .lower, .{}); - }, - .optional => |info| { - if (@typeInfo(info.child) == .pointer) { - try w.writeAll(@typeName(info.child) ++ "@"); - try w.printInt(@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); - - switch (fmt.len) { - 1 => switch (fmt[0]) { - '*' => return w.printAddress(value), - 'f' => return value.format(w), - 'd' => switch (@typeInfo(T)) { - .float, .comptime_float => return printFloat(w, value, options.toNumber(.decimal, .lower)), - .int, .comptime_int => return printInt(w, value, 10, .lower, options), - .@"struct" => return value.formatNumber(w, options.toNumber(.decimal, .lower)), - .@"enum" => return printInt(w, @intFromEnum(value), 10, .lower, options), - .vector => return printVector(w, fmt, options, value, max_depth), - else => invalidFmtError(fmt, value), - }, - 'c' => return w.printAsciiChar(value, options), - 'u' => return w.printUnicodeCodepoint(value), - 'b' => switch (@typeInfo(T)) { - .int, .comptime_int => return printInt(w, value, 2, .lower, options), - .@"enum" => return printInt(w, @intFromEnum(value), 2, .lower, options), - .@"struct" => return value.formatNumber(w, options.toNumber(.binary, .lower)), - .vector => return printVector(w, fmt, options, value, max_depth), - else => invalidFmtError(fmt, value), - }, - 'o' => switch (@typeInfo(T)) { - .int, .comptime_int => return printInt(w, value, 8, .lower, options), - .@"enum" => return printInt(w, @intFromEnum(value), 8, .lower, options), - .@"struct" => return value.formatNumber(w, options.toNumber(.octal, .lower)), - .vector => return printVector(w, fmt, options, value, max_depth), - else => invalidFmtError(fmt, value), - }, - 'x' => switch (@typeInfo(T)) { - .float, .comptime_float => return printFloatHexOptions(w, value, options.toNumber(.hex, .lower)), - .int, .comptime_int => return printInt(w, value, 16, .lower, options), - .@"enum" => return printInt(w, @intFromEnum(value), 16, .lower, options), - .@"struct" => return value.formatNumber(w, options.toNumber(.hex, .lower)), - .pointer => |info| switch (info.size) { - .one, .slice => { - const slice: []const u8 = value; - optionsForbidden(options); - return printHex(w, slice, .lower); - }, - .many, .c => { - const slice: [:0]const u8 = std.mem.span(value); - optionsForbidden(options); - return printHex(w, slice, .lower); - }, - }, - .array => { - const slice: []const u8 = &value; - optionsForbidden(options); - return printHex(w, slice, .lower); - }, - .vector => return printVector(w, fmt, options, value, max_depth), - else => invalidFmtError(fmt, value), - }, - 'X' => switch (@typeInfo(T)) { - .float, .comptime_float => return printFloatHexOptions(w, value, options.toNumber(.hex, .lower)), - .int, .comptime_int => return printInt(w, value, 16, .upper, options), - .@"enum" => return printInt(w, @intFromEnum(value), 16, .upper, options), - .@"struct" => return value.formatNumber(w, options.toNumber(.hex, .upper)), - .pointer => |info| switch (info.size) { - .one, .slice => { - const slice: []const u8 = value; - optionsForbidden(options); - return printHex(w, slice, .upper); - }, - .many, .c => { - const slice: [:0]const u8 = std.mem.span(value); - optionsForbidden(options); - return printHex(w, slice, .upper); - }, - }, - .array => { - const slice: []const u8 = &value; - optionsForbidden(options); - return printHex(w, slice, .upper); - }, - .vector => return printVector(w, fmt, options, value, max_depth), - else => invalidFmtError(fmt, value), - }, - 's' => switch (@typeInfo(T)) { - .pointer => |info| switch (info.size) { - .one, .slice => { - const slice: []const u8 = value; - return w.alignBufferOptions(slice, options); - }, - .many, .c => { - const slice: [:0]const u8 = std.mem.span(value); - return w.alignBufferOptions(slice, options); - }, - }, - .array => { - const slice: []const u8 = &value; - return w.alignBufferOptions(slice, options); - }, - else => invalidFmtError(fmt, value), - }, - 'B' => switch (@typeInfo(T)) { - .int, .comptime_int => return w.printByteSize(value, .decimal, options), - .@"struct" => return value.formatByteSize(w, .decimal), - else => invalidFmtError(fmt, value), - }, - 'D' => switch (@typeInfo(T)) { - .int, .comptime_int => return w.printDuration(value, options), - .@"struct" => return value.formatDuration(w), - else => invalidFmtError(fmt, value), - }, - 'e' => switch (@typeInfo(T)) { - .float, .comptime_float => return printFloat(w, value, options.toNumber(.scientific, .lower)), - .@"struct" => return value.formatNumber(w, options.toNumber(.scientific, .lower)), - else => invalidFmtError(fmt, value), - }, - 'E' => switch (@typeInfo(T)) { - .float, .comptime_float => return printFloat(w, value, options.toNumber(.scientific, .upper)), - .@"struct" => return value.formatNumber(w, options.toNumber(.scientific, .upper)), - else => invalidFmtError(fmt, value), - }, - 't' => switch (@typeInfo(T)) { - .error_set => return w.writeAll(@errorName(value)), - .@"enum", .@"union" => return w.writeAll(@tagName(value)), - else => invalidFmtError(fmt, value), - }, - else => {}, - }, - 2 => switch (fmt[0]) { - 'B' => switch (fmt[1]) { - 'i' => switch (@typeInfo(T)) { - .int, .comptime_int => return w.printByteSize(value, .binary, options), - .@"struct" => return value.formatByteSize(w, .binary), - else => invalidFmtError(fmt, value), - }, - else => {}, - }, - else => {}, - }, - 3 => if (fmt[0] == 'b' and fmt[1] == '6' and fmt[2] == '4') switch (@typeInfo(T)) { - .pointer => |info| switch (info.size) { - .one, .slice => { - const slice: []const u8 = value; - optionsForbidden(options); - return w.printBase64(slice); - }, - .many, .c => { - const slice: [:0]const u8 = std.mem.span(value); - optionsForbidden(options); - return w.printBase64(slice); - }, - }, - .array => { - const slice: []const u8 = &value; - optionsForbidden(options); - return w.printBase64(slice); - }, - else => invalidFmtError(fmt, value), - }, - else => {}, - } - - const is_any = comptime std.mem.eql(u8, fmt, ANY); - if (!is_any and std.meta.hasMethod(T, "format") and fmt.len == 0) { - // after 0.15.0 is tagged, delete this compile error and its condition - @compileError("ambiguous format string; specify {f} to call format method, or {any} to skip it"); - } - - switch (@typeInfo(T)) { - .float, .comptime_float => { - if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - return printFloat(w, value, options.toNumber(.decimal, .lower)); - }, - .int, .comptime_int => { - if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - return printInt(w, value, 10, .lower, options); - }, - .bool => { - if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - const string: []const u8 = if (value) "true" else "false"; - return w.alignBufferOptions(string, 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 (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - optionsForbidden(options); - return printErrorSet(w, value); - }, - .@"enum" => |info| { - if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - optionsForbidden(options); - if (info.is_exhaustive) { - return printEnumExhaustive(w, value); - } else { - return printEnumNonexhaustive(w, value); - } - }, - .@"union" => |info| { - if (!is_any) { - if (fmt.len != 0) invalidFmtError(fmt, value); - return printValue(w, ANY, options, value, max_depth); - } - 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 switch (info.layout) { - .auto => { - return w.writeAll(".{ ... }"); - }, - .@"extern", .@"packed" => { - if (info.fields.len == 0) return w.writeAll(".{}"); - try w.writeAll(".{ "); - inline for (info.fields) |field| { - try w.writeByte('.'); - try w.writeAll(field.name); - try w.writeAll(" = "); - try w.printValue(ANY, options, @field(value, field.name), max_depth - 1); - (try w.writableArray(2)).* = ", ".*; - } - w.buffer[w.end - 2 ..][0..2].* = " }".*; - }, - } - }, - .@"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; - } - 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 => |array_info| return w.printValue(fmt, options, @as([]const array_info.child, value), max_depth), - .@"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.printInt(@intFromPtr(value), 16, .lower, options); - return; - }, - }, - .many, .c => { - if (!is_any) @compileError("cannot format pointer without a specifier (i.e. {s} or {*})"); - optionsForbidden(options); - try w.printAddress(value); - }, - .slice => { - if (!is_any) - @compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})"); - if (max_depth == 0) return w.writeAll("{ ... }"); - 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 => { - if (!is_any) @compileError("cannot format array without a specifier (i.e. {s} or {any})"); - if (max_depth == 0) return w.writeAll("{ ... }"); - 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 => { - if (!is_any and fmt.len != 0) invalidFmtError(fmt, value); - return printVector(w, fmt, options, value, max_depth); - }, - .@"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); - optionsForbidden(options); - var vecs: [2][]const u8 = .{ ".", @tagName(value) }; - return w.writeVecAll(&vecs); - }, - .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 optionsForbidden(options: std.fmt.Options) void { - assert(options.precision == null); - assert(options.width == null); -} - -fn printErrorSet(w: *Writer, error_set: anyerror) Error!void { - var vecs: [2][]const u8 = .{ "error.", @errorName(error_set) }; - try w.writeVecAll(&vecs); -} - -fn printEnumExhaustive(w: *Writer, value: anytype) Error!void { - var vecs: [2][]const u8 = .{ ".", @tagName(value) }; - try w.writeVecAll(&vecs); -} - -fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void { - if (std.enums.tagName(@TypeOf(value), value)) |tag_name| { - var vecs: [2][]const u8 = .{ ".", tag_name }; - try w.writeVecAll(&vecs); - return; - } - try w.writeAll("@enumFromInt("); - try w.printInt(@intFromEnum(value), 10, .lower, .{}); - try w.writeByte(')'); -} - -pub fn printVector( - w: *Writer, - comptime fmt: []const u8, - options: std.fmt.Options, - value: anytype, - max_depth: usize, -) Error!void { - const len = @typeInfo(@TypeOf(value)).vector.len; - if (max_depth == 0) return w.writeAll("{ ... }"); - try w.writeAll("{ "); - inline for (0..len) |i| { - try w.printValue(fmt, options, value[i], max_depth - 1); - if (i < len - 1) try w.writeAll(", "); - } - try w.writeAll(" }"); -} - -// A wrapper around `printIntAny` to avoid the generic explosion of this -// function by funneling smaller integer types through `isize` and `usize`. -pub inline fn printInt( - w: *Writer, - value: anytype, - base: u8, - case: std.fmt.Case, - options: std.fmt.Options, -) Error!void { - switch (@TypeOf(value)) { - isize, usize => {}, - comptime_int => { - if (comptime std.math.cast(usize, value)) |x| return printIntAny(w, x, base, case, options); - if (comptime std.math.cast(isize, value)) |x| return printIntAny(w, x, base, case, options); - const Int = std.math.IntFittingRange(value, value); - return printIntAny(w, @as(Int, value), base, case, options); - }, - else => switch (@typeInfo(@TypeOf(value)).int.signedness) { - .signed => if (std.math.cast(isize, value)) |x| return printIntAny(w, x, base, case, options), - .unsigned => if (std.math.cast(usize, value)) |x| return printIntAny(w, x, base, case, options), - }, - } - return printIntAny(w, value, base, case, options); -} - -/// In general, prefer `printInt` to avoid generic explosion. However this -/// function may be used when optimal codegen for a particular integer type is -/// desired. -pub fn printIntAny( - w: *Writer, - value: anytype, - base: u8, - case: std.fmt.Case, - options: std.fmt.Options, -) Error!void { - assert(base >= 2); - const value_info = @typeInfo(@TypeOf(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(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 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) Error!void { - var buf: [4]u8 = undefined; - const len = std.unicode.utf8Encode(c, &buf) catch |err| switch (err) { - error.Utf8CannotEncodeSurrogateHalf, error.CodepointTooLarge => l: { - buf[0..3].* = std.unicode.replacement_character_utf8; - break :l 3; - }, - }; - return w.writeAll(buf[0..len]); -} - -/// Uses a larger stack buffer; asserts mode is decimal or scientific. -pub fn printFloat(w: *Writer, value: anytype, options: std.fmt.Number) Error!void { - const mode: std.fmt.float.Mode = switch (options.mode) { - .decimal => .decimal, - .scientific => .scientific, - .binary, .octal, .hex => unreachable, - }; - var buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined; - const s = std.fmt.float.render(&buf, value, .{ - .mode = mode, - .precision = options.precision, - }) catch |err| switch (err) { - error.BufferTooSmall => "(float)", - }; - return w.alignBuffer(s, options.width orelse s.len, options.alignment, options.fill); -} - -/// Uses a smaller stack buffer; asserts mode is not decimal or scientific. -pub fn printFloatHexOptions(w: *Writer, value: anytype, options: std.fmt.Number) Error!void { - var buf: [50]u8 = undefined; // for aligning - var sub_writer: Writer = .fixed(&buf); - switch (options.mode) { - .decimal => unreachable, - .scientific => unreachable, - .binary => @panic("TODO"), - .octal => @panic("TODO"), - .hex => {}, - } - printFloatHex(&sub_writer, value, options.case, options.precision) catch unreachable; // buf is large enough - - const printed = sub_writer.buffered(); - return w.alignBuffer(printed, options.width orelse printed.len, options.alignment, options.fill); -} - -pub fn printFloatHex(w: *Writer, value: anytype, case: std.fmt.Case, opt_precision: ?usize) Error!void { - if (std.math.signbit(value)) try w.writeByte('-'); - if (std.math.isNan(value)) return w.writeAll(switch (case) { - .lower => "nan", - .upper => "NAN", - }); - if (std.math.isInf(value)) return w.writeAll(switch (case) { - .lower => "inf", - .upper => "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, case, .{ .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.printInt(exponent - exponent_bias, 10, case, .{}); -} - -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.printInt(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.printInt(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.printInt(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.printInt(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_writer: Writer = .fixed(&buf); - if (@TypeOf(nanoseconds) == comptime_int) { - if (nanoseconds >= 0) { - sub_writer.printDurationUnsigned(nanoseconds) catch unreachable; - } else { - sub_writer.printDurationSigned(nanoseconds) catch unreachable; - } - } else switch (@typeInfo(@TypeOf(nanoseconds)).int.signedness) { - .signed => sub_writer.printDurationSigned(nanoseconds) catch unreachable, - .unsigned => sub_writer.printDurationUnsigned(nanoseconds) catch unreachable, - } - return w.alignBufferOptions(sub_writer.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 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 "printValue max_depth" { - const Vec2 = struct { - const SelfType = @This(); - x: f32, - y: f32, - - pub fn format(self: SelfType, w: *Writer) Error!void { - return w.print("({d:.3},{d:.3})", .{ self.x, self.y }); - } - }; - 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(".{ ... }", w.buffered()); - - w = .fixed(&buf); - try w.printValue("", .{}, inst, 1); - try testing.expectEqualStrings(".{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }", w.buffered()); - - w = .fixed(&buf); - try w.printValue("", .{}, inst, 2); - try testing.expectEqualStrings(".{ .a = .{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }, .tu = .{ .ptr = .{ ... } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }", w.buffered()); - - w = .fixed(&buf); - try w.printValue("", .{}, inst, 3); - try testing.expectEqualStrings(".{ .a = .{ .a = .{ .a = .{ ... }, .tu = .{ ... }, .e = .Two, .vec = .{ ... } }, .tu = .{ .ptr = .{ ... } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }, .tu = .{ .ptr = .{ .ptr = .{ ... } } }, .e = .Two, .vec = .{ .x = 10.2, .y = 2.22 } }", w.buffered()); - - const vec: @Vector(4, i32) = .{ 1, 2, 3, 4 }; - w = .fixed(&buf); - try w.printValue("", .{}, vec, 0); - try testing.expectEqualStrings("{ ... }", w.buffered()); - - w = .fixed(&buf); - try w.printValue("", .{}, vec, 1); - try testing.expectEqualStrings("{ 1, 2, 3, 4 }", w.buffered()); -} - -test printDuration { - try testDurationCase("0ns", 0); - try testDurationCase("1ns", 1); - try testDurationCase("999ns", std.time.ns_per_us - 1); - try testDurationCase("1us", std.time.ns_per_us); - try testDurationCase("1.45us", 1450); - try testDurationCase("1.5us", 3 * std.time.ns_per_us / 2); - try testDurationCase("14.5us", 14500); - try testDurationCase("145us", 145000); - try testDurationCase("999.999us", std.time.ns_per_ms - 1); - try testDurationCase("1ms", std.time.ns_per_ms + 1); - try testDurationCase("1.5ms", 3 * std.time.ns_per_ms / 2); - try testDurationCase("1.11ms", 1110000); - try testDurationCase("1.111ms", 1111000); - try testDurationCase("1.111ms", 1111100); - try testDurationCase("999.999ms", std.time.ns_per_s - 1); - try testDurationCase("1s", std.time.ns_per_s); - try testDurationCase("59.999s", std.time.ns_per_min - 1); - try testDurationCase("1m", std.time.ns_per_min); - try testDurationCase("1h", std.time.ns_per_hour); - try testDurationCase("1d", std.time.ns_per_day); - try testDurationCase("1w", std.time.ns_per_week); - try testDurationCase("1y", 365 * std.time.ns_per_day); - try testDurationCase("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1 - try testDurationCase("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); - try testDurationCase("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); - try testDurationCase("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); - try testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); - try testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); - try testDurationCase("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); - try testDurationCase("584y49w23h34m33.709s", std.math.maxInt(u64)); - - try testing.expectFmt("=======0ns", "{D:=>10}", .{0}); - try testing.expectFmt("1ns=======", "{D:=<10}", .{1}); - try testing.expectFmt(" 999ns ", "{D:^10}", .{std.time.ns_per_us - 1}); -} - -test printDurationSigned { - try testDurationCaseSigned("0ns", 0); - try testDurationCaseSigned("1ns", 1); - try testDurationCaseSigned("-1ns", -(1)); - try testDurationCaseSigned("999ns", std.time.ns_per_us - 1); - try testDurationCaseSigned("-999ns", -(std.time.ns_per_us - 1)); - try testDurationCaseSigned("1us", std.time.ns_per_us); - try testDurationCaseSigned("-1us", -(std.time.ns_per_us)); - try testDurationCaseSigned("1.45us", 1450); - try testDurationCaseSigned("-1.45us", -(1450)); - try testDurationCaseSigned("1.5us", 3 * std.time.ns_per_us / 2); - try testDurationCaseSigned("-1.5us", -(3 * std.time.ns_per_us / 2)); - try testDurationCaseSigned("14.5us", 14500); - try testDurationCaseSigned("-14.5us", -(14500)); - try testDurationCaseSigned("145us", 145000); - try testDurationCaseSigned("-145us", -(145000)); - try testDurationCaseSigned("999.999us", std.time.ns_per_ms - 1); - try testDurationCaseSigned("-999.999us", -(std.time.ns_per_ms - 1)); - try testDurationCaseSigned("1ms", std.time.ns_per_ms + 1); - try testDurationCaseSigned("-1ms", -(std.time.ns_per_ms + 1)); - try testDurationCaseSigned("1.5ms", 3 * std.time.ns_per_ms / 2); - try testDurationCaseSigned("-1.5ms", -(3 * std.time.ns_per_ms / 2)); - try testDurationCaseSigned("1.11ms", 1110000); - try testDurationCaseSigned("-1.11ms", -(1110000)); - try testDurationCaseSigned("1.111ms", 1111000); - try testDurationCaseSigned("-1.111ms", -(1111000)); - try testDurationCaseSigned("1.111ms", 1111100); - try testDurationCaseSigned("-1.111ms", -(1111100)); - try testDurationCaseSigned("999.999ms", std.time.ns_per_s - 1); - try testDurationCaseSigned("-999.999ms", -(std.time.ns_per_s - 1)); - try testDurationCaseSigned("1s", std.time.ns_per_s); - try testDurationCaseSigned("-1s", -(std.time.ns_per_s)); - try testDurationCaseSigned("59.999s", std.time.ns_per_min - 1); - try testDurationCaseSigned("-59.999s", -(std.time.ns_per_min - 1)); - try testDurationCaseSigned("1m", std.time.ns_per_min); - try testDurationCaseSigned("-1m", -(std.time.ns_per_min)); - try testDurationCaseSigned("1h", std.time.ns_per_hour); - try testDurationCaseSigned("-1h", -(std.time.ns_per_hour)); - try testDurationCaseSigned("1d", std.time.ns_per_day); - try testDurationCaseSigned("-1d", -(std.time.ns_per_day)); - try testDurationCaseSigned("1w", std.time.ns_per_week); - try testDurationCaseSigned("-1w", -(std.time.ns_per_week)); - try testDurationCaseSigned("1y", 365 * std.time.ns_per_day); - try testDurationCaseSigned("-1y", -(365 * std.time.ns_per_day)); - try testDurationCaseSigned("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1d - try testDurationCaseSigned("-1y52w23h59m59.999s", -(730 * std.time.ns_per_day - 1)); // 365d = 52w1d - try testDurationCaseSigned("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms); - try testDurationCaseSigned("-1y1h1.001s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms)); - try testDurationCaseSigned("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us); - try testDurationCaseSigned("-1y1h1s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us)); - try testDurationCaseSigned("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1); - try testDurationCaseSigned("-1y1h999.999us", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1)); - try testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms); - try testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms)); - try testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1); - try testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1)); - try testDurationCaseSigned("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999); - try testDurationCaseSigned("-1y1m999ns", -(365 * std.time.ns_per_day + std.time.ns_per_min + 999)); - try testDurationCaseSigned("292y24w3d23h47m16.854s", std.math.maxInt(i64)); - try testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64) + 1); - try testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64)); - - try testing.expectFmt("=======0ns", "{D:=>10}", .{0}); - try testing.expectFmt("1ns=======", "{D:=<10}", .{1}); - try testing.expectFmt("-1ns======", "{D:=<10}", .{-(1)}); - try testing.expectFmt(" -999ns ", "{D:^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 printInt { - 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 }); - - try testPrintIntCase("123456789123456789", @as(comptime_int, 123456789123456789), 10, .lower, .{}); -} - -test "printFloat with comptime_float" { - var buf: [20]u8 = undefined; - var w: Writer = .fixed(&buf); - try w.printFloat(@as(comptime_float, 1.0), std.fmt.Options.toNumber(.{}, .scientific, .lower)); - try testing.expectEqualStrings(w.buffered(), "1e0"); - try testing.expectFmt("1", "{}", .{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); - try w.printInt(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 testing.expectFmt("lowercase: cafebabe\n", "lowercase: {x}\n", .{some_bytes}); - try testing.expectFmt("uppercase: CAFEBABE\n", "uppercase: {X}\n", .{some_bytes}); - try testing.expectFmt("uppercase: CAFE\n", "uppercase: {X}\n", .{some_bytes[0..2]}); - try testing.expectFmt("lowercase: babe\n", "lowercase: {x}\n", .{some_bytes[2..]}); - const bytes_with_zeros = "\x00\x0E\xBA\xBE"; - try 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.WriteFailed, w.writeAll("!")); - try testing.expect(std.mem.eql(u8, w.buffered(), "Helloworld")); - - w = .fixed(&buffer); - - try testing.expect(w.buffered().len == 0); - - try testing.expectError(error.WriteFailed, w.writeAll("Hello world!")); - try testing.expect(std.mem.eql(u8, w.buffered(), "Hello worl")); -} - -test "writeSplat 0 len splat larger than capacity" { - var buf: [8]u8 = undefined; - var w: std.io.Writer = .fixed(&buf); - const n = try w.writeSplat(&.{"something that overflows buf"}, 0); - try testing.expectEqual(0, n); -} - -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 const Discarding = struct { - count: u64, - writer: Writer, - - pub fn init(buffer: []u8) Discarding { - return .{ - .count = 0, - .writer = .{ - .vtable = &.{ - .drain = Discarding.drain, - .sendFile = Discarding.sendFile, - }, - .buffer = buffer, - }, - }; - } - - pub fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { - const d: *Discarding = @alignCast(@fieldParentPtr("writer", w)); - const slice = data[0 .. data.len - 1]; - const pattern = data[slice.len..]; - var written: usize = pattern.len * splat; - for (slice) |bytes| written += bytes.len; - d.count += w.end + written; - w.end = 0; - return written; - } - - pub fn sendFile(w: *Writer, file_reader: *File.Reader, limit: Limit) FileError!usize { - if (File.Handle == void) return error.Unimplemented; - const d: *Discarding = @alignCast(@fieldParentPtr("writer", w)); - d.count += w.end; - w.end = 0; - if (file_reader.getSize()) |size| { - const n = limit.minInt64(size - file_reader.pos); - file_reader.seekBy(@intCast(n)) catch return error.Unimplemented; - w.end = 0; - d.count += n; - 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, - writer: Writer, - - pub fn init(out: *Writer, buffer: []u8) @This() { - return .initHasher(out, .{}, buffer); - } - - pub fn initHasher(out: *Writer, hasher: Hasher, buffer: []u8) @This() { - return .{ - .out = out, - .hasher = hasher, - .writer = .{ - .buffer = buffer, - .vtable = &.{ .drain = @This().drain }, - }, - }; - } - - fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { - const this: *@This() = @alignCast(@fieldParentPtr("writer", w)); - const aux = w.buffered(); - const aux_n = try this.out.writeSplatHeader(aux, 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(aux); - const n = aux_n - w.end; - w.end = 0; - var remaining: usize = n; - for (data[0 .. data.len - 1]) |slice| { - if (remaining <= slice.len) { - this.hasher.update(slice[0..remaining]); - return n; - } - remaining -= slice.len; - this.hasher.update(slice); - } - const pattern = data[data.len - 1]; - assert(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, - writer: Writer, - - pub fn init(allocator: Allocator) Allocating { - return .{ - .allocator = allocator, - .writer = .{ - .buffer = &.{}, - .vtable = &vtable, - }, - }; - } - - pub fn initCapacity(allocator: Allocator, capacity: usize) error{OutOfMemory}!Allocating { - return .{ - .allocator = allocator, - .writer = .{ - .buffer = try allocator.alloc(u8, capacity), - .vtable = &vtable, - }, - }; - } - - pub fn initOwnedSlice(allocator: Allocator, slice: []u8) Allocating { - return .{ - .allocator = allocator, - .writer = .{ - .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, - .writer = .{ - .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.writer.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.writer; - 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.writer.buffered(); - } - - pub fn shrinkRetainingCapacity(a: *Allocating, new_len: usize) void { - a.writer.end = new_len; - } - - 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("writer", 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; - // Even if we append no data, this function needs to ensure there is more - // capacity in the buffer to avoid infinite loop, hence the +1 in this loop. - assert(data.len != 0); - for (data) |bytes| { - list.ensureUnusedCapacity(gpa, bytes.len + splat_len + 1) 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("writer", 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.minInt64(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.writer.buffer = list.allocatedSlice(); - a.writer.end = list.items.len; - } - - test Allocating { - var a: Allocating = .init(testing.allocator); - defer a.deinit(); - const w = &a.writer; - - 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/bit_reader.zig b/lib/std/io/bit_reader.zig deleted file mode 100644 index 7823e47d43fcc5f8eb51416ad0df9265299a180c..0000000000000000000000000000000000000000 --- a/lib/std/io/bit_reader.zig +++ /dev/null @@ -1,238 +0,0 @@ -const std = @import("../std.zig"); - -//General note on endianess: -//Big endian is packed starting in the most significant part of the byte and subsequent -// bytes contain less significant bits. Thus we always take bits from the high -// end and place them below existing bits in our output. -//Little endian is packed starting in the least significant part of the byte and -// subsequent bytes contain more significant bits. Thus we always take bits from -// the low end and place them above existing bits in our output. -//Regardless of endianess, within any given byte the bits are always in most -// to least significant order. -//Also regardless of endianess, the buffer always aligns bits to the low end -// of the byte. - -/// Creates a bit reader which allows for reading bits from an underlying standard reader -pub fn BitReader(comptime endian: std.builtin.Endian, comptime Reader: type) type { - return struct { - reader: Reader, - bits: u8 = 0, - count: u4 = 0, - - const low_bit_mask = [9]u8{ - 0b00000000, - 0b00000001, - 0b00000011, - 0b00000111, - 0b00001111, - 0b00011111, - 0b00111111, - 0b01111111, - 0b11111111, - }; - - fn Bits(comptime T: type) type { - return struct { - T, - u16, - }; - } - - fn initBits(comptime T: type, out: anytype, num: u16) Bits(T) { - const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); - return .{ - @bitCast(@as(UT, @intCast(out))), - num, - }; - } - - /// Reads `bits` bits from the reader and returns a specified type - /// containing them in the least significant end, returning an error if the - /// specified number of bits could not be read. - pub fn readBitsNoEof(self: *@This(), comptime T: type, num: u16) !T { - const b, const c = try self.readBitsTuple(T, num); - if (c < num) return error.EndOfStream; - return b; - } - - /// Reads `bits` bits from the reader and returns a specified type - /// containing them in the least significant end. The number of bits successfully - /// read is placed in `out_bits`, as reaching the end of the stream is not an error. - pub fn readBits(self: *@This(), comptime T: type, num: u16, out_bits: *u16) !T { - const b, const c = try self.readBitsTuple(T, num); - out_bits.* = c; - return b; - } - - /// Reads `bits` bits from the reader and returns a tuple of the specified type - /// containing them in the least significant end, and the number of bits successfully - /// read. Reaching the end of the stream is not an error. - pub fn readBitsTuple(self: *@This(), comptime T: type, num: u16) !Bits(T) { - const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); - const U = if (@bitSizeOf(T) < 8) u8 else UT; //it is a pain to work with return initBits(T, out, out_count), - else => |e| return e, - }; - - switch (endian) { - .big => { - if (U == u8) out = 0 else out <<= 8; //shifting u8 by 8 is illegal in Zig - out |= byte; - }, - .little => { - const pos = @as(U, byte) << @intCast(out_count); - out |= pos; - }, - } - out_count += 8; - } - - const bits_left = num - out_count; - const keep = 8 - bits_left; - - if (bits_left == 0) return initBits(T, out, out_count); - - const final_byte = self.reader.readByte() catch |err| switch (err) { - error.EndOfStream => return initBits(T, out, out_count), - else => |e| return e, - }; - - switch (endian) { - .big => { - out <<= @intCast(bits_left); - out |= final_byte >> @intCast(keep); - self.bits = final_byte & low_bit_mask[keep]; - }, - .little => { - const pos = @as(U, final_byte & low_bit_mask[bits_left]) << @intCast(out_count); - out |= pos; - self.bits = final_byte >> @intCast(bits_left); - }, - } - - self.count = @intCast(keep); - return initBits(T, out, num); - } - - //convenience function for removing bits from - //the appropriate part of the buffer based on - //endianess. - fn removeBits(self: *@This(), num: u4) u8 { - if (num == 8) { - self.count = 0; - return self.bits; - } - - const keep = self.count - num; - const bits = switch (endian) { - .big => self.bits >> @intCast(keep), - .little => self.bits & low_bit_mask[num], - }; - switch (endian) { - .big => self.bits &= low_bit_mask[keep], - .little => self.bits >>= @intCast(num), - } - - self.count = keep; - return bits; - } - - pub fn alignToByte(self: *@This()) void { - self.bits = 0; - self.count = 0; - } - }; -} - -pub fn bitReader(comptime endian: std.builtin.Endian, reader: anytype) BitReader(endian, @TypeOf(reader)) { - return .{ .reader = reader }; -} - -/////////////////////////////// - -test "api coverage" { - const mem_be = [_]u8{ 0b11001101, 0b00001011 }; - const mem_le = [_]u8{ 0b00011101, 0b10010101 }; - - var mem_in_be = std.io.fixedBufferStream(&mem_be); - var bit_stream_be = bitReader(.big, mem_in_be.reader()); - - var out_bits: u16 = undefined; - - const expect = std.testing.expect; - const expectError = std.testing.expectError; - - try expect(1 == try bit_stream_be.readBits(u2, 1, &out_bits)); - try expect(out_bits == 1); - try expect(2 == try bit_stream_be.readBits(u5, 2, &out_bits)); - try expect(out_bits == 2); - try expect(3 == try bit_stream_be.readBits(u128, 3, &out_bits)); - try expect(out_bits == 3); - try expect(4 == try bit_stream_be.readBits(u8, 4, &out_bits)); - try expect(out_bits == 4); - try expect(5 == try bit_stream_be.readBits(u9, 5, &out_bits)); - try expect(out_bits == 5); - try expect(1 == try bit_stream_be.readBits(u1, 1, &out_bits)); - try expect(out_bits == 1); - - mem_in_be.pos = 0; - bit_stream_be.count = 0; - try expect(0b110011010000101 == try bit_stream_be.readBits(u15, 15, &out_bits)); - try expect(out_bits == 15); - - mem_in_be.pos = 0; - bit_stream_be.count = 0; - try expect(0b1100110100001011 == try bit_stream_be.readBits(u16, 16, &out_bits)); - try expect(out_bits == 16); - - _ = try bit_stream_be.readBits(u0, 0, &out_bits); - - try expect(0 == try bit_stream_be.readBits(u1, 1, &out_bits)); - try expect(out_bits == 0); - try expectError(error.EndOfStream, bit_stream_be.readBitsNoEof(u1, 1)); - - var mem_in_le = std.io.fixedBufferStream(&mem_le); - var bit_stream_le = bitReader(.little, mem_in_le.reader()); - - try expect(1 == try bit_stream_le.readBits(u2, 1, &out_bits)); - try expect(out_bits == 1); - try expect(2 == try bit_stream_le.readBits(u5, 2, &out_bits)); - try expect(out_bits == 2); - try expect(3 == try bit_stream_le.readBits(u128, 3, &out_bits)); - try expect(out_bits == 3); - try expect(4 == try bit_stream_le.readBits(u8, 4, &out_bits)); - try expect(out_bits == 4); - try expect(5 == try bit_stream_le.readBits(u9, 5, &out_bits)); - try expect(out_bits == 5); - try expect(1 == try bit_stream_le.readBits(u1, 1, &out_bits)); - try expect(out_bits == 1); - - mem_in_le.pos = 0; - bit_stream_le.count = 0; - try expect(0b001010100011101 == try bit_stream_le.readBits(u15, 15, &out_bits)); - try expect(out_bits == 15); - - mem_in_le.pos = 0; - bit_stream_le.count = 0; - try expect(0b1001010100011101 == try bit_stream_le.readBits(u16, 16, &out_bits)); - try expect(out_bits == 16); - - _ = try bit_stream_le.readBits(u0, 0, &out_bits); - - try expect(0 == try bit_stream_le.readBits(u1, 1, &out_bits)); - try expect(out_bits == 0); - try expectError(error.EndOfStream, bit_stream_le.readBitsNoEof(u1, 1)); -} diff --git a/lib/std/io/bit_writer.zig b/lib/std/io/bit_writer.zig deleted file mode 100644 index eef0ece81b437e244b9815bc79e4c80ceeb69ed7..0000000000000000000000000000000000000000 --- a/lib/std/io/bit_writer.zig +++ /dev/null @@ -1,179 +0,0 @@ -const std = @import("../std.zig"); - -//General note on endianess: -//Big endian is packed starting in the most significant part of the byte and subsequent -// bytes contain less significant bits. Thus we write out bits from the high end -// of our input first. -//Little endian is packed starting in the least significant part of the byte and -// subsequent bytes contain more significant bits. Thus we write out bits from -// the low end of our input first. -//Regardless of endianess, within any given byte the bits are always in most -// to least significant order. -//Also regardless of endianess, the buffer always aligns bits to the low end -// of the byte. - -/// Creates a bit writer which allows for writing bits to an underlying standard writer -pub fn BitWriter(comptime endian: std.builtin.Endian, comptime Writer: type) type { - return struct { - writer: Writer, - bits: u8 = 0, - count: u4 = 0, - - const low_bit_mask = [9]u8{ - 0b00000000, - 0b00000001, - 0b00000011, - 0b00000111, - 0b00001111, - 0b00011111, - 0b00111111, - 0b01111111, - 0b11111111, - }; - - /// Write the specified number of bits to the writer from the least significant bits of - /// the specified value. Bits will only be written to the writer when there - /// are enough to fill a byte. - pub fn writeBits(self: *@This(), value: anytype, num: u16) !void { - const T = @TypeOf(value); - const UT = std.meta.Int(.unsigned, @bitSizeOf(T)); - const U = if (@bitSizeOf(T) < 8) u8 else UT; // 0) { - //if we can't fill the buffer, add what we have - const bits_free = 8 - self.count; - if (num < bits_free) { - self.addBits(@truncate(in), @intCast(num)); - return; - } - - //finish filling the buffer and flush it - if (num == bits_free) { - self.addBits(@truncate(in), @intCast(num)); - return self.flushBits(); - } - - switch (endian) { - .big => { - const bits = in >> @intCast(in_count - bits_free); - self.addBits(@truncate(bits), bits_free); - }, - .little => { - self.addBits(@truncate(in), bits_free); - in >>= @intCast(bits_free); - }, - } - in_count -= bits_free; - try self.flushBits(); - } - - //write full bytes while we can - const full_bytes_left = in_count / 8; - for (0..full_bytes_left) |_| { - switch (endian) { - .big => { - const bits = in >> @intCast(in_count - 8); - try self.writer.writeByte(@truncate(bits)); - }, - .little => { - try self.writer.writeByte(@truncate(in)); - if (U == u8) in = 0 else in >>= 8; - }, - } - in_count -= 8; - } - - //save the remaining bits in the buffer - self.addBits(@truncate(in), @intCast(in_count)); - } - - //convenience funciton for adding bits to the buffer - //in the appropriate position based on endianess - fn addBits(self: *@This(), bits: u8, num: u4) void { - if (num == 8) self.bits = bits else switch (endian) { - .big => { - self.bits <<= @intCast(num); - self.bits |= bits & low_bit_mask[num]; - }, - .little => { - const pos = bits << @intCast(self.count); - self.bits |= pos; - }, - } - self.count += num; - } - - /// Flush any remaining bits to the writer, filling - /// unused bits with 0s. - pub fn flushBits(self: *@This()) !void { - if (self.count == 0) return; - if (endian == .big) self.bits <<= @intCast(8 - self.count); - try self.writer.writeByte(self.bits); - self.bits = 0; - self.count = 0; - } - }; -} - -pub fn bitWriter(comptime endian: std.builtin.Endian, writer: anytype) BitWriter(endian, @TypeOf(writer)) { - return .{ .writer = writer }; -} - -/////////////////////////////// - -test "api coverage" { - var mem_be = [_]u8{0} ** 2; - var mem_le = [_]u8{0} ** 2; - - var mem_out_be = std.io.fixedBufferStream(&mem_be); - var bit_stream_be = bitWriter(.big, mem_out_be.writer()); - - const testing = std.testing; - - try bit_stream_be.writeBits(@as(u2, 1), 1); - try bit_stream_be.writeBits(@as(u5, 2), 2); - try bit_stream_be.writeBits(@as(u128, 3), 3); - try bit_stream_be.writeBits(@as(u8, 4), 4); - try bit_stream_be.writeBits(@as(u9, 5), 5); - try bit_stream_be.writeBits(@as(u1, 1), 1); - - try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001011); - - mem_out_be.pos = 0; - - try bit_stream_be.writeBits(@as(u15, 0b110011010000101), 15); - try bit_stream_be.flushBits(); - try testing.expect(mem_be[0] == 0b11001101 and mem_be[1] == 0b00001010); - - mem_out_be.pos = 0; - try bit_stream_be.writeBits(@as(u32, 0b110011010000101), 16); - try testing.expect(mem_be[0] == 0b01100110 and mem_be[1] == 0b10000101); - - try bit_stream_be.writeBits(@as(u0, 0), 0); - - var mem_out_le = std.io.fixedBufferStream(&mem_le); - var bit_stream_le = bitWriter(.little, mem_out_le.writer()); - - try bit_stream_le.writeBits(@as(u2, 1), 1); - try bit_stream_le.writeBits(@as(u5, 2), 2); - try bit_stream_le.writeBits(@as(u128, 3), 3); - try bit_stream_le.writeBits(@as(u8, 4), 4); - try bit_stream_le.writeBits(@as(u9, 5), 5); - try bit_stream_le.writeBits(@as(u1, 1), 1); - - try testing.expect(mem_le[0] == 0b00011101 and mem_le[1] == 0b10010101); - - mem_out_le.pos = 0; - try bit_stream_le.writeBits(@as(u15, 0b110011010000101), 15); - try bit_stream_le.flushBits(); - try testing.expect(mem_le[0] == 0b10000101 and mem_le[1] == 0b01100110); - - mem_out_le.pos = 0; - try bit_stream_le.writeBits(@as(u32, 0b1100110100001011), 16); - try testing.expect(mem_le[0] == 0b00001011 and mem_le[1] == 0b11001101); - - try bit_stream_le.writeBits(@as(u0, 0), 0); -} diff --git a/lib/std/io/buffered_atomic_file.zig b/lib/std/io/buffered_atomic_file.zig deleted file mode 100644 index 48510bde52a2b097677316c4f287f56d53d88393..0000000000000000000000000000000000000000 --- a/lib/std/io/buffered_atomic_file.zig +++ /dev/null @@ -1,55 +0,0 @@ -const std = @import("../std.zig"); -const mem = std.mem; -const fs = std.fs; -const File = std.fs.File; - -pub const BufferedAtomicFile = struct { - atomic_file: fs.AtomicFile, - file_writer: File.Writer, - buffered_writer: BufferedWriter, - allocator: mem.Allocator, - - pub const buffer_size = 4096; - pub const BufferedWriter = std.io.BufferedWriter(buffer_size, File.Writer); - 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 - pub fn create( - allocator: mem.Allocator, - dir: fs.Dir, - dest_path: []const u8, - atomic_file_options: fs.Dir.AtomicFileOptions, - ) !*BufferedAtomicFile { - var self = try allocator.create(BufferedAtomicFile); - self.* = BufferedAtomicFile{ - .atomic_file = undefined, - .file_writer = undefined, - .buffered_writer = undefined, - .allocator = allocator, - }; - errdefer allocator.destroy(self); - - self.atomic_file = try dir.atomicFile(dest_path, atomic_file_options); - errdefer self.atomic_file.deinit(); - - self.file_writer = self.atomic_file.file.deprecatedWriter(); - self.buffered_writer = .{ .unbuffered_writer = self.file_writer }; - return self; - } - - /// always call destroy, even after successful finish() - pub fn destroy(self: *BufferedAtomicFile) void { - self.atomic_file.deinit(); - self.allocator.destroy(self); - } - - pub fn finish(self: *BufferedAtomicFile) !void { - try self.buffered_writer.flush(); - try self.atomic_file.finish(); - } - - pub fn writer(self: *BufferedAtomicFile) Writer { - return .{ .context = &self.buffered_writer }; - } -}; diff --git a/lib/std/io/buffered_reader.zig b/lib/std/io/buffered_reader.zig deleted file mode 100644 index 548dd92f736238be712549aadd065dd6d36c8cba..0000000000000000000000000000000000000000 --- a/lib/std/io/buffered_reader.zig +++ /dev/null @@ -1,201 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const mem = std.mem; -const assert = std.debug.assert; -const testing = std.testing; - -pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) type { - return struct { - unbuffered_reader: ReaderType, - buf: [buffer_size]u8 = undefined, - start: usize = 0, - end: usize = 0, - - pub const Error = ReaderType.Error; - pub const Reader = io.GenericReader(*Self, Error, read); - - const Self = @This(); - - pub fn read(self: *Self, dest: []u8) Error!usize { - // First try reading from the already buffered data onto the destination. - const current = self.buf[self.start..self.end]; - if (current.len != 0) { - const to_transfer = @min(current.len, dest.len); - @memcpy(dest[0..to_transfer], current[0..to_transfer]); - self.start += to_transfer; - return to_transfer; - } - - // If dest is large, read from the unbuffered reader directly into the destination. - if (dest.len >= buffer_size) { - return self.unbuffered_reader.read(dest); - } - - // If dest is small, read from the unbuffered reader into our own internal buffer, - // and then transfer to destination. - self.end = try self.unbuffered_reader.read(&self.buf); - const to_transfer = @min(self.end, dest.len); - @memcpy(dest[0..to_transfer], self.buf[0..to_transfer]); - self.start = to_transfer; - return to_transfer; - } - - pub fn reader(self: *Self) Reader { - return .{ .context = self }; - } - }; -} - -pub fn bufferedReader(reader: anytype) BufferedReader(4096, @TypeOf(reader)) { - return .{ .unbuffered_reader = reader }; -} - -pub fn bufferedReaderSize(comptime size: usize, reader: anytype) BufferedReader(size, @TypeOf(reader)) { - return .{ .unbuffered_reader = reader }; -} - -test "OneByte" { - const OneByteReadReader = struct { - str: []const u8, - curr: usize, - - const Error = error{NoError}; - const Self = @This(); - const Reader = io.GenericReader(*Self, Error, read); - - fn init(str: []const u8) Self { - return Self{ - .str = str, - .curr = 0, - }; - } - - fn read(self: *Self, dest: []u8) Error!usize { - if (self.str.len <= self.curr or dest.len == 0) - return 0; - - dest[0] = self.str[self.curr]; - self.curr += 1; - return 1; - } - - fn reader(self: *Self) Reader { - return .{ .context = self }; - } - }; - - const str = "This is a test"; - var one_byte_stream = OneByteReadReader.init(str); - var buf_reader = bufferedReader(one_byte_stream.reader()); - const stream = buf_reader.reader(); - - const res = try stream.readAllAlloc(testing.allocator, str.len + 1); - defer testing.allocator.free(res); - try testing.expectEqualSlices(u8, str, res); -} - -fn smallBufferedReader(underlying_stream: anytype) BufferedReader(8, @TypeOf(underlying_stream)) { - return .{ .unbuffered_reader = underlying_stream }; -} -test "Block" { - const BlockReader = struct { - block: []const u8, - reads_allowed: usize, - curr_read: usize, - - const Error = error{NoError}; - const Self = @This(); - const Reader = io.GenericReader(*Self, Error, read); - - fn init(block: []const u8, reads_allowed: usize) Self { - return Self{ - .block = block, - .reads_allowed = reads_allowed, - .curr_read = 0, - }; - } - - fn read(self: *Self, dest: []u8) Error!usize { - if (self.curr_read >= self.reads_allowed) return 0; - @memcpy(dest[0..self.block.len], self.block); - - self.curr_read += 1; - return self.block.len; - } - - fn reader(self: *Self) Reader { - return .{ .context = self }; - } - }; - - const block = "0123"; - - // len out == block - { - var test_buf_reader: BufferedReader(4, BlockReader) = .{ - .unbuffered_reader = BlockReader.init(block, 2), - }; - const reader = test_buf_reader.reader(); - var out_buf: [4]u8 = undefined; - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, block); - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, block); - try testing.expectEqual(try reader.readAll(&out_buf), 0); - } - - // len out < block - { - var test_buf_reader: BufferedReader(4, BlockReader) = .{ - .unbuffered_reader = BlockReader.init(block, 2), - }; - const reader = test_buf_reader.reader(); - var out_buf: [3]u8 = undefined; - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, "012"); - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, "301"); - const n = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, out_buf[0..n], "23"); - try testing.expectEqual(try reader.readAll(&out_buf), 0); - } - - // len out > block - { - var test_buf_reader: BufferedReader(4, BlockReader) = .{ - .unbuffered_reader = BlockReader.init(block, 2), - }; - const reader = test_buf_reader.reader(); - var out_buf: [5]u8 = undefined; - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, "01230"); - const n = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, out_buf[0..n], "123"); - try testing.expectEqual(try reader.readAll(&out_buf), 0); - } - - // len out == 0 - { - var test_buf_reader: BufferedReader(4, BlockReader) = .{ - .unbuffered_reader = BlockReader.init(block, 2), - }; - const reader = test_buf_reader.reader(); - var out_buf: [0]u8 = undefined; - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, ""); - } - - // len bufreader buf > block - { - var test_buf_reader: BufferedReader(5, BlockReader) = .{ - .unbuffered_reader = BlockReader.init(block, 2), - }; - const reader = test_buf_reader.reader(); - var out_buf: [4]u8 = undefined; - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, block); - _ = try reader.readAll(&out_buf); - try testing.expectEqualSlices(u8, &out_buf, block); - try testing.expectEqual(try reader.readAll(&out_buf), 0); - } -} diff --git a/lib/std/io/buffered_writer.zig b/lib/std/io/buffered_writer.zig deleted file mode 100644 index ef95de0f0ce83d831ace24f433f95c349bc19fdb..0000000000000000000000000000000000000000 --- a/lib/std/io/buffered_writer.zig +++ /dev/null @@ -1,43 +0,0 @@ -const std = @import("../std.zig"); - -const io = std.io; -const mem = std.mem; - -pub fn BufferedWriter(comptime buffer_size: usize, comptime WriterType: type) type { - return struct { - unbuffered_writer: WriterType, - buf: [buffer_size]u8 = undefined, - end: usize = 0, - - pub const Error = WriterType.Error; - pub const Writer = io.GenericWriter(*Self, Error, write); - - const Self = @This(); - - pub fn flush(self: *Self) !void { - try self.unbuffered_writer.writeAll(self.buf[0..self.end]); - self.end = 0; - } - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - pub fn write(self: *Self, bytes: []const u8) Error!usize { - if (self.end + bytes.len > self.buf.len) { - try self.flush(); - if (bytes.len > self.buf.len) - return self.unbuffered_writer.write(bytes); - } - - const new_end = self.end + bytes.len; - @memcpy(self.buf[self.end..new_end], bytes); - self.end = new_end; - return bytes.len; - } - }; -} - -pub fn bufferedWriter(underlying_stream: anytype) BufferedWriter(4096, @TypeOf(underlying_stream)) { - return .{ .unbuffered_writer = underlying_stream }; -} diff --git a/lib/std/io/c_writer.zig b/lib/std/io/c_writer.zig deleted file mode 100644 index 30d0cabcf5145b692eb77ac9ad9c2dcf8b4158c1..0000000000000000000000000000000000000000 --- a/lib/std/io/c_writer.zig +++ /dev/null @@ -1,44 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const io = std.io; -const testing = std.testing; - -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 }; -} - -fn cWriterWrite(c_file: *std.c.FILE, bytes: []const u8) std.fs.File.WriteError!usize { - const amt_written = std.c.fwrite(bytes.ptr, 1, bytes.len, c_file); - if (amt_written >= 0) return amt_written; - switch (@as(std.c.E, @enumFromInt(std.c._errno().*))) { - .SUCCESS => unreachable, - .INVAL => unreachable, - .FAULT => unreachable, - .AGAIN => unreachable, // this is a blocking API - .BADF => unreachable, // always a race condition - .DESTADDRREQ => unreachable, // connect was never called - .DQUOT => return error.DiskQuota, - .FBIG => return error.FileTooBig, - .IO => return error.InputOutput, - .NOSPC => return error.NoSpaceLeft, - .PERM => return error.PermissionDenied, - .PIPE => return error.BrokenPipe, - else => |err| return std.posix.unexpectedErrno(err), - } -} - -test cWriter { - if (!builtin.link_libc or builtin.os.tag == .wasi) return error.SkipZigTest; - - const filename = "tmp_io_test_file.txt"; - const out_file = std.c.fopen(filename, "w") orelse return error.UnableToOpenTestFile; - defer { - _ = std.c.fclose(out_file); - std.fs.cwd().deleteFileZ(filename) catch {}; - } - - const writer = cWriter(out_file); - try writer.print("hi: {}\n", .{@as(i32, 123)}); -} diff --git a/lib/std/io/change_detection_stream.zig b/lib/std/io/change_detection_stream.zig deleted file mode 100644 index d9da1c4a0eb0d0a934a00cf882cccf19b88c35a7..0000000000000000000000000000000000000000 --- a/lib/std/io/change_detection_stream.zig +++ /dev/null @@ -1,55 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const mem = std.mem; -const assert = std.debug.assert; - -/// Used to detect if the data written to a stream differs from a source buffer -pub fn ChangeDetectionStream(comptime WriterType: type) type { - return struct { - const Self = @This(); - pub const Error = WriterType.Error; - pub const Writer = io.GenericWriter(*Self, Error, write); - - anything_changed: bool, - underlying_writer: WriterType, - source_index: usize, - source: []const u8, - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - fn write(self: *Self, bytes: []const u8) Error!usize { - if (!self.anything_changed) { - const end = self.source_index + bytes.len; - if (end > self.source.len) { - self.anything_changed = true; - } else { - const src_slice = self.source[self.source_index..end]; - self.source_index += bytes.len; - if (!mem.eql(u8, bytes, src_slice)) { - self.anything_changed = true; - } - } - } - - return self.underlying_writer.write(bytes); - } - - pub fn changeDetected(self: *Self) bool { - return self.anything_changed or (self.source_index != self.source.len); - } - }; -} - -pub fn changeDetectionStream( - source: []const u8, - underlying_writer: anytype, -) ChangeDetectionStream(@TypeOf(underlying_writer)) { - return ChangeDetectionStream(@TypeOf(underlying_writer)){ - .anything_changed = false, - .underlying_writer = underlying_writer, - .source_index = 0, - .source = source, - }; -} diff --git a/lib/std/io/counting_reader.zig b/lib/std/io/counting_reader.zig deleted file mode 100644 index bc1e1b6ec72433a4c19ba8ae618864831b1648c9..0000000000000000000000000000000000000000 --- a/lib/std/io/counting_reader.zig +++ /dev/null @@ -1,43 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const testing = std.testing; - -/// A Reader that counts how many bytes has been read from it. -pub fn CountingReader(comptime ReaderType: anytype) type { - return struct { - child_reader: ReaderType, - bytes_read: u64 = 0, - - pub const Error = ReaderType.Error; - 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); - self.bytes_read += amt; - return amt; - } - - pub fn reader(self: *@This()) Reader { - return .{ .context = self }; - } - }; -} - -pub fn countingReader(reader: anytype) CountingReader(@TypeOf(reader)) { - return .{ .child_reader = reader }; -} - -test CountingReader { - const bytes = "yay" ** 100; - var fbs = io.fixedBufferStream(bytes); - - var counting_stream = countingReader(fbs.reader()); - const stream = counting_stream.reader(); - - //read and discard all bytes - while (stream.readByte()) |_| {} else |err| { - try testing.expect(err == error.EndOfStream); - } - - try testing.expect(counting_stream.bytes_read == bytes.len); -} diff --git a/lib/std/io/counting_writer.zig b/lib/std/io/counting_writer.zig deleted file mode 100644 index 32c3ed930fcaaa69709f6abc37f3523b325a85aa..0000000000000000000000000000000000000000 --- a/lib/std/io/counting_writer.zig +++ /dev/null @@ -1,39 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const testing = std.testing; - -/// A Writer that counts how many bytes has been written to it. -pub fn CountingWriter(comptime WriterType: type) type { - return struct { - bytes_written: u64, - child_stream: WriterType, - - pub const Error = WriterType.Error; - pub const Writer = io.GenericWriter(*Self, Error, write); - - const Self = @This(); - - pub fn write(self: *Self, bytes: []const u8) Error!usize { - const amt = try self.child_stream.write(bytes); - self.bytes_written += amt; - return amt; - } - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - }; -} - -pub fn countingWriter(child_stream: anytype) CountingWriter(@TypeOf(child_stream)) { - return .{ .bytes_written = 0, .child_stream = child_stream }; -} - -test CountingWriter { - var counting_stream = countingWriter(std.io.null_writer); - const stream = counting_stream.writer(); - - const bytes = "yay" ** 100; - stream.writeAll(bytes) catch unreachable; - try testing.expect(counting_stream.bytes_written == bytes.len); -} diff --git a/lib/std/io/find_byte_writer.zig b/lib/std/io/find_byte_writer.zig deleted file mode 100644 index fe6836f6037bb1fcb8381c0cfbb0cebc15573a93..0000000000000000000000000000000000000000 --- a/lib/std/io/find_byte_writer.zig +++ /dev/null @@ -1,40 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const assert = std.debug.assert; - -/// A Writer that returns whether the given character has been written to it. -/// The contents are not written to anything. -pub fn FindByteWriter(comptime UnderlyingWriter: type) type { - return struct { - const Self = @This(); - pub const Error = UnderlyingWriter.Error; - pub const Writer = io.GenericWriter(*Self, Error, write); - - underlying_writer: UnderlyingWriter, - byte_found: bool, - byte: u8, - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - fn write(self: *Self, bytes: []const u8) Error!usize { - if (!self.byte_found) { - self.byte_found = blk: { - for (bytes) |b| - if (b == self.byte) break :blk true; - break :blk false; - }; - } - return self.underlying_writer.write(bytes); - } - }; -} - -pub fn findByteWriter(byte: u8, underlying_writer: anytype) FindByteWriter(@TypeOf(underlying_writer)) { - return FindByteWriter(@TypeOf(underlying_writer)){ - .underlying_writer = underlying_writer, - .byte = byte, - .byte_found = false, - }; -} diff --git a/lib/std/io/fixed_buffer_stream.zig b/lib/std/io/fixed_buffer_stream.zig deleted file mode 100644 index 67d6f3d286381db34eb92c80358523e184a500c7..0000000000000000000000000000000000000000 --- a/lib/std/io/fixed_buffer_stream.zig +++ /dev/null @@ -1,198 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const testing = std.testing; -const mem = std.mem; -const assert = std.debug.assert; - -/// 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`. - buffer: Buffer, - pos: usize, - - pub const ReadError = error{}; - pub const WriteError = error{NoSpaceLeft}; - pub const SeekError = error{}; - pub const GetSeekPosError = error{}; - - pub const Reader = io.GenericReader(*Self, ReadError, read); - pub const Writer = io.GenericWriter(*Self, WriteError, write); - - pub const SeekableStream = io.SeekableStream( - *Self, - SeekError, - GetSeekPosError, - seekTo, - seekBy, - getPos, - getEndPos, - ); - - const Self = @This(); - - pub fn reader(self: *Self) Reader { - return .{ .context = self }; - } - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - pub fn seekableStream(self: *Self) SeekableStream { - return .{ .context = self }; - } - - pub fn read(self: *Self, dest: []u8) ReadError!usize { - const size = @min(dest.len, self.buffer.len - self.pos); - const end = self.pos + size; - - @memcpy(dest[0..size], self.buffer[self.pos..end]); - self.pos = end; - - return size; - } - - /// If the returned number of bytes written is less than requested, the - /// buffer is full. Returns `error.NoSpaceLeft` when no bytes would be written. - /// Note: `error.NoSpaceLeft` matches the corresponding error from - /// `std.fs.File.WriteError`. - pub fn write(self: *Self, bytes: []const u8) WriteError!usize { - if (bytes.len == 0) return 0; - if (self.pos >= self.buffer.len) return error.NoSpaceLeft; - - const n = @min(self.buffer.len - self.pos, bytes.len); - @memcpy(self.buffer[self.pos..][0..n], bytes[0..n]); - self.pos += n; - - if (n == 0) return error.NoSpaceLeft; - - return n; - } - - pub fn seekTo(self: *Self, pos: u64) SeekError!void { - self.pos = @min(std.math.lossyCast(usize, pos), self.buffer.len); - } - - pub fn seekBy(self: *Self, amt: i64) SeekError!void { - if (amt < 0) { - const abs_amt = @abs(amt); - const abs_amt_usize = std.math.cast(usize, abs_amt) orelse std.math.maxInt(usize); - if (abs_amt_usize > self.pos) { - self.pos = 0; - } else { - self.pos -= abs_amt_usize; - } - } else { - const amt_usize = std.math.cast(usize, amt) orelse std.math.maxInt(usize); - const new_pos = std.math.add(usize, self.pos, amt_usize) catch std.math.maxInt(usize); - self.pos = @min(self.buffer.len, new_pos); - } - } - - pub fn getEndPos(self: *Self) GetSeekPosError!u64 { - return self.buffer.len; - } - - pub fn getPos(self: *Self) GetSeekPosError!u64 { - return self.pos; - } - - pub fn getWritten(self: Self) Buffer { - return self.buffer[0..self.pos]; - } - - pub fn reset(self: *Self) void { - self.pos = 0; - } - }; -} - -pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) { - return .{ .buffer = buffer, .pos = 0 }; -} - -fn Slice(comptime T: type) type { - switch (@typeInfo(T)) { - .pointer => |ptr_info| { - var new_ptr_info = ptr_info; - switch (ptr_info.size) { - .slice => {}, - .one => switch (@typeInfo(ptr_info.child)) { - .array => |info| new_ptr_info.child = info.child, - else => @compileError("invalid type given to fixedBufferStream"), - }, - else => @compileError("invalid type given to fixedBufferStream"), - } - new_ptr_info.size = .slice; - return @Type(.{ .pointer = new_ptr_info }); - }, - else => @compileError("invalid type given to fixedBufferStream"), - } -} - -test "output" { - var buf: [255]u8 = undefined; - var fbs = fixedBufferStream(&buf); - const stream = fbs.writer(); - - try stream.print("{s}{s}!", .{ "Hello", "World" }); - try testing.expectEqualSlices(u8, "HelloWorld!", fbs.getWritten()); -} - -test "output at comptime" { - comptime { - var buf: [255]u8 = undefined; - var fbs = fixedBufferStream(&buf); - const stream = fbs.writer(); - - try stream.print("{s}{s}!", .{ "Hello", "World" }); - try testing.expectEqualSlices(u8, "HelloWorld!", fbs.getWritten()); - } -} - -test "output 2" { - var buffer: [10]u8 = undefined; - var fbs = fixedBufferStream(&buffer); - - try fbs.writer().writeAll("Hello"); - try testing.expect(mem.eql(u8, fbs.getWritten(), "Hello")); - - try fbs.writer().writeAll("world"); - try testing.expect(mem.eql(u8, fbs.getWritten(), "Helloworld")); - - try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("!")); - try testing.expect(mem.eql(u8, fbs.getWritten(), "Helloworld")); - - fbs.reset(); - try testing.expect(fbs.getWritten().len == 0); - - try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("Hello world!")); - try testing.expect(mem.eql(u8, fbs.getWritten(), "Hello worl")); - - try fbs.seekTo((try fbs.getEndPos()) + 1); - try testing.expectError(error.NoSpaceLeft, fbs.writer().writeAll("H")); -} - -test "input" { - const bytes = [_]u8{ 1, 2, 3, 4, 5, 6, 7 }; - var fbs = fixedBufferStream(&bytes); - - var dest: [4]u8 = undefined; - - var read = try fbs.reader().read(&dest); - try testing.expect(read == 4); - try testing.expect(mem.eql(u8, dest[0..4], bytes[0..4])); - - read = try fbs.reader().read(&dest); - try testing.expect(read == 3); - try testing.expect(mem.eql(u8, dest[0..3], bytes[4..7])); - - read = try fbs.reader().read(&dest); - try testing.expect(read == 0); - - try fbs.seekTo((try fbs.getEndPos()) + 1); - read = try fbs.reader().read(&dest); - try testing.expect(read == 0); -} diff --git a/lib/std/io/limited_reader.zig b/lib/std/io/limited_reader.zig deleted file mode 100644 index b6b555f76deca749d492879f00af9f80b579b372..0000000000000000000000000000000000000000 --- a/lib/std/io/limited_reader.zig +++ /dev/null @@ -1,45 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; -const assert = std.debug.assert; -const testing = std.testing; - -pub fn LimitedReader(comptime ReaderType: type) type { - return struct { - inner_reader: ReaderType, - bytes_left: u64, - - pub const Error = ReaderType.Error; - pub const Reader = io.GenericReader(*Self, Error, read); - - const Self = @This(); - - pub fn read(self: *Self, dest: []u8) Error!usize { - const max_read = @min(self.bytes_left, dest.len); - const n = try self.inner_reader.read(dest[0..max_read]); - self.bytes_left -= n; - return n; - } - - pub fn reader(self: *Self) Reader { - return .{ .context = self }; - } - }; -} - -/// Returns an initialised `LimitedReader`. -/// `bytes_left` is a `u64` to be able to take 64 bit file offsets -pub fn limitedReader(inner_reader: anytype, bytes_left: u64) LimitedReader(@TypeOf(inner_reader)) { - return .{ .inner_reader = inner_reader, .bytes_left = bytes_left }; -} - -test "basic usage" { - const data = "hello world"; - var fbs = std.io.fixedBufferStream(data); - var early_stream = limitedReader(fbs.reader(), 3); - - var buf: [5]u8 = undefined; - try testing.expectEqual(@as(usize, 3), try early_stream.reader().read(&buf)); - try testing.expectEqualSlices(u8, data[0..3], buf[0..3]); - try testing.expectEqual(@as(usize, 0), try early_stream.reader().read(&buf)); - try testing.expectError(error.EndOfStream, early_stream.reader().skipBytes(10, .{})); -} diff --git a/lib/std/io/multi_writer.zig b/lib/std/io/multi_writer.zig deleted file mode 100644 index 20e9e782de9d02999fe889c659985d70fe1d1508..0000000000000000000000000000000000000000 --- a/lib/std/io/multi_writer.zig +++ /dev/null @@ -1,53 +0,0 @@ -const std = @import("../std.zig"); -const io = std.io; - -/// Takes a tuple of streams, and constructs a new stream that writes to all of them -pub fn MultiWriter(comptime Writers: type) type { - comptime var ErrSet = error{}; - inline for (@typeInfo(Writers).@"struct".fields) |field| { - const StreamType = field.type; - ErrSet = ErrSet || StreamType.Error; - } - - return struct { - const Self = @This(); - - streams: Writers, - - pub const Error = ErrSet; - pub const Writer = io.GenericWriter(*Self, Error, write); - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - pub fn write(self: *Self, bytes: []const u8) Error!usize { - inline for (self.streams) |stream| - try stream.writeAll(bytes); - return bytes.len; - } - }; -} - -pub fn multiWriter(streams: anytype) MultiWriter(@TypeOf(streams)) { - return .{ .streams = streams }; -} - -const testing = std.testing; - -test "MultiWriter" { - var tmp = testing.tmpDir(.{}); - defer tmp.cleanup(); - var f = try tmp.dir.createFile("t.txt", .{}); - - var buf1: [255]u8 = undefined; - var fbs1 = io.fixedBufferStream(&buf1); - var buf2: [255]u8 = undefined; - var stream = multiWriter(.{ fbs1.writer(), f.writer() }); - - try stream.writer().print("HI", .{}); - f.close(); - - try testing.expectEqualSlices(u8, "HI", fbs1.getWritten()); - try testing.expectEqualSlices(u8, "HI", try tmp.dir.readFile("t.txt", &buf2)); -} diff --git a/lib/std/io/seekable_stream.zig b/lib/std/io/seekable_stream.zig deleted file mode 100644 index 1aa653dbe52cc6297c94b0422c35c74b46515b6c..0000000000000000000000000000000000000000 --- a/lib/std/io/seekable_stream.zig +++ /dev/null @@ -1,35 +0,0 @@ -const std = @import("../std.zig"); - -pub fn SeekableStream( - comptime Context: type, - comptime SeekErrorType: type, - comptime GetSeekPosErrorType: type, - comptime seekToFn: fn (context: Context, pos: u64) SeekErrorType!void, - comptime seekByFn: fn (context: Context, pos: i64) SeekErrorType!void, - comptime getPosFn: fn (context: Context) GetSeekPosErrorType!u64, - comptime getEndPosFn: fn (context: Context) GetSeekPosErrorType!u64, -) type { - return struct { - context: Context, - - const Self = @This(); - pub const SeekError = SeekErrorType; - pub const GetSeekPosError = GetSeekPosErrorType; - - pub fn seekTo(self: Self, pos: u64) SeekError!void { - return seekToFn(self.context, pos); - } - - pub fn seekBy(self: Self, amt: i64) SeekError!void { - return seekByFn(self.context, amt); - } - - pub fn getEndPos(self: Self) GetSeekPosError!u64 { - return getEndPosFn(self.context); - } - - pub fn getPos(self: Self) GetSeekPosError!u64 { - return getPosFn(self.context); - } - }; -} diff --git a/lib/std/io/stream_source.zig b/lib/std/io/stream_source.zig deleted file mode 100644 index 2a3527e47934873dc22cc5997286801c4601ffe7..0000000000000000000000000000000000000000 --- a/lib/std/io/stream_source.zig +++ /dev/null @@ -1,127 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const io = std.io; - -/// 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.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 - const has_file = (builtin.os.tag != .freestanding and builtin.os.tag != .uefi); - - /// The stream access is redirected to this buffer. - buffer: io.FixedBufferStream([]u8), - - /// The stream access is redirected to this buffer. - /// Writing to the source will always yield `error.AccessDenied`. - const_buffer: io.FixedBufferStream([]const u8), - - /// The stream access is redirected to this file. - /// On freestanding, this must never be initialized! - file: if (has_file) std.fs.File else void, - - pub const ReadError = io.FixedBufferStream([]u8).ReadError || (if (has_file) std.fs.File.ReadError else error{}); - pub const WriteError = error{AccessDenied} || io.FixedBufferStream([]u8).WriteError || (if (has_file) std.fs.File.WriteError else error{}); - 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.GenericReader(*StreamSource, ReadError, read); - pub const Writer = io.GenericWriter(*StreamSource, WriteError, write); - pub const SeekableStream = io.SeekableStream( - *StreamSource, - SeekError, - GetSeekPosError, - seekTo, - seekBy, - getPos, - getEndPos, - ); - - pub fn read(self: *StreamSource, dest: []u8) ReadError!usize { - switch (self.*) { - .buffer => |*x| return x.read(dest), - .const_buffer => |*x| return x.read(dest), - .file => |x| if (!has_file) unreachable else return x.read(dest), - } - } - - pub fn write(self: *StreamSource, bytes: []const u8) WriteError!usize { - switch (self.*) { - .buffer => |*x| return x.write(bytes), - .const_buffer => return error.AccessDenied, - .file => |x| if (!has_file) unreachable else return x.write(bytes), - } - } - - pub fn seekTo(self: *StreamSource, pos: u64) SeekError!void { - switch (self.*) { - .buffer => |*x| return x.seekTo(pos), - .const_buffer => |*x| return x.seekTo(pos), - .file => |x| if (!has_file) unreachable else return x.seekTo(pos), - } - } - - pub fn seekBy(self: *StreamSource, amt: i64) SeekError!void { - switch (self.*) { - .buffer => |*x| return x.seekBy(amt), - .const_buffer => |*x| return x.seekBy(amt), - .file => |x| if (!has_file) unreachable else return x.seekBy(amt), - } - } - - pub fn getEndPos(self: *StreamSource) GetSeekPosError!u64 { - switch (self.*) { - .buffer => |*x| return x.getEndPos(), - .const_buffer => |*x| return x.getEndPos(), - .file => |x| if (!has_file) unreachable else return x.getEndPos(), - } - } - - pub fn getPos(self: *StreamSource) GetSeekPosError!u64 { - switch (self.*) { - .buffer => |*x| return x.getPos(), - .const_buffer => |*x| return x.getPos(), - .file => |x| if (!has_file) unreachable else return x.getPos(), - } - } - - pub fn reader(self: *StreamSource) Reader { - return .{ .context = self }; - } - - pub fn writer(self: *StreamSource) Writer { - return .{ .context = self }; - } - - pub fn seekableStream(self: *StreamSource) SeekableStream { - return .{ .context = self }; - } -}; - -test "refs" { - std.testing.refAllDecls(StreamSource); -} - -test "mutable buffer" { - var buffer: [64]u8 = undefined; - var source = StreamSource{ .buffer = std.io.fixedBufferStream(&buffer) }; - - var writer = source.writer(); - - try writer.writeAll("Hello, World!"); - - try std.testing.expectEqualStrings("Hello, World!", source.buffer.getWritten()); -} - -test "const buffer" { - const buffer: [64]u8 = "Hello, World!".* ++ ([1]u8{0xAA} ** 51); - var source = StreamSource{ .const_buffer = std.io.fixedBufferStream(&buffer) }; - - var reader = source.reader(); - - var dst_buffer: [13]u8 = undefined; - try reader.readNoEof(&dst_buffer); - - try std.testing.expectEqualStrings("Hello, World!", &dst_buffer); -} diff --git a/lib/std/io/test.zig b/lib/std/io/test.zig deleted file mode 100644 index bf14f0c24ca15447283e531db5f125680a9767d0..0000000000000000000000000000000000000000 --- a/lib/std/io/test.zig +++ /dev/null @@ -1,182 +0,0 @@ -const std = @import("std"); -const io = std.io; -const DefaultPrng = std.Random.DefaultPrng; -const expect = std.testing.expect; -const expectEqual = std.testing.expectEqual; -const expectError = std.testing.expectError; -const mem = std.mem; -const fs = std.fs; -const File = std.fs.File; -const native_endian = @import("builtin").target.cpu.arch.endian(); - -const tmpDir = std.testing.tmpDir; - -test "write a file, read it, then delete it" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - var data: [1024]u8 = undefined; - var prng = DefaultPrng.init(std.testing.random_seed); - const random = prng.random(); - random.bytes(data[0..]); - const tmp_file_name = "temp_test_file.txt"; - { - var file = try tmp.dir.createFile(tmp_file_name, .{}); - defer file.close(); - - var buf_stream = io.bufferedWriter(file.deprecatedWriter()); - const st = buf_stream.writer(); - try st.print("begin", .{}); - try st.writeAll(data[0..]); - try st.print("end", .{}); - try buf_stream.flush(); - } - - { - // Make sure the exclusive flag is honored. - try expectError(File.OpenError.PathAlreadyExists, tmp.dir.createFile(tmp_file_name, .{ .exclusive = true })); - } - - { - var file = try tmp.dir.openFile(tmp_file_name, .{}); - defer file.close(); - - const file_size = try file.getEndPos(); - const expected_file_size: u64 = "begin".len + data.len + "end".len; - try expectEqual(expected_file_size, file_size); - - var buf_stream = io.bufferedReader(file.deprecatedReader()); - const st = buf_stream.reader(); - const contents = try st.readAllAlloc(std.testing.allocator, 2 * 1024); - defer std.testing.allocator.free(contents); - - try expect(mem.eql(u8, contents[0.."begin".len], "begin")); - try expect(mem.eql(u8, contents["begin".len .. contents.len - "end".len], &data)); - try expect(mem.eql(u8, contents[contents.len - "end".len ..], "end")); - } - try tmp.dir.deleteFile(tmp_file_name); -} - -test "BitStreams with File Stream" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - const tmp_file_name = "temp_test_file.txt"; - { - var file = try tmp.dir.createFile(tmp_file_name, .{}); - defer file.close(); - - var bit_stream = io.bitWriter(native_endian, file.deprecatedWriter()); - - try bit_stream.writeBits(@as(u2, 1), 1); - try bit_stream.writeBits(@as(u5, 2), 2); - try bit_stream.writeBits(@as(u128, 3), 3); - try bit_stream.writeBits(@as(u8, 4), 4); - try bit_stream.writeBits(@as(u9, 5), 5); - try bit_stream.writeBits(@as(u1, 1), 1); - try bit_stream.flushBits(); - } - { - var file = try tmp.dir.openFile(tmp_file_name, .{}); - defer file.close(); - - var bit_stream = io.bitReader(native_endian, file.deprecatedReader()); - - var out_bits: u16 = undefined; - - try expect(1 == try bit_stream.readBits(u2, 1, &out_bits)); - try expect(out_bits == 1); - try expect(2 == try bit_stream.readBits(u5, 2, &out_bits)); - try expect(out_bits == 2); - try expect(3 == try bit_stream.readBits(u128, 3, &out_bits)); - try expect(out_bits == 3); - try expect(4 == try bit_stream.readBits(u8, 4, &out_bits)); - try expect(out_bits == 4); - try expect(5 == try bit_stream.readBits(u9, 5, &out_bits)); - try expect(out_bits == 5); - try expect(1 == try bit_stream.readBits(u1, 1, &out_bits)); - try expect(out_bits == 1); - - try expectError(error.EndOfStream, bit_stream.readBitsNoEof(u1, 1)); - } - try tmp.dir.deleteFile(tmp_file_name); -} - -test "File seek ops" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - const tmp_file_name = "temp_test_file.txt"; - var file = try tmp.dir.createFile(tmp_file_name, .{}); - defer file.close(); - - try file.writeAll(&([_]u8{0x55} ** 8192)); - - // Seek to the end - try file.seekFromEnd(0); - try expect((try file.getPos()) == try file.getEndPos()); - // Negative delta - try file.seekBy(-4096); - try expect((try file.getPos()) == 4096); - // Positive delta - try file.seekBy(10); - try expect((try file.getPos()) == 4106); - // Absolute position - try file.seekTo(1234); - try expect((try file.getPos()) == 1234); -} - -test "setEndPos" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - const tmp_file_name = "temp_test_file.txt"; - var file = try tmp.dir.createFile(tmp_file_name, .{}); - defer file.close(); - - // Verify that the file size changes and the file offset is not moved - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 0); - try file.setEndPos(8192); - try std.testing.expect((try file.getEndPos()) == 8192); - try std.testing.expect((try file.getPos()) == 0); - try file.seekTo(100); - try file.setEndPos(4096); - try std.testing.expect((try file.getEndPos()) == 4096); - try std.testing.expect((try file.getPos()) == 100); - try file.setEndPos(0); - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 100); -} - -test "updateTimes" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - const tmp_file_name = "just_a_temporary_file.txt"; - var file = try tmp.dir.createFile(tmp_file_name, .{ .read = true }); - defer file.close(); - - const stat_old = try file.stat(); - // Set atime and mtime to 5s before - try file.updateTimes( - stat_old.atime - 5 * std.time.ns_per_s, - stat_old.mtime - 5 * std.time.ns_per_s, - ); - const stat_new = try file.stat(); - try expect(stat_new.atime < stat_old.atime); - try expect(stat_new.mtime < stat_old.mtime); -} - -test "GenericReader methods can return error.EndOfStream" { - // https://github.com/ziglang/zig/issues/17733 - var fbs = std.io.fixedBufferStream(""); - try std.testing.expectError( - error.EndOfStream, - fbs.reader().readEnum(enum(u8) { a, b }, .little), - ); - try std.testing.expectError( - error.EndOfStream, - fbs.reader().isBytes("foo"), - ); -} diff --git a/lib/std/io/tty.zig b/lib/std/io/tty.zig deleted file mode 100644 index fa17d9a16def9331c778c9de74abc8a41721639a..0000000000000000000000000000000000000000 --- a/lib/std/io/tty.zig +++ /dev/null @@ -1,138 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const File = std.fs.File; -const process = std.process; -const windows = std.os.windows; -const native_os = builtin.os.tag; - -/// Deprecated in favor of `Config.detect`. -pub fn detectConfig(file: File) Config { - return .detect(file); -} - -pub const Color = enum { - black, - red, - green, - yellow, - blue, - magenta, - cyan, - white, - bright_black, - bright_red, - bright_green, - bright_yellow, - bright_blue, - bright_magenta, - bright_cyan, - bright_white, - dim, - bold, - reset, -}; - -/// Provides simple functionality for manipulating the terminal in some way, -/// such as coloring text, etc. -pub const Config = union(enum) { - no_color, - escape_codes, - windows_api: if (native_os == .windows) WindowsContext else void, - - /// Detect suitable TTY configuration options for the given file (commonly stdout/stderr). - /// This includes feature checks for ANSI escape codes and the Windows console API, as well as - /// respecting the `NO_COLOR` and `CLICOLOR_FORCE` environment variables to override the default. - /// Will attempt to enable ANSI escape code support if necessary/possible. - pub fn detect(file: File) Config { - const force_color: ?bool = if (builtin.os.tag == .wasi) - null // wasi does not support environment variables - else if (process.hasNonEmptyEnvVarConstant("NO_COLOR")) - false - else if (process.hasNonEmptyEnvVarConstant("CLICOLOR_FORCE")) - true - else - null; - - if (force_color == false) return .no_color; - - if (file.getOrEnableAnsiEscapeSupport()) return .escape_codes; - - if (native_os == .windows and file.isTty()) { - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) == windows.FALSE) { - return if (force_color == true) .escape_codes else .no_color; - } - return .{ .windows_api = .{ - .handle = file.handle, - .reset_attributes = info.wAttributes, - } }; - } - - return if (force_color == true) .escape_codes else .no_color; - } - - pub const WindowsContext = struct { - handle: File.Handle, - reset_attributes: u16, - }; - - pub const SetColorError = std.os.windows.SetConsoleTextAttributeError || std.io.Writer.Error; - - pub fn setColor(conf: Config, w: *std.io.Writer, color: Color) SetColorError!void { - nosuspend switch (conf) { - .no_color => return, - .escape_codes => { - const color_string = switch (color) { - .black => "\x1b[30m", - .red => "\x1b[31m", - .green => "\x1b[32m", - .yellow => "\x1b[33m", - .blue => "\x1b[34m", - .magenta => "\x1b[35m", - .cyan => "\x1b[36m", - .white => "\x1b[37m", - .bright_black => "\x1b[90m", - .bright_red => "\x1b[91m", - .bright_green => "\x1b[92m", - .bright_yellow => "\x1b[93m", - .bright_blue => "\x1b[94m", - .bright_magenta => "\x1b[95m", - .bright_cyan => "\x1b[96m", - .bright_white => "\x1b[97m", - .bold => "\x1b[1m", - .dim => "\x1b[2m", - .reset => "\x1b[0m", - }; - try w.writeAll(color_string); - }, - .windows_api => |ctx| if (native_os == .windows) { - const attributes = switch (color) { - .black => 0, - .red => windows.FOREGROUND_RED, - .green => windows.FOREGROUND_GREEN, - .yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN, - .blue => windows.FOREGROUND_BLUE, - .magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE, - .cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .white => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE, - .bright_black => windows.FOREGROUND_INTENSITY, - .bright_red => windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY, - .bright_green => windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_yellow => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY, - .bright_blue => windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_magenta => windows.FOREGROUND_RED | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_cyan => windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - .bright_white, .bold => windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY, - // "dim" is not supported using basic character attributes, but let's still make it do *something*. - // This matches the old behavior of TTY.Color before the bright variants were added. - .dim => windows.FOREGROUND_INTENSITY, - .reset => ctx.reset_attributes, - }; - try w.flush(); - try windows.SetConsoleTextAttribute(ctx.handle, attributes); - } else { - unreachable; - }, - }; - } -}; diff --git a/lib/std/std.zig b/lib/std/std.zig index 5f13b931d1d1f18790c9dbb4501ca59f2b0433ec..564b04c609f86ee3e37765dc3be1a35719a6c171 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -25,6 +25,7 @@ pub const EnumMap = enums.EnumMap; pub const EnumSet = enums.EnumSet; pub const HashMap = hash_map.HashMap; pub const HashMapUnmanaged = hash_map.HashMapUnmanaged; +pub const Io = @import("Io.zig"); pub const MultiArrayList = @import("multi_array_list.zig").MultiArrayList; pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue; pub const PriorityDequeue = @import("priority_dequeue.zig").PriorityDequeue; @@ -67,7 +68,8 @@ pub const hash = @import("hash.zig"); pub const hash_map = @import("hash_map.zig"); pub const heap = @import("heap.zig"); pub const http = @import("http.zig"); -pub const io = @import("io.zig"); +/// Deprecated +pub const io = Io; pub const json = @import("json.zig"); pub const leb = @import("leb128.zig"); pub const log = @import("log.zig");