zig/lib/std/io/BufferedWriter.zig

1690 lines
66 KiB
Zig

const std = @import("../std.zig");
const BufferedWriter = @This();
const assert = std.debug.assert;
const native_endian = @import("builtin").target.cpu.arch.endian();
const Writer = std.io.Writer;
const Allocator = std.mem.Allocator;
const testing = std.testing;
/// Underlying stream to send bytes to.
///
/// A write will only be sent here if it could not fit into `buffer`, or if it
/// is a `writeFile`.
///
/// `unbuffered_writer` may modify `buffer` if the number of bytes returned
/// equals number of bytes provided. This property is exploited by
/// `std.io.AllocatingWriter` for example.
unbuffered_writer: Writer,
/// If this has length zero, the writer is unbuffered, and `flush` is a no-op.
buffer: []u8,
/// Marks the end of `buffer` - before this are buffered bytes, after this is
/// undefined.
end: usize = 0,
/// Tracks total number of bytes written to this `BufferedWriter`. This value
/// only increases. In the case of fixed mode, this value always equals `end`.
count: usize = 0,
/// Number of slices to store on the stack, when trying to send as many byte
/// vectors through the underlying write calls as possible.
pub const max_buffers_len = 8;
/// Although `BufferedWriter` can easily satisfy the `Writer` interface, it's
/// generally more practical to pass a `BufferedWriter` instance itself around,
/// since it will result in fewer calls across vtable boundaries.
pub fn writer(bw: *BufferedWriter) Writer {
return .{
.context = bw,
.vtable = &.{
.writeSplat = passthru_writeSplat,
.writeFile = passthru_writeFile,
},
};
}
const fixed_vtable: Writer.VTable = .{
.writeSplat = fixed_writeSplat,
.writeFile = Writer.unimplemented_writeFile,
};
/// Replaces the `BufferedWriter` with one that writes to `buffer` and returns
/// `error.NoSpaceLeft` when it is full. `end` and `count` will always be
/// equal.
pub fn initFixed(bw: *BufferedWriter, buffer: []u8) void {
bw.* = .{
.unbuffered_writer = .{
.context = bw,
.vtable = &fixed_vtable,
},
.buffer = buffer,
};
}
/// This function is available when using `initFixed`.
pub fn getWritten(bw: *const BufferedWriter) []u8 {
assert(bw.unbuffered_writer.vtable == &fixed_vtable);
return bw.buffer[0..bw.end];
}
/// This function is available when using `initFixed`.
pub fn reset(bw: *BufferedWriter) void {
assert(bw.unbuffered_writer.vtable == &fixed_vtable);
bw.end = 0;
bw.count = 0;
}
pub fn flush(bw: *BufferedWriter) anyerror!void {
const send_buffer = bw.buffer[0..bw.end];
var index: usize = 0;
while (index < send_buffer.len) index += try bw.unbuffered_writer.writev(&.{send_buffer[index..]});
bw.end = 0;
}
pub fn unusedCapacitySlice(bw: *const BufferedWriter) []u8 {
return bw.buffer[bw.end..];
}
/// Asserts the provided buffer has total capacity enough for `minimum_length`.
pub fn writableSlice(bw: *BufferedWriter, minimum_length: usize) anyerror![]u8 {
assert(bw.buffer.len >= minimum_length);
const cap_slice = bw.buffer[bw.end..];
if (cap_slice.len >= minimum_length) {
@branchHint(.likely);
return cap_slice;
}
const buffer = bw.buffer[0..bw.end];
const n = try bw.unbuffered_writer.write(buffer);
if (n == buffer.len) {
@branchHint(.likely);
bw.end = 0;
return bw.buffer;
}
if (n > 0) {
const remainder = buffer[n..];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
}
return bw.buffer[bw.end..];
}
/// After calling `writableSlice`, this function tracks how many bytes were written to it.
pub fn advance(bw: *BufferedWriter, n: usize) void {
const new_end = bw.end + n;
assert(new_end <= bw.buffer.len);
bw.end = new_end;
bw.count += n;
}
/// The `data` parameter is mutable because this function needs to mutate the
/// fields in order to handle partial writes from `Writer.VTable.writev`.
pub fn writevAll(bw: *BufferedWriter, data: [][]const u8) anyerror!void {
var i: usize = 0;
while (true) {
var n = try passthru_writeSplat(bw, data[i..], 1);
const len = data[i].len;
while (n >= len) {
n -= len;
i += 1;
if (i >= data.len) return;
}
data[i] = data[i][n..];
}
}
pub fn writeSplat(bw: *BufferedWriter, data: []const []const u8, splat: usize) anyerror!usize {
return passthru_writeSplat(bw, data, splat);
}
pub fn writev(bw: *BufferedWriter, data: []const []const u8) anyerror!usize {
return passthru_writeSplat(bw, data, 1);
}
fn passthru_writeSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) anyerror!usize {
const bw: *BufferedWriter = @alignCast(@ptrCast(context));
const buffer = bw.buffer;
const start_end = bw.end;
var buffers: [max_buffers_len][]const u8 = undefined;
var end = start_end;
for (data, 0..) |bytes, i| {
const new_end = end + bytes.len;
if (new_end <= buffer.len) {
@branchHint(.likely);
@memcpy(buffer[end..new_end], bytes);
end = new_end;
continue;
}
if (end == 0) return track(&bw.count, try bw.unbuffered_writer.writeSplat(data, splat));
buffers[0] = buffer[0..end];
const remaining_data = data[i..];
const remaining_buffers = buffers[1..];
const len: usize = @min(remaining_data.len, remaining_buffers.len);
@memcpy(remaining_buffers[0..len], remaining_data[0..len]);
const send_buffers = buffers[0 .. len + 1];
if (len >= remaining_data.len) {
@branchHint(.likely);
// Made it past the headers, so we can enable splatting.
const n = try bw.unbuffered_writer.writeSplat(send_buffers, splat);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
}
const n = try bw.unbuffered_writer.writeSplat(send_buffers, 1);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
}
const pattern = data[data.len - 1];
if (splat == 0) {
@branchHint(.unlikely);
// It was added in the loop above; undo it here.
end -= pattern.len;
bw.end = end;
return track(&bw.count, end - start_end);
}
const remaining_splat = splat - 1;
switch (pattern.len) {
0 => {
bw.end = end;
return track(&bw.count, end - start_end);
},
1 => {
const new_end = end + remaining_splat;
if (new_end <= buffer.len) {
@branchHint(.likely);
@memset(buffer[end..new_end], pattern[0]);
bw.end = new_end;
return track(&bw.count, new_end - start_end);
}
buffers[0] = buffer[0..end];
buffers[1] = pattern;
const n = try bw.unbuffered_writer.writeSplat(buffers[0..2], remaining_splat);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
},
else => {
const new_end = end + pattern.len * remaining_splat;
if (new_end <= buffer.len) {
@branchHint(.likely);
while (end < new_end) : (end += pattern.len) {
@memcpy(buffer[end..][0..pattern.len], pattern);
}
bw.end = new_end;
return track(&bw.count, new_end - start_end);
}
buffers[0] = buffer[0..end];
buffers[1] = pattern;
const n = try bw.unbuffered_writer.writeSplat(buffers[0..2], remaining_splat);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
},
}
}
fn track(count: *usize, n: usize) usize {
count.* += n;
return n;
}
/// When this function is called it 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.
fn fixed_writeSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) anyerror!usize {
const bw: *BufferedWriter = @alignCast(@ptrCast(context));
for (data) |bytes| {
const dest = bw.buffer[bw.end..];
if (dest.len == 0) return error.NoSpaceLeft;
const len = @min(bytes.len, dest.len);
@memcpy(dest[0..len], bytes[0..len]);
bw.end += len;
bw.count = bw.end;
}
const pattern = data[data.len - 1];
const dest = bw.buffer[bw.end..];
switch (pattern.len) {
0 => unreachable,
1 => @memset(dest, pattern[0]),
else => for (0..splat - 1) |i| @memcpy(dest[i * pattern.len ..][0..pattern.len], pattern),
}
bw.end = bw.buffer.len;
bw.count = bw.end;
return error.NoSpaceLeft;
}
pub fn write(bw: *BufferedWriter, bytes: []const u8) anyerror!usize {
const buffer = bw.buffer;
const end = bw.end;
const new_end = end + bytes.len;
if (new_end > buffer.len) {
var data: [2][]const u8 = .{ buffer[0..end], bytes };
const n = try bw.unbuffered_writer.writev(&data);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return 0;
}
bw.end = 0;
return track(&bw.count, n - end);
}
@memcpy(buffer[end..new_end], bytes);
bw.end = new_end;
return track(&bw.count, bytes.len);
}
/// Calls `write` as many times as necessary such that all of `bytes` are
/// transferred.
pub fn writeAll(bw: *BufferedWriter, bytes: []const u8) anyerror!void {
var index: usize = 0;
while (index < bytes.len) index += try write(bw, bytes[index..]);
}
pub fn print(bw: *BufferedWriter, comptime format: []const u8, args: anytype) anyerror!void {
try std.fmt.format(bw, format, args);
}
pub fn writeByte(bw: *BufferedWriter, byte: u8) anyerror!void {
const buffer = bw.buffer[0..bw.end];
if (buffer.len < bw.buffer.len) {
@branchHint(.likely);
buffer.ptr[buffer.len] = byte;
bw.end = buffer.len + 1;
bw.count += 1;
return;
}
var buffers: [2][]const u8 = .{ buffer, &.{byte} };
while (true) {
const n = try bw.unbuffered_writer.writev(&buffers);
if (n == 0) {
@branchHint(.unlikely);
continue;
}
bw.count += 1;
if (n >= buffer.len) {
@branchHint(.likely);
if (n > buffer.len) {
@branchHint(.likely);
bw.end = 0;
return;
} else {
buffer[0] = byte;
bw.end = 1;
return;
}
}
const remainder = buffer[n..];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
buffer[remainder.len] = byte;
bw.end = remainder.len + 1;
return;
}
}
/// Writes the same byte many times, performing the underlying write call as
/// many times as necessary.
pub fn splatByteAll(bw: *BufferedWriter, byte: u8, n: usize) anyerror!void {
var remaining: usize = n;
while (remaining > 0) remaining -= try splatByte(bw, byte, remaining);
}
/// Writes the same byte many times, allowing short writes.
///
/// Does maximum of one underlying `Writer.VTable.writeSplat`.
pub fn splatByte(bw: *BufferedWriter, byte: u8, n: usize) anyerror!usize {
return passthru_writeSplat(bw, &.{&.{byte}}, n);
}
/// Writes the same slice many times, performing the underlying write call as
/// many times as necessary.
pub fn splatBytesAll(bw: *BufferedWriter, bytes: []const u8, splat: usize) anyerror!void {
var remaining_bytes: usize = bytes.len * splat;
remaining_bytes -= try splatBytes(bw, 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 splatBytes(bw, &buffers, splat);
}
}
/// Writes the same slice many times, allowing short writes.
///
/// Does maximum of one underlying `Writer.VTable.writev`.
pub fn splatBytes(bw: *BufferedWriter, bytes: []const u8, n: usize) anyerror!usize {
return passthru_writeSplat(bw, &.{bytes}, n);
}
/// Asserts the `buffer` was initialized with a capacity of at least `@sizeOf(T)` bytes.
pub inline fn writeInt(bw: *BufferedWriter, comptime T: type, value: T, endian: std.builtin.Endian) anyerror!void {
var bytes: [@divExact(@typeInfo(T).int.bits, 8)]u8 = undefined;
std.mem.writeInt(std.math.ByteAlignedInt(@TypeOf(value)), &bytes, value, endian);
return bw.writeAll(&bytes);
}
pub fn writeStruct(bw: *BufferedWriter, value: anytype) anyerror!void {
// Only extern and packed structs have defined in-memory layout.
comptime assert(@typeInfo(@TypeOf(value)).@"struct".layout != .auto);
return bw.writeAll(std.mem.asBytes(&value));
}
pub fn writeStructEndian(bw: *BufferedWriter, value: anytype, endian: std.builtin.Endian) anyerror!void {
// TODO: make sure this value is not a reference type
if (native_endian == endian) {
return bw.writeStruct(value);
} else {
var copy = value;
std.mem.byteSwapAllFields(@TypeOf(value), &copy);
return bw.writeStruct(copy);
}
}
pub fn writeFile(
bw: *BufferedWriter,
file: std.fs.File,
offset: Writer.Offset,
limit: Writer.Limit,
headers_and_trailers: []const []const u8,
headers_len: usize,
) anyerror!usize {
return passthru_writeFile(bw, file, offset, limit, headers_and_trailers, headers_len);
}
fn passthru_writeFile(
context: ?*anyopaque,
file: std.fs.File,
offset: Writer.Offset,
limit: Writer.Limit,
headers_and_trailers: []const []const u8,
headers_len: usize,
) anyerror!usize {
const bw: *BufferedWriter = @alignCast(@ptrCast(context));
const buffer = bw.buffer;
if (buffer.len == 0) return track(
&bw.count,
try bw.unbuffered_writer.writeFile(file, offset, limit, headers_and_trailers, headers_len),
);
const start_end = bw.end;
const headers = headers_and_trailers[0..headers_len];
const trailers = headers_and_trailers[headers_len..];
var buffers: [max_buffers_len][]const u8 = undefined;
var end = start_end;
for (headers, 0..) |header, i| {
const new_end = end + header.len;
if (new_end <= buffer.len) {
@branchHint(.likely);
@memcpy(buffer[end..new_end], header);
end = new_end;
continue;
}
buffers[0] = buffer[0..end];
const remaining_headers = headers[i..];
const remaining_buffers = buffers[1..];
const buffers_len: usize = @min(remaining_headers.len, remaining_buffers.len);
@memcpy(remaining_buffers[0..buffers_len], remaining_headers[0..buffers_len]);
if (buffers_len >= remaining_headers.len) {
// Made it past the headers, so we can call `writeFile`.
const remaining_buffers_for_trailers = remaining_buffers[buffers_len..];
const send_trailers_len: usize = @min(trailers.len, remaining_buffers_for_trailers.len);
@memcpy(remaining_buffers_for_trailers[0..send_trailers_len], trailers[0..send_trailers_len]);
const send_headers_len = 1 + buffers_len;
const send_buffers = buffers[0 .. send_headers_len + send_trailers_len];
const n = try bw.unbuffered_writer.writeFile(file, offset, limit, send_buffers, send_headers_len);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
}
// Have not made it past the headers yet; must call `writev`.
const n = try bw.unbuffered_writer.writev(buffers[0 .. buffers_len + 1]);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
}
// All headers written to buffer.
buffers[0] = buffer[0..end];
const remaining_buffers = buffers[1..];
const send_trailers_len: usize = @min(trailers.len, remaining_buffers.len);
@memcpy(remaining_buffers[0..send_trailers_len], trailers[0..send_trailers_len]);
const send_headers_len = 1;
const send_buffers = buffers[0 .. send_headers_len + send_trailers_len];
const n = try bw.unbuffered_writer.writeFile(file, offset, limit, send_buffers, send_headers_len);
if (n < end) {
@branchHint(.unlikely);
const remainder = buffer[n..end];
std.mem.copyForwards(u8, buffer[0..remainder.len], remainder);
bw.end = remainder.len;
return track(&bw.count, end - start_end);
}
bw.end = 0;
return track(&bw.count, n - start_end);
}
pub const WriteFileOptions = struct {
offset: Writer.Offset = .none,
/// If the size of the source file is known, it is likely that passing the
/// size here will save one syscall.
limit: Writer.Limit = .unlimited,
/// Headers and trailers must be passed together so that in case `len` is
/// zero, they can be forwarded directly to `Writer.VTable.writev`.
///
/// The parameter is mutable because this function needs to mutate the
/// fields in order to handle partial writes from `Writer.VTable.writeFile`.
headers_and_trailers: [][]const u8 = &.{},
/// The number of trailers is inferred from
/// `headers_and_trailers.len - headers_len`.
headers_len: usize = 0,
};
pub fn writeFileAll(bw: *BufferedWriter, file: std.fs.File, options: WriteFileOptions) anyerror!void {
const headers_and_trailers = options.headers_and_trailers;
const headers = headers_and_trailers[0..options.headers_len];
switch (options.limit) {
.nothing => return writevAll(bw, headers_and_trailers),
.unlimited => {
// When reading the whole file, we cannot include the trailers in the
// call that reads from the file handle, because we have no way to
// determine whether a partial write is past the end of the file or
// not.
var i: usize = 0;
var offset = options.offset;
while (true) {
var n = try writeFile(bw, file, offset, .unlimited, headers[i..], headers.len - i);
while (i < headers.len and n >= headers[i].len) {
n -= headers[i].len;
i += 1;
}
if (i < headers.len) {
headers[i] = headers[i][n..];
continue;
}
if (n == 0) break;
offset = offset.advance(n);
}
},
else => {
var len = options.limit.toInt().?;
var i: usize = 0;
var offset = options.offset;
while (true) {
var n = try writeFile(bw, file, offset, .limited(len), headers_and_trailers[i..], headers.len - i);
while (i < headers.len and n >= headers[i].len) {
n -= headers[i].len;
i += 1;
}
if (i < headers.len) {
headers[i] = headers[i][n..];
continue;
}
if (n >= len) {
n -= len;
if (i >= headers_and_trailers.len) return;
while (n >= headers_and_trailers[i].len) {
n -= headers_and_trailers[i].len;
i += 1;
if (i >= headers_and_trailers.len) return;
}
headers_and_trailers[i] = headers_and_trailers[i][n..];
return writevAll(bw, headers_and_trailers[i..]);
}
offset = offset.advance(n);
len -= n;
}
},
}
}
pub fn alignBuffer(
bw: *BufferedWriter,
buffer: []const u8,
width: usize,
alignment: std.fmt.Alignment,
fill: u8,
) anyerror!void {
const padding = if (buffer.len < width) width - buffer.len else 0;
if (padding == 0) {
@branchHint(.likely);
return bw.writeAll(buffer);
}
switch (alignment) {
.left => {
try bw.writeAll(buffer);
try bw.splatByteAll(fill, padding);
},
.center => {
const left_padding = padding / 2;
const right_padding = (padding + 1) / 2;
try bw.splatByteAll(fill, left_padding);
try bw.writeAll(buffer);
try bw.splatByteAll(fill, right_padding);
},
.right => {
try bw.splatByteAll(fill, padding);
try bw.writeAll(buffer);
},
}
}
pub fn alignBufferOptions(bw: *BufferedWriter, buffer: []const u8, options: std.fmt.Options) anyerror!void {
return alignBuffer(bw, buffer, options.width orelse buffer.len, options.alignment, options.fill);
}
pub fn printAddress(bw: *BufferedWriter, value: anytype) anyerror!void {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.pointer => |info| {
try bw.writeAll(@typeName(info.child) ++ "@");
if (info.size == .slice)
try printIntOptions(bw, @intFromPtr(value.ptr), 16, .lower, .{})
else
try printIntOptions(bw, @intFromPtr(value), 16, .lower, .{});
return;
},
.optional => |info| {
if (@typeInfo(info.child) == .pointer) {
try bw.writeAll(@typeName(info.child) ++ "@");
try printIntOptions(bw, @intFromPtr(value), 16, .lower, .{});
return;
}
},
else => {},
}
@compileError("cannot format non-pointer type " ++ @typeName(T) ++ " with * specifier");
}
pub fn printValue(
bw: *BufferedWriter,
comptime fmt: []const u8,
options: std.fmt.Options,
value: anytype,
max_depth: usize,
) anyerror!void {
const T = @TypeOf(value);
const actual_fmt = comptime if (std.mem.eql(u8, fmt, ANY))
defaultFormatString(T)
else if (fmt.len != 0 and (fmt[0] == '?' or fmt[0] == '!')) switch (@typeInfo(T)) {
.optional, .error_union => fmt,
else => stripOptionalOrErrorUnionSpec(fmt),
} else fmt;
if (comptime std.mem.eql(u8, actual_fmt, "*")) {
return printAddress(bw, value);
}
if (std.meta.hasMethod(T, "format")) {
if (fmt.len > 0 and fmt[0] == 'f') {
return value.format(bw, fmt[1..]);
} else if (fmt.len == 0) {
// after 0.15.0 is tagged, delete the hasMethod condition and this compile error
@compileError("ambiguous format string; specify {f} to call format method, or {any} to skip it");
}
}
switch (@typeInfo(T)) {
.float, .comptime_float => return printFloat(bw, actual_fmt, options, value),
.int, .comptime_int => return printInt(bw, actual_fmt, options, value),
.bool => {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
return alignBufferOptions(bw, if (value) "true" else "false", options);
},
.void => {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
return alignBufferOptions(bw, "void", options);
},
.optional => {
if (actual_fmt.len == 0 or actual_fmt[0] != '?')
@compileError("cannot print optional without a specifier (i.e. {?} or {any})");
const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
if (value) |payload| {
return printValue(bw, remaining_fmt, options, payload, max_depth);
} else {
return alignBufferOptions(bw, "null", options);
}
},
.error_union => {
if (actual_fmt.len == 0 or actual_fmt[0] != '!')
@compileError("cannot format error union without a specifier (i.e. {!} or {any})");
const remaining_fmt = comptime stripOptionalOrErrorUnionSpec(actual_fmt);
if (value) |payload| {
return printValue(bw, remaining_fmt, options, payload, max_depth);
} else |err| {
return printValue(bw, "", options, err, max_depth);
}
},
.error_set => {
if (actual_fmt.len > 0 and actual_fmt[0] == 's') {
return bw.writeAll(@errorName(value));
} else if (actual_fmt.len != 0) {
invalidFmtError(fmt, value);
} else {
try bw.writeAll("error.");
try bw.writeAll(@errorName(value));
}
},
.@"enum" => |enum_info| {
try bw.writeAll(@typeName(T));
if (enum_info.is_exhaustive) {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
try bw.writeAll(".");
try bw.writeAll(@tagName(value));
return;
}
// Use @tagName only if value is one of known fields
@setEvalBranchQuota(3 * enum_info.fields.len);
inline for (enum_info.fields) |enumField| {
if (@intFromEnum(value) == enumField.value) {
try bw.writeAll(".");
try bw.writeAll(@tagName(value));
return;
}
}
try bw.writeByte('(');
try printValue(bw, actual_fmt, options, @intFromEnum(value), max_depth);
try bw.writeByte(')');
},
.@"union" => |info| {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
try bw.writeAll(@typeName(T));
if (max_depth == 0) {
bw.writeAll("{ ... }");
return;
}
if (info.tag_type) |UnionTagType| {
try bw.writeAll("{ .");
try bw.writeAll(@tagName(@as(UnionTagType, value)));
try bw.writeAll(" = ");
inline for (info.fields) |u_field| {
if (value == @field(UnionTagType, u_field.name)) {
try printValue(bw, ANY, options, @field(value, u_field.name), max_depth - 1);
}
}
try bw.writeAll(" }");
} else {
try bw.writeByte('@');
try bw.printIntOptions(@intFromPtr(&value), 16, .lower);
}
},
.@"struct" => |info| {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
if (info.is_tuple) {
// Skip the type and field names when formatting tuples.
if (max_depth == 0) {
try bw.writeAll("{ ... }");
return;
}
try bw.writeAll("{");
inline for (info.fields, 0..) |f, i| {
if (i == 0) {
try bw.writeAll(" ");
} else {
try bw.writeAll(", ");
}
try printValue(bw, ANY, options, @field(value, f.name), max_depth - 1);
}
try bw.writeAll(" }");
return;
}
try bw.writeAll(@typeName(T));
if (max_depth == 0) {
try bw.writeAll("{ ... }");
return;
}
try bw.writeAll("{");
inline for (info.fields, 0..) |f, i| {
if (i == 0) {
try bw.writeAll(" .");
} else {
try bw.writeAll(", .");
}
try bw.writeAll(f.name);
try bw.writeAll(" = ");
try printValue(bw, ANY, options, @field(value, f.name), max_depth - 1);
}
try bw.writeAll(" }");
},
.pointer => |ptr_info| switch (ptr_info.size) {
.one => switch (@typeInfo(ptr_info.child)) {
.array, .@"enum", .@"union", .@"struct" => {
return printValue(bw, actual_fmt, options, value.*, max_depth);
},
else => {
var buffers: [2][]const u8 = .{ @typeName(ptr_info.child), "@" };
try writevAll(bw, &buffers);
try printIntOptions(bw, @intFromPtr(value), 16, .lower, options);
return;
},
},
.many, .c => {
if (actual_fmt.len == 0)
@compileError("cannot format pointer without a specifier (i.e. {s} or {*})");
if (ptr_info.sentinel() != null) {
return printValue(bw, actual_fmt, options, std.mem.span(value), max_depth);
}
if (actual_fmt[0] == 's' and ptr_info.child == u8) {
return alignBufferOptions(bw, std.mem.span(value), options);
}
invalidFmtError(fmt, value);
},
.slice => {
if (actual_fmt.len == 0)
@compileError("cannot format slice without a specifier (i.e. {s}, {x}, {b64}, or {any})");
if (max_depth == 0) {
return bw.writeAll("{ ... }");
}
if (ptr_info.child == u8) switch (actual_fmt.len) {
1 => switch (actual_fmt[0]) {
's' => return alignBufferOptions(bw, value, options),
'x' => return printHex(bw, value, .lower),
'X' => return printHex(bw, value, .upper),
else => {},
},
3 => if (actual_fmt[0] == 'b' and actual_fmt[1] == '6' and actual_fmt[2] == '4') {
return printBase64(bw, value);
},
else => {},
};
try bw.writeAll("{ ");
for (value, 0..) |elem, i| {
try printValue(bw, actual_fmt, options, elem, max_depth - 1);
if (i != value.len - 1) {
try bw.writeAll(", ");
}
}
try bw.writeAll(" }");
},
},
.array => |info| {
if (actual_fmt.len == 0)
@compileError("cannot format array without a specifier (i.e. {s} or {any})");
if (max_depth == 0) {
return bw.writeAll("{ ... }");
}
if (info.child == u8) {
if (actual_fmt[0] == 's') {
return alignBufferOptions(bw, &value, options);
} else if (actual_fmt[0] == 'x') {
return printHex(bw, &value, .lower);
} else if (actual_fmt[0] == 'X') {
return printHex(bw, &value, .upper);
}
}
try bw.writeAll("{ ");
for (value, 0..) |elem, i| {
try printValue(bw, actual_fmt, options, elem, max_depth - 1);
if (i < value.len - 1) {
try bw.writeAll(", ");
}
}
try bw.writeAll(" }");
},
.vector => |info| {
if (max_depth == 0) {
return bw.writeAll("{ ... }");
}
try bw.writeAll("{ ");
var i: usize = 0;
while (i < info.len) : (i += 1) {
try printValue(bw, actual_fmt, options, value[i], max_depth - 1);
if (i < info.len - 1) {
try bw.writeAll(", ");
}
}
try bw.writeAll(" }");
},
.@"fn" => @compileError("unable to format function body type, use '*const " ++ @typeName(T) ++ "' for a function pointer type"),
.type => {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
return alignBufferOptions(bw, @typeName(value), options);
},
.enum_literal => {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
const buffer = [_]u8{'.'} ++ @tagName(value);
return alignBufferOptions(bw, buffer, options);
},
.null => {
if (actual_fmt.len != 0) invalidFmtError(fmt, value);
return alignBufferOptions(bw, "null", options);
},
else => @compileError("unable to format type '" ++ @typeName(T) ++ "'"),
}
}
pub fn printInt(
bw: *BufferedWriter,
comptime fmt: []const u8,
options: std.fmt.Options,
value: anytype,
) anyerror!void {
const int_value = if (@TypeOf(value) == comptime_int) blk: {
const Int = std.math.IntFittingRange(value, value);
break :blk @as(Int, value);
} else value;
switch (fmt.len) {
0 => return printIntOptions(bw, int_value, 10, .lower, options),
1 => switch (fmt[0]) {
'd' => return printIntOptions(bw, int_value, 10, .lower, options),
'c' => {
if (@typeInfo(@TypeOf(int_value)).int.bits <= 8) {
return printAsciiChar(bw, @as(u8, int_value), options);
} else {
@compileError("cannot print integer that is larger than 8 bits as an ASCII character");
}
},
'u' => {
if (@typeInfo(@TypeOf(int_value)).int.bits <= 21) {
return printUnicodeCodepoint(bw, @as(u21, int_value), options);
} else {
@compileError("cannot print integer that is larger than 21 bits as an UTF-8 sequence");
}
},
'b' => return printIntOptions(bw, int_value, 2, .lower, options),
'x' => return printIntOptions(bw, int_value, 16, .lower, options),
'X' => return printIntOptions(bw, int_value, 16, .upper, options),
'o' => return printIntOptions(bw, int_value, 8, .lower, options),
'B' => return printByteSize(bw, int_value, .decimal, options),
'D' => return printDuration(bw, int_value, options),
else => invalidFmtError(fmt, value),
},
2 => {
if (fmt[0] == 'B' and fmt[1] == 'i') {
return printByteSize(bw, int_value, .binary, options);
} else {
invalidFmtError(fmt, value);
}
},
else => invalidFmtError(fmt, value),
}
comptime unreachable;
}
pub fn printAsciiChar(bw: *BufferedWriter, c: u8, options: std.fmt.Options) anyerror!void {
return alignBufferOptions(bw, @as(*const [1]u8, &c), options);
}
pub fn printAscii(bw: *BufferedWriter, bytes: []const u8, options: std.fmt.Options) anyerror!void {
return alignBufferOptions(bw, bytes, options);
}
pub fn printUnicodeCodepoint(bw: *BufferedWriter, c: u21, options: std.fmt.Options) anyerror!void {
var buf: [4]u8 = undefined;
const len = try std.unicode.utf8Encode(c, &buf);
return alignBufferOptions(bw, buf[0..len], options);
}
pub fn printIntOptions(
bw: *BufferedWriter,
value: anytype,
base: u8,
case: std.fmt.Case,
options: std.fmt.Options,
) anyerror!void {
assert(base >= 2);
const int_value = if (@TypeOf(value) == comptime_int) blk: {
const Int = std.math.IntFittingRange(value, value);
break :blk @as(Int, value);
} else value;
const value_info = @typeInfo(@TypeOf(int_value)).int;
// The type must have the same size as `base` or be wider in order for the
// division to work
const min_int_bits = comptime @max(value_info.bits, 8);
const MinInt = std.meta.Int(.unsigned, min_int_bits);
const abs_value = @abs(int_value);
// The worst case in terms of space needed is base 2, plus 1 for the sign
var buf: [1 + @max(@as(comptime_int, value_info.bits), 1)]u8 = undefined;
var a: MinInt = abs_value;
var index: usize = buf.len;
if (base == 10) {
while (a >= 100) : (a = @divTrunc(a, 100)) {
index -= 2;
buf[index..][0..2].* = std.fmt.digits2(@intCast(a % 100));
}
if (a < 10) {
index -= 1;
buf[index] = '0' + @as(u8, @intCast(a));
} else {
index -= 2;
buf[index..][0..2].* = std.fmt.digits2(@intCast(a));
}
} else {
while (true) {
const digit = a % base;
index -= 1;
buf[index] = std.fmt.digitToChar(@intCast(digit), case);
a /= base;
if (a == 0) break;
}
}
if (value_info.signedness == .signed) {
if (value < 0) {
// Negative integer
index -= 1;
buf[index] = '-';
} else if (options.width == null or options.width.? == 0) {
// Positive integer, omit the plus sign
} else {
// Positive integer
index -= 1;
buf[index] = '+';
}
}
return alignBufferOptions(bw, buf[index..], options);
}
pub fn printFloat(
bw: *BufferedWriter,
comptime fmt: []const u8,
options: std.fmt.Options,
value: anytype,
) anyerror!void {
var buf: [std.fmt.float.bufferSize(.decimal, f64)]u8 = undefined;
if (fmt.len > 1) invalidFmtError(fmt, value);
switch (if (fmt.len == 0) 'e' else fmt[0]) {
'e' => {
const s = std.fmt.float.render(&buf, value, .{ .mode = .scientific, .precision = options.precision }) catch |err| switch (err) {
error.BufferTooSmall => "(float)",
};
return alignBufferOptions(bw, s, options);
},
'd' => {
const s = std.fmt.float.render(&buf, value, .{ .mode = .decimal, .precision = options.precision }) catch |err| switch (err) {
error.BufferTooSmall => "(float)",
};
return alignBufferOptions(bw, s, options);
},
'x' => {
var sub_bw: BufferedWriter = undefined;
sub_bw.initFixed(&buf);
sub_bw.printFloatHexadecimal(value, options.precision) catch unreachable;
return alignBufferOptions(bw, sub_bw.getWritten(), options);
},
else => invalidFmtError(fmt, value),
}
}
pub fn printFloatHexadecimal(bw: *BufferedWriter, value: anytype, opt_precision: ?usize) anyerror!void {
if (std.math.signbit(value)) try bw.writeByte('-');
if (std.math.isNan(value)) return bw.writeAll("nan");
if (std.math.isInf(value)) return bw.writeAll("inf");
const T = @TypeOf(value);
const TU = std.meta.Int(.unsigned, @bitSizeOf(T));
const mantissa_bits = std.math.floatMantissaBits(T);
const fractional_bits = std.math.floatFractionalBits(T);
const exponent_bits = std.math.floatExponentBits(T);
const mantissa_mask = (1 << mantissa_bits) - 1;
const exponent_mask = (1 << exponent_bits) - 1;
const exponent_bias = (1 << (exponent_bits - 1)) - 1;
const as_bits: TU = @bitCast(value);
var mantissa = as_bits & mantissa_mask;
var exponent: i32 = @as(u16, @truncate((as_bits >> mantissa_bits) & exponent_mask));
const is_denormal = exponent == 0 and mantissa != 0;
const is_zero = exponent == 0 and mantissa == 0;
if (is_zero) {
// Handle this case here to simplify the logic below.
try bw.writeAll("0x0");
if (opt_precision) |precision| {
if (precision > 0) {
try bw.writeAll(".");
try bw.splatByteAll('0', precision);
}
} else {
try bw.writeAll(".0");
}
try bw.writeAll("p0");
return;
}
if (is_denormal) {
// Adjust the exponent for printing.
exponent += 1;
} else {
if (fractional_bits == mantissa_bits)
mantissa |= 1 << fractional_bits; // Add the implicit integer bit.
}
const mantissa_digits = (fractional_bits + 3) / 4;
// Fill in zeroes to round the fraction width to a multiple of 4.
mantissa <<= mantissa_digits * 4 - fractional_bits;
if (opt_precision) |precision| {
// Round if needed.
if (precision < mantissa_digits) {
// We always have at least 4 extra bits.
var extra_bits = (mantissa_digits - precision) * 4;
// The result LSB is the Guard bit, we need two more (Round and
// Sticky) to round the value.
while (extra_bits > 2) {
mantissa = (mantissa >> 1) | (mantissa & 1);
extra_bits -= 1;
}
// Round to nearest, tie to even.
mantissa |= @intFromBool(mantissa & 0b100 != 0);
mantissa += 1;
// Drop the excess bits.
mantissa >>= 2;
// Restore the alignment.
mantissa <<= @as(std.math.Log2Int(TU), @intCast((mantissa_digits - precision) * 4));
const overflow = mantissa & (1 << 1 + mantissa_digits * 4) != 0;
// Prefer a normalized result in case of overflow.
if (overflow) {
mantissa >>= 1;
exponent += 1;
}
}
}
// +1 for the decimal part.
var buf: [1 + mantissa_digits]u8 = undefined;
assert(std.fmt.printInt(&buf, mantissa, 16, .lower, .{ .fill = '0', .width = 1 + mantissa_digits }) == buf.len);
try bw.writeAll("0x");
try bw.writeByte(buf[0]);
const trimmed = std.mem.trimRight(u8, buf[1..], "0");
if (opt_precision) |precision| {
if (precision > 0) try bw.writeAll(".");
} else if (trimmed.len > 0) {
try bw.writeAll(".");
}
try bw.writeAll(trimmed);
// Add trailing zeros if explicitly requested.
if (opt_precision) |precision| if (precision > 0) {
if (precision > trimmed.len)
try bw.splatByteAll('0', precision - trimmed.len);
};
try bw.writeAll("p");
try printIntOptions(bw, exponent - exponent_bias, 10, .lower, .{});
}
pub const ByteSizeUnits = enum {
/// This formatter represents the number as multiple of 1000 and uses the SI
/// measurement units (kB, MB, GB, ...).
decimal,
/// This formatter represents the number as multiple of 1024 and uses the IEC
/// measurement units (KiB, MiB, GiB, ...).
binary,
};
/// Format option `precision` is ignored when `value` is less than 1kB
pub fn printByteSize(
bw: *std.io.BufferedWriter,
value: u64,
comptime units: ByteSizeUnits,
options: std.fmt.Options,
) anyerror!void {
if (value == 0) return alignBufferOptions(bw, "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 alignBufferOptions(bw, buf[0..i], options);
}
// This ANY const is a workaround for: https://github.com/ziglang/zig/issues/7948
const ANY = "any";
fn defaultFormatString(comptime T: type) [:0]const u8 {
switch (@typeInfo(T)) {
.array, .vector => return ANY,
.pointer => |ptr_info| switch (ptr_info.size) {
.one => switch (@typeInfo(ptr_info.child)) {
.array => return ANY,
else => {},
},
.many, .c => return "*",
.slice => return ANY,
},
.optional => |info| return "?" ++ defaultFormatString(info.child),
.error_union => |info| return "!" ++ defaultFormatString(info.payload),
else => {},
}
return "";
}
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(bw: *BufferedWriter, ns: i64) anyerror!void {
if (ns < 0) try bw.writeByte('-');
return printDurationUnsigned(bw, @abs(ns));
}
pub fn printDurationUnsigned(bw: *BufferedWriter, ns: u64) anyerror!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 bw.printIntOptions(units, 10, .lower, .{});
try bw.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 bw.printIntOptions(kunits / 1000, 10, .lower, .{});
const frac = kunits % 1000;
if (frac > 0) {
// Write up to 3 decimal places
var decimal_buf = [_]u8{ '.', 0, 0, 0 };
var inner: BufferedWriter = undefined;
inner.initFixed(decimal_buf[1..]);
inner.printIntOptions(frac, 10, .lower, .{ .fill = '0', .width = 3 }) catch unreachable;
var end: usize = 4;
while (end > 1) : (end -= 1) {
if (decimal_buf[end - 1] != '0') break;
}
try bw.writeAll(decimal_buf[0..end]);
}
return bw.writeAll(unit.sep);
}
}
try printIntOptions(bw, ns_remaining, 10, .lower, .{});
try bw.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(bw: *BufferedWriter, nanoseconds: anytype, options: std.fmt.Options) anyerror!void {
// worst case: "-XXXyXXwXXdXXhXXmXX.XXXs".len = 24
var buf: [24]u8 = undefined;
var sub_bw: BufferedWriter = undefined;
sub_bw.initFixed(&buf);
switch (@typeInfo(@TypeOf(nanoseconds)).int.signedness) {
.signed => sub_bw.printDurationSigned(nanoseconds) catch unreachable,
.unsigned => sub_bw.printDurationUnsigned(nanoseconds) catch unreachable,
}
return alignBufferOptions(bw, sub_bw.getWritten(), options);
}
pub fn printHex(bw: *BufferedWriter, bytes: []const u8, case: std.fmt.Case) anyerror!void {
const charset = switch (case) {
.upper => "0123456789ABCDEF",
.lower => "0123456789abcdef",
};
for (bytes) |c| {
try writeByte(bw, charset[c >> 4]);
try writeByte(bw, charset[c & 15]);
}
}
pub fn printBase64(bw: *BufferedWriter, bytes: []const u8) anyerror!void {
var chunker = std.mem.window(u8, bytes, 3, 3);
var temp: [5]u8 = undefined;
while (chunker.next()) |chunk| {
try bw.writeAll(std.base64.standard.Encoder.encode(&temp, chunk));
}
}
/// Write a single unsigned integer as unsigned LEB128 to the given writer.
pub fn writeUleb128(bw: *std.io.BufferedWriter, arg: anytype) anyerror!void {
const Arg = @TypeOf(arg);
const Int = switch (Arg) {
comptime_int => std.math.IntFittingRange(arg, arg),
else => Arg,
};
const Value = if (@typeInfo(Int).int.bits < 8) u8 else Int;
var value: Value = arg;
while (true) {
const byte: u8 = @truncate(value & 0x7f);
value >>= 7;
if (value == 0) {
try bw.writeByte(byte);
return;
} else {
try bw.writeByte(byte | 0x80);
}
}
}
/// Write a single signed integer as signed LEB128 to the given writer.
pub fn writeIleb128(bw: *std.io.BufferedWriter, arg: anytype) anyerror!void {
const Arg = @TypeOf(arg);
const Int = switch (Arg) {
comptime_int => std.math.IntFittingRange(-@abs(arg), @abs(arg)),
else => Arg,
};
const Signed = if (@typeInfo(Int).int.bits < 8) i8 else Int;
const Unsigned = std.meta.Int(.unsigned, @typeInfo(Signed).int.bits);
var value: Signed = arg;
while (true) {
const unsigned: Unsigned = @bitCast(value);
const byte: u8 = @truncate(unsigned);
value >>= 6;
if (value == -1 or value == 0) {
try bw.writeByte(byte & 0x7F);
return;
} else {
value >>= 1;
try bw.writeByte(byte | 0x80);
}
}
}
test "formatValue max_depth" {
const Vec2 = struct {
const SelfType = @This();
x: f32,
y: f32,
pub fn format(
self: SelfType,
comptime fmt: []const u8,
options: std.fmt.Options,
bw: *BufferedWriter,
) anyerror!void {
_ = options;
if (fmt.len == 0) {
return bw.print("({d:.3},{d:.3})", .{ self.x, self.y });
} else {
@compileError("unknown format string: '" ++ fmt ++ "'");
}
}
};
const E = enum {
One,
Two,
Three,
};
const TU = union(enum) {
const SelfType = @This();
float: f32,
int: u32,
ptr: ?*SelfType,
};
const S = struct {
const SelfType = @This();
a: ?*SelfType,
tu: TU,
e: E,
vec: Vec2,
};
var inst = S{
.a = null,
.tu = TU{ .ptr = null },
.e = E.Two,
.vec = Vec2{ .x = 10.2, .y = 2.22 },
};
inst.a = &inst;
inst.tu.ptr = &inst.tu;
var buf: [1000]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.printValue("", .{}, inst, 0);
try testing.expectEqualStrings("io.BufferedWriter.test.printValue max_depth.S{ ... }", bw.getWritten());
bw.reset();
try bw.printValue("", .{}, inst, 1);
try testing.expectEqualStrings("io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ ... }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ ... }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", bw.getWritten());
bw.reset();
try bw.printValue("", .{}, inst, 2);
try testing.expectEqualStrings("io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ ... }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ ... }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ .ptr = io.BufferedWriter.test.printValue max_depth.TU{ ... } }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", bw.getWritten());
bw.reset();
try bw.printValue("", .{}, inst, 3);
try testing.expectEqualStrings("io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ .a = io.BufferedWriter.test.printValue max_depth.S{ ... }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ ... }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ .ptr = io.BufferedWriter.test.printValue max_depth.TU{ ... } }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }, .tu = io.BufferedWriter.test.printValue max_depth.TU{ .ptr = io.BufferedWriter.test.printValue max_depth.TU{ .ptr = io.BufferedWriter.test.printValue max_depth.TU{ ... } } }, .e = io.BufferedWriter.test.printValue max_depth.E.Two, .vec = (10.200,2.220) }", bw.getWritten());
const vec: @Vector(4, i32) = .{ 1, 2, 3, 4 };
bw.reset();
try bw.printValue("", .{}, vec, 0);
try testing.expectEqualStrings("{ ... }", bw.getWritten());
bw.reset();
try bw.printValue("", .{}, vec, 1);
try testing.expectEqualStrings("{ 1, 2, 3, 4 }", bw.getWritten());
}
test printDuration {
testDurationCase("0ns", 0);
testDurationCase("1ns", 1);
testDurationCase("999ns", std.time.ns_per_us - 1);
testDurationCase("1us", std.time.ns_per_us);
testDurationCase("1.45us", 1450);
testDurationCase("1.5us", 3 * std.time.ns_per_us / 2);
testDurationCase("14.5us", 14500);
testDurationCase("145us", 145000);
testDurationCase("999.999us", std.time.ns_per_ms - 1);
testDurationCase("1ms", std.time.ns_per_ms + 1);
testDurationCase("1.5ms", 3 * std.time.ns_per_ms / 2);
testDurationCase("1.11ms", 1110000);
testDurationCase("1.111ms", 1111000);
testDurationCase("1.111ms", 1111100);
testDurationCase("999.999ms", std.time.ns_per_s - 1);
testDurationCase("1s", std.time.ns_per_s);
testDurationCase("59.999s", std.time.ns_per_min - 1);
testDurationCase("1m", std.time.ns_per_min);
testDurationCase("1h", std.time.ns_per_hour);
testDurationCase("1d", std.time.ns_per_day);
testDurationCase("1w", std.time.ns_per_week);
testDurationCase("1y", 365 * std.time.ns_per_day);
testDurationCase("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1
testDurationCase("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms);
testDurationCase("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us);
testDurationCase("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1);
testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms);
testDurationCase("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1);
testDurationCase("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999);
testDurationCase("584y49w23h34m33.709s", std.math.maxInt(u64));
testing.expectFmt("=======0ns", "{D:=>10}", .{0});
testing.expectFmt("1ns=======", "{D:=<10}", .{1});
testing.expectFmt(" 999ns ", "{D:^10}", .{std.time.ns_per_us - 1});
}
test printDurationSigned {
testDurationCaseSigned("0ns", 0);
testDurationCaseSigned("1ns", 1);
testDurationCaseSigned("-1ns", -(1));
testDurationCaseSigned("999ns", std.time.ns_per_us - 1);
testDurationCaseSigned("-999ns", -(std.time.ns_per_us - 1));
testDurationCaseSigned("1us", std.time.ns_per_us);
testDurationCaseSigned("-1us", -(std.time.ns_per_us));
testDurationCaseSigned("1.45us", 1450);
testDurationCaseSigned("-1.45us", -(1450));
testDurationCaseSigned("1.5us", 3 * std.time.ns_per_us / 2);
testDurationCaseSigned("-1.5us", -(3 * std.time.ns_per_us / 2));
testDurationCaseSigned("14.5us", 14500);
testDurationCaseSigned("-14.5us", -(14500));
testDurationCaseSigned("145us", 145000);
testDurationCaseSigned("-145us", -(145000));
testDurationCaseSigned("999.999us", std.time.ns_per_ms - 1);
testDurationCaseSigned("-999.999us", -(std.time.ns_per_ms - 1));
testDurationCaseSigned("1ms", std.time.ns_per_ms + 1);
testDurationCaseSigned("-1ms", -(std.time.ns_per_ms + 1));
testDurationCaseSigned("1.5ms", 3 * std.time.ns_per_ms / 2);
testDurationCaseSigned("-1.5ms", -(3 * std.time.ns_per_ms / 2));
testDurationCaseSigned("1.11ms", 1110000);
testDurationCaseSigned("-1.11ms", -(1110000));
testDurationCaseSigned("1.111ms", 1111000);
testDurationCaseSigned("-1.111ms", -(1111000));
testDurationCaseSigned("1.111ms", 1111100);
testDurationCaseSigned("-1.111ms", -(1111100));
testDurationCaseSigned("999.999ms", std.time.ns_per_s - 1);
testDurationCaseSigned("-999.999ms", -(std.time.ns_per_s - 1));
testDurationCaseSigned("1s", std.time.ns_per_s);
testDurationCaseSigned("-1s", -(std.time.ns_per_s));
testDurationCaseSigned("59.999s", std.time.ns_per_min - 1);
testDurationCaseSigned("-59.999s", -(std.time.ns_per_min - 1));
testDurationCaseSigned("1m", std.time.ns_per_min);
testDurationCaseSigned("-1m", -(std.time.ns_per_min));
testDurationCaseSigned("1h", std.time.ns_per_hour);
testDurationCaseSigned("-1h", -(std.time.ns_per_hour));
testDurationCaseSigned("1d", std.time.ns_per_day);
testDurationCaseSigned("-1d", -(std.time.ns_per_day));
testDurationCaseSigned("1w", std.time.ns_per_week);
testDurationCaseSigned("-1w", -(std.time.ns_per_week));
testDurationCaseSigned("1y", 365 * std.time.ns_per_day);
testDurationCaseSigned("-1y", -(365 * std.time.ns_per_day));
testDurationCaseSigned("1y52w23h59m59.999s", 730 * std.time.ns_per_day - 1); // 365d = 52w1d
testDurationCaseSigned("-1y52w23h59m59.999s", -(730 * std.time.ns_per_day - 1)); // 365d = 52w1d
testDurationCaseSigned("1y1h1.001s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms);
testDurationCaseSigned("-1y1h1.001s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + std.time.ns_per_ms));
testDurationCaseSigned("1y1h1s", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us);
testDurationCaseSigned("-1y1h1s", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_s + 999 * std.time.ns_per_us));
testDurationCaseSigned("1y1h999.999us", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1);
testDurationCaseSigned("-1y1h999.999us", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms - 1));
testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms);
testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms));
testDurationCaseSigned("1y1h1ms", 365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1);
testDurationCaseSigned("-1y1h1ms", -(365 * std.time.ns_per_day + std.time.ns_per_hour + std.time.ns_per_ms + 1));
testDurationCaseSigned("1y1m999ns", 365 * std.time.ns_per_day + std.time.ns_per_min + 999);
testDurationCaseSigned("-1y1m999ns", -(365 * std.time.ns_per_day + std.time.ns_per_min + 999));
testDurationCaseSigned("292y24w3d23h47m16.854s", std.math.maxInt(i64));
testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64) + 1);
testDurationCaseSigned("-292y24w3d23h47m16.854s", std.math.minInt(i64));
testing.expectFmt("=======0ns", "{s:=>10}", .{0});
testing.expectFmt("1ns=======", "{s:=<10}", .{1});
testing.expectFmt("-1ns======", "{s:=<10}", .{-(1)});
testing.expectFmt(" -999ns ", "{s:^10}", .{-(std.time.ns_per_us - 1)});
}
fn testDurationCase(expected: []const u8, input: u64) !void {
var buf: [24]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.printDurationUnsigned(input);
try testing.expectEqualStrings(expected, bw.getWritten());
}
fn testDurationCaseSigned(expected: []const u8, input: i64) !void {
var buf: [24]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.printDurationSigned(input);
try testing.expectEqualStrings(expected, bw.getWritten());
}
test printIntOptions {
try testPrintIntCase("-1", @as(i1, -1), 10, .lower, .{});
try testPrintIntCase("-101111000110000101001110", @as(i32, -12345678), 2, .lower, .{});
try testPrintIntCase("-12345678", @as(i32, -12345678), 10, .lower, .{});
try testPrintIntCase("-bc614e", @as(i32, -12345678), 16, .lower, .{});
try testPrintIntCase("-BC614E", @as(i32, -12345678), 16, .upper, .{});
try testPrintIntCase("12345678", @as(u32, 12345678), 10, .upper, .{});
try testPrintIntCase(" 666", @as(u32, 666), 10, .lower, .{ .width = 6 });
try testPrintIntCase(" 1234", @as(u32, 0x1234), 16, .lower, .{ .width = 6 });
try testPrintIntCase("1234", @as(u32, 0x1234), 16, .lower, .{ .width = 1 });
try testPrintIntCase("+42", @as(i32, 42), 10, .lower, .{ .width = 3 });
try testPrintIntCase("-42", @as(i32, -42), 10, .lower, .{ .width = 3 });
}
test "printInt with comptime_int" {
var buf: [20]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.printInt(@as(comptime_int, 123456789123456789), "", .{});
try std.testing.expectEqualStrings("123456789123456789", bw.getWritten());
}
test "printFloat with comptime_float" {
var buf: [20]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.printFloat("", .{}, @as(comptime_float, 1.0));
try std.testing.expectEqualStrings(bw.getWritten(), "1e0");
try std.testing.expectFmt("1e0", "{}", .{1.0});
}
fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {
var buffer: [100]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buffer);
bw.printIntOptions(value, base, case, options);
try testing.expectEqualStrings(expected, bw.getWritten());
}
test printByteSize {
try testing.expectFmt("file size: 42B\n", "file size: {B}\n", .{42});
try testing.expectFmt("file size: 42B\n", "file size: {Bi}\n", .{42});
try testing.expectFmt("file size: 63MB\n", "file size: {B}\n", .{63 * 1000 * 1000});
try testing.expectFmt("file size: 63MiB\n", "file size: {Bi}\n", .{63 * 1024 * 1024});
try testing.expectFmt("file size: 42B\n", "file size: {B:.2}\n", .{42});
try testing.expectFmt("file size: 42B\n", "file size: {B:>9.2}\n", .{42});
try testing.expectFmt("file size: 66.06MB\n", "file size: {B:.2}\n", .{63 * 1024 * 1024});
try testing.expectFmt("file size: 60.08MiB\n", "file size: {Bi:.2}\n", .{63 * 1000 * 1000});
try testing.expectFmt("file size: =66.06MB=\n", "file size: {B:=^9.2}\n", .{63 * 1024 * 1024});
try testing.expectFmt("file size: 66.06MB\n", "file size: {B: >9.2}\n", .{63 * 1024 * 1024});
try testing.expectFmt("file size: 66.06MB \n", "file size: {B: <9.2}\n", .{63 * 1024 * 1024});
try testing.expectFmt("file size: 0.01844674407370955ZB\n", "file size: {B}\n", .{std.math.maxInt(u64)});
}
test "bytes.hex" {
const some_bytes = "\xCA\xFE\xBA\xBE";
try std.testing.expectFmt("lowercase: cafebabe\n", "lowercase: {x}\n", .{some_bytes});
try std.testing.expectFmt("uppercase: CAFEBABE\n", "uppercase: {X}\n", .{some_bytes});
try std.testing.expectFmt("uppercase: CAFE\n", "uppercase: {X}\n", .{some_bytes[0..2]});
try std.testing.expectFmt("lowercase: babe\n", "lowercase: {x}\n", .{some_bytes[2..]});
const bytes_with_zeros = "\x00\x0E\xBA\xBE";
try std.testing.expectFmt("lowercase: 000ebabe\n", "lowercase: {x}\n", .{bytes_with_zeros});
}
test initFixed {
{
var buf: [255]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.print("{s}{s}!", .{ "Hello", "World" });
try testing.expectEqualStrings("HelloWorld!", bw.getWritten());
}
comptime {
var buf: [255]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buf);
try bw.print("{s}{s}!", .{ "Hello", "World" });
try testing.expectEqualStrings("HelloWorld!", bw.getWritten());
}
}
test "fixed output" {
var buffer: [10]u8 = undefined;
var bw: BufferedWriter = undefined;
bw.initFixed(&buffer);
try bw.writeAll("Hello");
try testing.expect(std.mem.eql(u8, bw.getWritten(), "Hello"));
try bw.writeAll("world");
try testing.expect(std.mem.eql(u8, bw.getWritten(), "Helloworld"));
try testing.expectError(error.WriteStreamEnd, bw.writeAll("!"));
try testing.expect(std.mem.eql(u8, bw.getWritten(), "Helloworld"));
bw.reset();
try testing.expect(bw.getWritten().len == 0);
try testing.expectError(error.WriteStreamEnd, bw.writeAll("Hello world!"));
try testing.expect(std.mem.eql(u8, bw.getWritten(), "Hello worl"));
try bw.seekTo((try bw.getEndPos()) + 1);
try testing.expectError(error.WriteStreamEnd, bw.writeAll("H"));
}