diff --git a/lib/std/io.zig b/lib/std/io.zig index 640f575654e2..636fd0ff7ae0 100644 --- a/lib/std/io.zig +++ b/lib/std/io.zig @@ -5,6 +5,7 @@ const c = std.c; const is_windows = builtin.os.tag == .windows; const windows = std.os.windows; const posix = std.posix; +const native_endian = @import("builtin").target.cpu.arch.endian(); const math = std.math; const assert = std.debug.assert; @@ -362,6 +363,277 @@ pub fn GenericWriter( }; } +/// A stream which has a certain number of bytes available to be read before `read` is called on +/// `std.io.`(`Any`)`Reader`. Retrieving the available data is referred to as "peeking". A "peek" +/// does not have any effect, other than possibly making more data available for reading before +/// `read` is called. Future `read` calls, however, do change what data is available for +/// peeking. Keep this in mind when using (`Any`)`Peeker` and an (`Any`)`Reader` on the same +/// underlying stream. +// +// Design considerations: +// +// - A `getMaximumAvailableBytes` function returning the maximum number of bytes that will be +// available for peeking. This would remove the need for guesswork and inefficient copies in +// functions like `peekArrayList`, but also increase the size of `AnyPeeker` if a virtual table +// approach like in `std.mem.Allocator` is not used. It's unclear whether all peekable streams can +// support such a function, and it is not necessary for the basic functionality provided by a +// peekable stream, so it is not yet included. +// - Having `peekFn` reference an internal buffer. This is more performant, but very bug-prone, +// *especially* since (`Any`)`Reader` and (`Any`)`Peeker` can be separated. Much like with +// `SeekableStream`, this makes little sense and adds clunkiness. As before, it's not clear if it +// can be applied to all peekable streams, and not required. +// - Instead of the number of bytes fulfilled, `peekFn` could return the number of bytes +// available. This could be unreliable though, since sometimes, the number of bytes that are +// avaiable and the number of bytes that could be available are not equal. +// - Revisit 0 bytes available being an EOF condition. +// - An `ungetc`-like API is intentionally not provided, to allow for read-only streams. +pub const AnyPeeker = struct { + context: *const anyopaque, + peekFn: *const fn (context: *const anyopaque, buffer: []u8) anyerror!usize, + + /// Returns the number of bytes peeked. It may be less than `buffer.len`, this indicates that no + /// more bytes are available. If the number of bytes peeked is 0, it means end of stream. End + /// of stream is not an error condition. + pub fn peek(self: AnyPeeker, buffer: []u8) anyerror!usize { + return self.peekFn(self.context, buffer); + } + + /// If the number of bytes available is smaller than `buffer.len`, `error.NotEnoughData` is + /// returned instead. + pub fn peekNoEof(self: AnyPeeker, buffer: []u8) anyerror!void { + const nb_fulfilled = try self.peek(buffer); + if (nb_fulfilled < buffer.len) return error.NotEnoughData; + } + + /// Peeks exactly `n` bytes. If the number of bytes available is smaller than `n`, + /// `error.NotEnoughData` is returned instead. + pub fn peekBytes(self: AnyPeeker, comptime n: usize) anyerror![n]u8 { + var buf: [n]u8 = undefined; + try self.peekNoEof(&buf); + return buf; + } + + /// Append up to `max_len` available bytes into `buf`. `error.StreamTooLong` is returned if more + /// than `max_len` bytes are available. + pub fn peekArrayList(self: AnyPeeker, buf: *std.ArrayList(u8), max_len: usize) anyerror!void { + return self.peekArrayListAligned(null, buf, max_len); + } + + /// Append up to `max_len` available bytes into `buf`. `error.StreamTooLong` is returned if more + /// than `max_len` bytes are available. + pub fn peekArrayListAligned( + self: AnyPeeker, + comptime alignment: ?u29, + buf: *std.ArrayListAligned(u8, alignment), + max_len: usize, + ) anyerror!void { + // Initial guess and other implementation details from `std.io.AnyReader.readAllArrayList`. + try buf.ensureTotalCapacity(@min(max_len, 4096)); + const cursor = buf.items.len; + + while (true) { + buf.expandToCapacity(); + + const dest = buf.items[cursor..]; + + const nb_peeked = try self.peek(dest); + buf.shrinkRetainingCapacity(cursor + nb_peeked); + + if (nb_peeked > max_len) + return error.StreamTooLong; + + if (nb_peeked <= dest.len) + return; + + try buf.ensureUnusedCapacity(1); + } + } + + /// Reads and allocates up to `max_len` available bytes. `error.StreamTooLong` is returned if + /// more than `max_len` bytes are available. + pub fn peekAlloc(self: AnyPeeker, allocator: std.mem.Allocator, max_len: usize) anyerror![]u8 { + var alist = std.ArrayList(u8).init(allocator); + try self.peekArrayList(&alist, max_len); + return try alist.toOwnedSlice(); + } + + /// Returns the next available byte or `error.NotEnoughData.`. + pub fn peekByte(self: AnyPeeker) anyerror!u8 { + const buf = try self.peekBytes(1); + return buf[0]; + } + + /// Returns the next available byte, interpreted as a signed integer, or `error.NotEnoughData`. + pub fn peekByteSigned(self: AnyPeeker) anyerror!i8 { + return @bitCast(try self.peekByte()); + } + + /// Returns the next available integer `Int` using `endian` or `error.NotEnoughData`. Marked + /// `inline` to propagate the comptimeness of `endian`. + pub inline fn peekInt(self: AnyPeeker, comptime Int: type, endian: std.builtin.Endian) anyerror!Int { + const buf = try self.peekBytes(@divExact(@bitSizeOf(Int), std.mem.byte_size_in_bits)); + return std.mem.readInt(Int, &buf, endian); + } + + /// Peeks `n` bytes and returns if they are equal to `array_p` or `error.NotEnoughData`. + pub fn isBytes(self: AnyPeeker, comptime n: usize, array_p: *const [n]u8) anyerror!bool { + const buf = try self.peekBytes(n); + return std.mem.eql(u8, &buf, array_p); + } + + /// Peeks `slice.len` bytes and returns if they are equal to `slice` or `error.NotEnoughData`. + pub fn isByteSlice(self: AnyPeeker, comptime slice: []const u8) anyerror!bool { + return self.isBytes(slice.len, slice[0..slice.len]); + } + + /// Peeks one struct of type `T`, which must have a defined layout, using the native endianness. + pub fn peekStruct(self: AnyPeeker, comptime T: type) anyerror!T { + // Only extern and packed structs have defined in-memory layout. + comptime std.debug.assert(@typeInfo(T).@"struct".layout != .auto); + return @bitCast(try self.peekBytes(@sizeOf(T))); + } + + /// Peeks one struct of type `T`, which must have a defined layout, using `endian`. Marked + /// `inline` to propagate the comptimeness of `endian`. + pub inline fn peekStructEndian(self: AnyPeeker, comptime T: type, endian: std.builtin.Endian) anyerror!T { + var res = try self.peekStruct(T); + if (native_endian != endian) { + std.mem.byteSwapAllFields(T, &res); + } + return res; + } + + /// Reads an integer with the same size as the given enum's tag type. If the integer matches an enum + /// tag, casts the integer to the enum tag and returns it. Otherwise, returns an + /// `error.InvalidValue`. Marked inline to propagate the comptimeness of `endian`. + pub inline fn peekEnum(self: AnyPeeker, comptime Enum: type, endian: std.builtin.Endian) anyerror!Enum { + const buf = try self.peekBytes(@divExact(@typeInfo(@typeInfo(Enum).@"enum".tag_type).int.bits, 8)); + return try std.mem.readEnum(Enum, &buf, endian); + } +}; + +pub fn GenericPeeker( + comptime Context: type, + comptime PeekError: type, + comptime peekFn: fn (context: Context, buffer: []u8) PeekError!usize, +) type { + return struct { + context: Context, + + pub const Error = PeekError; + pub const NotEnoughDataError = PeekError || error{ + NotEnoughData, + }; + + pub const StreamTooLongError = PeekError || error{ + StreamTooLong, + }; + + pub const PeekEnumError = NotEnoughDataError || error{ + /// An integer was read, but it did not match any of the tags in the supplied enum. + InvalidValue, + }; + + pub inline fn any(self: *const Self) AnyPeeker { + return .{ + .context = @ptrCast(&self.context), + .peekFn = typeErasedPeekFn, + }; + } + + const Self = @This(); + + fn typeErasedPeekFn(context: *const anyopaque, buffer: []u8) anyerror!usize { + const ptr: *const Context = @alignCast(@ptrCast(context)); + return peekFn(ptr.*, buffer); + } + + /// Returns the number of bytes peeked. It may be less than `buffer.len`, this indicates that no + /// more bytes are available. If the number of bytes peeked is 0, it means end of stream. End + /// of stream is not an error condition. + pub inline fn peek(self: Self, buffer: []u8) PeekError!usize { + return peekFn(self.context, buffer); + } + + /// If the number of bytes available is smaller than `buffer.len`, `error.NotEnoughData` is + /// returned instead. + pub inline fn peekNoEof(self: Self, buffer: []u8) NotEnoughDataError!void { + return @errorCast(self.any().peekNoEof(buffer)); + } + + /// Peeks exactly `n` bytes. If the number of bytes available is smaller than `n`, + /// `error.NotEnoughData` is returned instead. + pub inline fn peekBytes(self: Self, comptime n: usize) NotEnoughDataError![n]u8 { + return @errorCast(self.any().peekBytes(n)); + } + + /// Append up to `max_len` available bytes into `buf`. `error.StreamTooLong` is returned if more + /// than `max_len` bytes are available. + pub inline fn peekArrayList(self: Self, buf: *std.ArrayList(u8), max_len: usize) StreamTooLongError!void { + return @errorCast(self.any().peekArrayList(buf, max_len)); + } + + /// Append up to `max_len` available bytes into `buf`. `error.StreamTooLong` is returned if more + /// than `max_len` bytes are available. + pub inline fn peekArrayListAligned( + self: Self, + comptime alignment: ?u29, + buf: *std.ArrayListAligned(u8, alignment), + max_len: usize, + ) StreamTooLongError!void { + return @errorCast(self.any().peekArrayListAligned(alignment, buf, max_len)); + } + + /// Reads and allocates up to `max_len` available bytes. `error.StreamTooLong` is returned if + /// more than `max_len` bytes are available. + pub inline fn peekAlloc(self: Self, allocator: std.mem.Allocator, max_len: usize) StreamTooLongError![]u8 { + return @errorCast(self.any().peekAlloc(allocator, max_len)); + } + + /// Returns the next available byte or `error.NotEnoughData.`. + pub inline fn peekByte(self: Self) NotEnoughDataError!u8 { + return @errorCast(self.any().peekByte()); + } + + /// Returns the next available byte, interpreted as a signed integer, or `error.NotEnoughData`. + pub inline fn peekByteSigned(self: Self) NotEnoughDataError!i8 { + return @errorCast(self.any().peekByteSigned()); + } + + /// Returns the next available integer `Int` using `endian` or `error.NotEnoughData`. Marked + /// `inline` to propagate the comptimeness of `endian`. + pub inline fn peekInt(self: Self, comptime Int: type, endian: std.builtin.Endian) NotEnoughDataError!Int { + return @errorCast(self.any().peekInt(Int, endian)); + } + + /// Peeks `n` bytes and returns if they are equal to `array_p` or `error.NotEnoughData`. + pub fn isBytes(self: Self, comptime n: usize, array_p: *const [n]u8) NotEnoughDataError!bool { + return @errorCast(self.any().isBytes(n, array_p)); + } + + /// Peeks `slice.len` bytes and returns if they are equal to `slice` or `error.NotEnoughData`. + pub fn isByteSlice(self: Self, comptime slice: []const u8) NotEnoughDataError!bool { + return @errorCast(self.any().isByteSlice(slice)); + } + + /// Peeks one struct of type `T`, which must have a defined layout, using the native endianness. + pub fn peekStruct(self: Self, comptime T: type) NotEnoughDataError!T { + return @errorCast(self.any().peekStruct(T)); + } + + /// Peeks one struct of type `T`, which must have a defined layout, using `endian`. Marked + /// `inline` to propagate the comptimeness of `endian`. + pub inline fn peekStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian) NotEnoughDataError!T { + return @errorCast(self.any().peekStructEndian(T, endian)); + } + + /// Peeks one enum of type `Enum`, rounding up to the nearest 8 bits if necessary. + pub inline fn peekEnum(self: Self, comptime Enum: type, endian: std.builtin.Endian) PeekEnumError!Enum { + return @errorCast(self.any().peekEnum(Enum, endian)); + } + }; +} + /// Deprecated; consider switching to `AnyReader` or use `GenericReader` /// to use previous API. pub const Reader = GenericReader; diff --git a/lib/std/io/Reader.zig b/lib/std/io/Reader.zig index 33187125b86b..2d9f40c33cb1 100644 --- a/lib/std/io/Reader.zig +++ b/lib/std/io/Reader.zig @@ -344,20 +344,8 @@ pub fn readStructEndian(self: Self, comptime T: type, endian: std.builtin.Endian /// 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; + const buf = try self.readBytesNoEof(@divExact(@typeInfo(@typeInfo(Enum).@"enum".tag_type).int.bits, 8)); + return std.mem.readEnum(Enum, &buf, endian); } /// Reads the stream until the end, ignoring all the data. diff --git a/lib/std/io/buffered_reader.zig b/lib/std/io/buffered_reader.zig index bcf54fb88222..78c5a95f70d4 100644 --- a/lib/std/io/buffered_reader.zig +++ b/lib/std/io/buffered_reader.zig @@ -13,6 +13,7 @@ pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) ty pub const Error = ReaderType.Error; pub const Reader = io.Reader(*Self, Error, read); + pub const GenericPeeker = io.GenericPeeker(*Self, Error, peek); const Self = @This(); @@ -40,9 +41,59 @@ pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) ty return to_transfer; } + /// Returns at most `buffer_size` bytes of data into `dest` without advancing the stream + /// pointer, the data read is still available in the stream. Returning fewer than `dest.len` + /// bytes is not an error. `0` is returned if the end of the stream has been reached. + pub fn peek(self: *Self, dest: []u8) Error!usize { + const nb_buffered_bytes = self.end - self.start; + + if (dest.len <= nb_buffered_bytes) { + // Fulfill the peek from the buffer. + @memcpy(dest, self.buf[self.start..self.end][0..dest.len]); + return dest.len; + } + + // Trying to fulfill the peek by reading more into the buffer without moving is not a + // worthwhile tradeoff. This always leads to more calls to `read`, in the worst case, + // syscalls, which we would like to avoid. `copyForwards` is cheap in comparison. In the + // best case, read doesn't require a syscall, and most likely, peek is already + // implemented by the unbuffered reader. + + // Move the available data to the start of the buffer. + if (self.start != 0) + std.mem.copyForwards(u8, self.buf[0..nb_buffered_bytes], self.buf[self.start..self.end]); + + self.end = nb_buffered_bytes; + self.start = 0; + + // Because peeking isn't a stream, we can't simply ask the user to re-peek if `read` + // returns fewer bytes than is required for `dest` to be filled, so, we always try to + // read repeatedly for `dest` to be full. However, in line with `read`, we do not try to + // fill the buffer completely by calling read repeatedly. + + const desired_minimum_nb_bytes_read = dest.len - nb_buffered_bytes; + + while (self.end - nb_buffered_bytes < desired_minimum_nb_bytes_read) { + const nb_bytes_read = try self.unbuffered_reader.read(self.buf[self.end..]); + + if (nb_bytes_read == 0) + break; // EOS + + self.end += nb_bytes_read; + } + + const nb_bytes_result = @min(self.end, dest.len); + @memcpy(dest[0..nb_bytes_result], self.buf[0..nb_bytes_result]); + return nb_bytes_result; + } + pub fn reader(self: *Self) Reader { return .{ .context = self }; } + + pub fn peeker(self: *Self) GenericPeeker { + return .{ .context = self }; + } }; } @@ -199,3 +250,35 @@ test "Block" { try testing.expectEqual(try reader.readAll(&out_buf), 0); } } + +test "peek BufferedReader with FixedBufferStream" { + var fbs = io.fixedBufferStream("meow mrow grrr"); + var test_buf = bufferedReaderSize(5, fbs.reader()); + var tmp: [5]u8 = undefined; + + try std.testing.expectEqual(try test_buf.peek(tmp[0..1]), 1); + try std.testing.expectEqualStrings(tmp[0..1], "m"); + + try std.testing.expectEqual(try test_buf.peek(tmp[0..2]), 2); + try std.testing.expectEqualStrings(tmp[0..2], "me"); + + try std.testing.expectEqual(try test_buf.peek(tmp[0..5]), 5); + try std.testing.expectEqualStrings(tmp[0..5], "meow "); + + try std.testing.expectEqual(try test_buf.read(tmp[0..1]), 1); + try std.testing.expectEqualStrings(tmp[0..1], "m"); + + try std.testing.expectEqual(try test_buf.peek(tmp[0..4]), 4); + try std.testing.expectEqualStrings(tmp[0..4], "eow "); + + // requires move and read + try std.testing.expectEqual(try test_buf.peek(tmp[0..5]), 5); + try std.testing.expectEqualStrings(tmp[0..5], "eow m"); + + // clear buffer completely + try std.testing.expectEqual(try test_buf.read(tmp[0..5]), 5); + try std.testing.expectEqualStrings(tmp[0..5], "eow m"); + + try std.testing.expectEqual(try test_buf.peek(tmp[0..5]), 5); + try std.testing.expectEqualStrings(tmp[0..5], "row g"); +} diff --git a/lib/std/io/fixed_buffer_stream.zig b/lib/std/io/fixed_buffer_stream.zig index 7750c29fc8c0..3938993878da 100644 --- a/lib/std/io/fixed_buffer_stream.zig +++ b/lib/std/io/fixed_buffer_stream.zig @@ -16,9 +16,11 @@ pub fn FixedBufferStream(comptime Buffer: type) type { pub const WriteError = error{NoSpaceLeft}; pub const SeekError = error{}; pub const GetSeekPosError = error{}; + pub const PeekError = error{}; pub const Reader = io.Reader(*Self, ReadError, read); pub const Writer = io.Writer(*Self, WriteError, write); + pub const GenericPeeker = io.GenericPeeker(*Self, PeekError, peek); pub const SeekableStream = io.SeekableStream( *Self, @@ -44,14 +46,23 @@ pub fn FixedBufferStream(comptime Buffer: type) type { 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; + pub fn peeker(self: *Self) GenericPeeker { + return .{ .context = self }; + } + + pub fn peek(self: *Self, dest: []u8) PeekError!usize { + const len = @min(dest.len, self.buffer.len - self.pos); + const end = self.pos + len; - @memcpy(dest[0..size], self.buffer[self.pos..end]); - self.pos = end; + @memcpy(dest[0..len], self.buffer[self.pos..end]); - return size; + return len; + } + + pub fn read(self: *Self, dest: []u8) ReadError!usize { + const len = try self.peek(dest); + self.pos += len; + return len; } /// If the returned number of bytes written is less than requested, the @@ -196,3 +207,20 @@ test "input" { read = try fbs.reader().read(&dest); try testing.expect(read == 0); } +test "peek" { + const bytes = [_]u8{ 1, 2, 3, 4, 5, 6, 7 }; + var fbs = fixedBufferStream(&bytes); + + var dest: [4]u8 = undefined; + + try std.testing.expectEqual(try fbs.peek(dest[0..4]), 4); + try std.testing.expectEqualStrings(bytes[0..4], dest[0..4]); + + try std.testing.expectEqual(try fbs.read(dest[0..4]), 4); + + try std.testing.expectEqual(try fbs.peek(dest[0..2]), 2); + try std.testing.expectEqualStrings(bytes[4..][0..2], dest[0..2]); + + try std.testing.expectEqual(try fbs.peek(dest[0..4]), 3); + try std.testing.expectEqualStrings(bytes[4..][0..3], dest[0..3]); +} diff --git a/lib/std/io/test.zig b/lib/std/io/test.zig index 6505fcd4facf..294dbaf0cca8 100644 --- a/lib/std/io/test.zig +++ b/lib/std/io/test.zig @@ -188,3 +188,97 @@ test "GenericReader methods can return error.EndOfStream" { fbs.reader().isBytes("foo"), ); } + +test "peek with GenericPeeker with BufferedReader with FixedBufferStream" { + var fbs = io.fixedBufferStream("meow mrow grrr woem"); + var test_buf = std.io.bufferedReaderSize(5, fbs.reader()); + const peeker = test_buf.peeker(); + + { + var tmp: [2]u8 = undefined; + try std.testing.expectEqual(try peeker.peek(&tmp), 2); + try std.testing.expectEqualStrings(&tmp, "me"); + try peeker.peekNoEof(&tmp); + try std.testing.expectEqualStrings(&tmp, "me"); + } + + try std.testing.expectEqualStrings(&(try peeker.peekBytes(2)), "me"); + + { + var alist = std.ArrayList(u8).init(std.testing.allocator); + defer alist.deinit(); + try peeker.peekArrayList(&alist, 256); + try std.testing.expectEqualStrings("meow ", alist.items); + } + + { + var alist = std.ArrayListAligned(u8, 8).init(std.testing.allocator); + defer alist.deinit(); + try std.testing.expectError(error.StreamTooLong, peeker.peekArrayListAligned(8, &alist, 4)); + alist.shrinkAndFree(0); + try peeker.peekArrayListAligned(8, &alist, 256); + try std.testing.expectEqualStrings("meow ", alist.items); + } + + { + const all = try peeker.peekAlloc(std.testing.allocator, 256); + defer std.testing.allocator.free(all); + try std.testing.expectEqualStrings("meow ", all); + } + + try std.testing.expectEqual(try peeker.peekByte(), 'm'); + try std.testing.expectEqual(try peeker.peekByteSigned(), 'm'); + try std.testing.expectEqual(try peeker.peekInt(u32, .big), std.mem.readInt(u32, "meow", .big)); + try std.testing.expect(try peeker.isBytes(5, "meow ")); + try std.testing.expect(try peeker.isByteSlice("meow ")); + try std.testing.expectEqual((try peeker.peekStruct(extern struct { m: u8 })).m, 'm'); + try std.testing.expectEqual((try peeker.peekStructEndian(extern struct { m: u16 }, .big)).m, std.mem.readInt(u16, "me", .big)); + try std.testing.expectEqual(try peeker.peekEnum(enum(u8) { m = 'm' }, .big), .m); +} + +test "peek with GenericPeeker with FixedBufferStream" { + const bytes = "meow mrow grrr woem"; + var fbs = io.fixedBufferStream(bytes); + const peeker = fbs.peeker(); + + { + var tmp: [2]u8 = undefined; + try std.testing.expectEqual(try peeker.peek(&tmp), 2); + try std.testing.expectEqualStrings(&tmp, "me"); + try peeker.peekNoEof(&tmp); + try std.testing.expectEqualStrings(&tmp, "me"); + } + + try std.testing.expectEqualStrings(&(try peeker.peekBytes(2)), "me"); + + { + var alist = std.ArrayList(u8).init(std.testing.allocator); + defer alist.deinit(); + try peeker.peekArrayList(&alist, 256); + try std.testing.expectEqualStrings(bytes, alist.items); + } + + { + var alist = std.ArrayListAligned(u8, 8).init(std.testing.allocator); + defer alist.deinit(); + try std.testing.expectError(error.StreamTooLong, peeker.peekArrayListAligned(8, &alist, 4)); + alist.shrinkAndFree(0); + try peeker.peekArrayListAligned(8, &alist, 256); + try std.testing.expectEqualStrings(bytes, alist.items); + } + + { + const all = try peeker.peekAlloc(std.testing.allocator, 256); + defer std.testing.allocator.free(all); + try std.testing.expectEqualStrings(bytes, all); + } + + try std.testing.expectEqual(try peeker.peekByte(), 'm'); + try std.testing.expectEqual(try peeker.peekByteSigned(), 'm'); + try std.testing.expectEqual(try peeker.peekInt(u32, .big), std.mem.readInt(u32, "meow", .big)); + try std.testing.expect(try peeker.isBytes(6, "meow m")); + try std.testing.expect(try peeker.isByteSlice("meow m")); + try std.testing.expectEqual((try peeker.peekStruct(extern struct { m: u8 })).m, 'm'); + try std.testing.expectEqual((try peeker.peekStructEndian(extern struct { m: u16 }, .big)).m, std.mem.readInt(u16, "me", .big)); + try std.testing.expectEqual(try peeker.peekEnum(enum(u8) { m = 'm' }, .big), .m); +} diff --git a/lib/std/mem.zig b/lib/std/mem.zig index 1f1e925f54b1..02fe092c061f 100644 --- a/lib/std/mem.zig +++ b/lib/std/mem.zig @@ -1771,6 +1771,27 @@ test readInt { try comptime moreReadIntTests(); } +/// 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`. Marked inline to propagate the comptimeness of `endian`. +/// TODO optimization taking advantage of most fields being in order +pub inline fn readEnum( + comptime Enum: type, + buffer: *const [@divExact(@typeInfo(@typeInfo(Enum).@"enum".tag_type).int.bits, 8)]u8, + endian: Endian, +) error{InvalidValue}!Enum { + const type_info = @typeInfo(Enum).@"enum"; + const tag = readInt(type_info.tag_type, buffer, endian); + + inline for (std.meta.fields(Enum)) |field| { + if (tag == field.value) { + return @field(Enum, field.name); + } + } + + return error.InvalidValue; +} + fn readPackedIntLittle(comptime T: type, bytes: []const u8, bit_offset: usize) T { const uN = std.meta.Int(.unsigned, @bitSizeOf(T)); const Log2N = std.math.Log2Int(T);