commit dcb58a755471cc9e24bc683208918c1d8f922f6f Author: Jeffrey C. Ollie Date: Thu Sep 21 10:54:27 2023 -0500 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dc382c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/zig-cache +/zig-out + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3e9cb48 --- /dev/null +++ b/build.zig @@ -0,0 +1,70 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "ztacacs", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9803fde --- /dev/null +++ b/flake.lock @@ -0,0 +1,292 @@ +{ + "nodes": { + "bash": { + "locked": { + "lastModified": 1678247195, + "narHash": "sha256-m/wSwlSket+hob3JED4XUvoWJLtW7yhtOiZrlRDMShs=", + "ref": "refs/heads/main", + "rev": "e7a00dcc0e75bc3ef6856bdd94d7d809245f5636", + "revCount": 1, + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + }, + "original": { + "type": "git", + "url": "https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git" + } + }, + "binned_allocator": { + "flake": false, + "locked": { + "narHash": "sha256-m/kr4kmkG2rLkAj5YwvM0HmXTd+chAiQHzYK6ozpWlw=", + "type": "tarball", + "url": "https://gist.github.com/antlilja/8372900fcc09e38d7b0b6bbaddad3904/archive/6c3321e0969ff2463f8335da5601986cf2108690.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://gist.github.com/antlilja/8372900fcc09e38d7b0b6bbaddad3904/archive/6c3321e0969ff2463f8335da5601986cf2108690.tar.gz" + } + }, + "diffz": { + "flake": false, + "locked": { + "narHash": "sha256-3CdYo6WevT0alRwKmbABahjhFKz7V9rdkDUZ43VtDeU=", + "type": "tarball", + "url": "https://github.com/ziglibs/diffz/archive/90353d401c59e2ca5ed0abe5444c29ad3d7489aa.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/ziglibs/diffz/archive/90353d401c59e2ca5ed0abe5444c29ad3d7489aa.tar.gz" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "zls", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1694102001, + "narHash": "sha256-vky6VPK1n1od6vXbqzOXnekrQpTL4hbPAwUhT5J9c9E=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "9e21c80adf67ebcb077d75bd5e7d724d21eeafd6", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "known_folders": { + "flake": false, + "locked": { + "narHash": "sha256-bZfn+jgCzrtm8vKPDDMNWLkJYoo7vKxZu+e2tGvSGHY=", + "type": "tarball", + "url": "https://github.com/ziglibs/known-folders/archive/a564f582122326328dad6b59209d070d57c4e6ae.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/ziglibs/known-folders/archive/a564f582122326328dad6b59209d070d57c4e6ae.tar.gz" + } + }, + "langref": { + "flake": false, + "locked": { + "narHash": "sha256-UDwr6vJynfpD5SEoZzhXouoKu+Okdtpv20Vx2E5Ltcc=", + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/f1992a39a59b941f397b8501a525b38e5863a527/doc/langref.html.in" + }, + "original": { + "type": "file", + "url": "https://raw.githubusercontent.com/ziglang/zig/f1992a39a59b941f397b8501a525b38e5863a527/doc/langref.html.in" + } + }, + "make-shell": { + "locked": { + "lastModified": 1634940815, + "narHash": "sha256-P69OmveboXzS+es1vQGS4bt+ckwbeIExqxfGLjGuJqA=", + "owner": "ursi", + "repo": "nix-make-shell", + "rev": "8add91681170924e4d0591b22f294aee3f5516f9", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "nix-make-shell", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1694959747, + "narHash": "sha256-CXQ2MuledDVlVM5dLC4pB41cFlBWxRw4tCBsFrq3cRk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "970a59bd19eff3752ce552935687100c46e820a5", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1689088367, + "narHash": "sha256-Y2tl2TlKCWEHrOeM9ivjCLlRAKH3qoPUE/emhZECU14=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5c9ddb86679c400d6b7360797b8a22167c2053f8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "bash": "bash", + "flake-utils": "flake-utils", + "make-shell": "make-shell", + "nixpkgs": "nixpkgs", + "zig": "zig", + "zls": "zls" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1695125316, + "narHash": "sha256-9Ewco7m4zgajBhppCM1mEmQE/K6ObkbwUhtJ3lJlfto=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "078666381440c3303566a8a0f34628703202b54e", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + }, + "zls": { + "inputs": { + "binned_allocator": "binned_allocator", + "diffz": "diffz", + "flake-utils": "flake-utils_3", + "gitignore": "gitignore", + "known_folders": "known_folders", + "langref": "langref", + "nixpkgs": [ + "nixpkgs" + ], + "zig-overlay": [ + "zig" + ] + }, + "locked": { + "lastModified": 1695231678, + "narHash": "sha256-R6z0+6U7okQxmOR867nUTCzbRwtuGokGgtqvXP78XK8=", + "owner": "zigtools", + "repo": "zls", + "rev": "20b80784998e342296473a2192ea70935ab84b8d", + "type": "github" + }, + "original": { + "owner": "zigtools", + "repo": "zls", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0ad1ff3 --- /dev/null +++ b/flake.nix @@ -0,0 +1,78 @@ +{ + description = "ztacacs"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + bash = { + url = "git+https://git.ocjtech.us/jeff/nixos-bash-prompt-builder.git"; + }; + make-shell = { + url = "github:ursi/nix-make-shell"; + }; + zig = { + url = "github:mitchellh/zig-overlay"; + }; + zls = { + url = "github:zigtools/zls"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.zig-overlay.follows = "zig"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, bash, ... }@inputs: + let + # overlays = [ + # ( + # final: prev: { + # zigpkgs = inputs.zig.packages.${prev.system}; + # } + + # ) + # ]; + systems = builtins.attrNames inputs.zig.packages; + in + flake-utils.lib.eachSystem systems ( + system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + devShells.default = + let + project = "ztacacs"; + prompt = ( + bash.build_prompt + bash.ansi_normal_blue + "${project} - ${bash.username}@${bash.hostname_short}: ${bash.current_working_directory}" + "${project}:${bash.current_working_directory}" + ); + make-shell = import inputs.make-shell { + inherit system; + pkgs = pkgs; + }; + in + make-shell { + packages = [ + inputs.zig.packages.${system}.master + inputs.zls.packages.${system}.zls + ]; + env = { + PS1 = prompt; + }; + }; + # packages.default = inputs.zig.packages.${system}.zigStdenv.mkDerivation { + # pname = "ztacacs"; + # version = "0.1.0"; + # buildInputs = [ ]; + # src = ./.; + # }; + } + ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c8e3f03 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const serde = @import("serde.zig"); +const packet = @import("packet.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + var socket = std.net.StreamServer.init(.{ .reuse_address = true, .reuse_port = true }); + defer socket.deinit(); + + try socket.listen(std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 4949)); + + while (true) { + const server = try allocator.create(Server); + server.* = Server{ + .allocator = allocator, + .conn = try socket.accept(), + }; + const thread = try std.Thread.spawn(.{ .allocator = allocator }, Server.run, .{server}); + thread.detach(); + } +} + +const Server = struct { + allocator: std.mem.Allocator, + conn: std.net.StreamServer.Connection, + + const Self = @This(); + + pub fn run(self: *Self) !void { + std.debug.print("starting: {}\n", .{self.conn.address}); + defer self.deinit(); + + while (true) { + var buffer: [@sizeOf(packet.Header)]u8 = undefined; + const len = try self.conn.stream.read(buffer[0..]); + // if (len < @sizeOf(packet.Header)) break; + if (len == 0) break; + std.debug.print("{}\n", .{len}); + _ = try self.conn.stream.write(buffer[0..len]); + } + + std.debug.print("finished\n", .{}); + } + + fn deinit(self: *Self) void { + std.debug.print("start deinit\n", .{}); + self.conn.stream.close(); + self.allocator.destroy(self); + std.debug.print("end deinit\n", .{}); + } +}; + +test "main" { + std.debug.print("hello\n", .{}); +} diff --git a/src/packet.zig b/src/packet.zig new file mode 100644 index 0000000..cd82695 --- /dev/null +++ b/src/packet.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const serde = @import("serde.zig"); + +pub const MajorVersion = enum(u4) { + Default = 0xc, + _, +}; + +pub const MinorVersion = enum(u4) { + Default = 0x0, + One = 0x1, + _, +}; + +pub const PacketType = enum(u8) { + Authentication = 1, + Authorization = 2, + Accounting = 3, + _, +}; + +pub const HeaderFlags = packed struct(u8) { + Unencrypted: bool = false, + _p1: u1 = 0, + SingleConnect: bool = false, + _p2: u5 = 0, + + pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) std.os.WriteError!void { + try writer.print("{s}{{ .Unencrypted = {}, .SingleConnect = {} }}", .{ + @typeName(@This()), + value.Unencrypted, + value.SingleConnect, + }); + } +}; + +pub const Header = struct { + major: MajorVersion, + minor: MinorVersion, + type: PacketType, + seq_no: u8, + flags: HeaderFlags, + session_id: u32, + length: u32, +}; + +test "header 1" { + const packet = [_]u8{ 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var reader = serde.Reader.init(&packet); + const header = try reader.read(Header); + std.debug.print("{}\n", .{header}); + try std.testing.expect(header.major == .Default); + try std.testing.expect(header.minor == .Default); + try std.testing.expect(header.type == .Authentication); + try std.testing.expect(header.seq_no == 0); + try std.testing.expect(!header.flags.Unencrypted); + try std.testing.expect(!header.flags.SingleConnect); + try std.testing.expect(header.session_id == 0); + try std.testing.expect(header.length == 0); +} + +test "header 2" { + const packet = [_]u8{ 0xc0, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var reader = serde.Reader.init(&packet); + const header = try reader.read(Header); + std.debug.print("{}\n", .{header}); + try std.testing.expect(header.major == .Default); + try std.testing.expect(header.minor == .Default); + try std.testing.expect(header.type == .Authorization); + try std.testing.expect(header.seq_no == 2); + try std.testing.expect(header.flags.Unencrypted); + try std.testing.expect(!header.flags.SingleConnect); + try std.testing.expect(header.session_id == 0); + try std.testing.expect(header.length == 0); +} + +test "header 3" { + const packet = [_]u8{ 0xc0, 0x03, 0xff, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + var reader = serde.Reader.init(&packet); + const header = try reader.read(Header); + std.debug.print("{}\n", .{header}); + try std.testing.expect(header.major == .Default); + try std.testing.expect(header.minor == .Default); + try std.testing.expect(header.type == .Accounting); + try std.testing.expect(header.seq_no == 255); + try std.testing.expect(!header.flags.Unencrypted); + try std.testing.expect(header.flags.SingleConnect); + try std.testing.expect(header.session_id == 0); + try std.testing.expect(header.length == 0); +} + +pub const AuthenticationAction = enum(u8) { + Login = 0x01, + ChangePassword = 0x02, + SendAuth = 0x03, + _, +}; + +pub const AuthenticationType = enum(u8) { + ASCII = 0x01, + PAP = 0x02, + CHAP = 0x03, + MSCHAP = 0x05, + MSCHAPV2 = 0x06, + _, +}; + +pub const AuthenticationService = enum(u8) { + NONE = 0x00, + LOGIN = 0x01, + ENABLE = 0x02, + PPP = 0x03, + PT = 0x05, + RCMD = 0x06, + X25 = 0x07, + NASI = 0x08, + FWPROXY = 0x09, + _, +}; + +pub const AuthenticationStartHeader = struct { + action: AuthenticationAction, + priv_lvl: u8, + authen_type: AuthenticationType, + authen_service: AuthenticationService, + user_len: u8, + port_len: u8, + rem_addr_len: u8, + data_len: u8, +}; + +pub const AuthenticationStatus = enum(u8) { + PASS = 0x01, + FAIL = 0x02, + GETDATA = 0x03, + GETUSER = 0x04, + GETPASS = 0x05, + RESTART = 0x06, + ERROR = 0x07, + FOLLOW = 0x21, +}; + +pub const AuthenticationReplyFlags = packed struct(u8) { + NoEcho: bool, + _p1: u7 = 0, + + pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) std.os.WriteError!void { + try writer.print("{s}{{ .NoEcho = {}, }}", .{ + @typeName(@This()), + value.NoEcho, + }); + } +}; + +pub const AuthenticationReplyHeader = struct { + status: AuthenticationStatus, + flags: AuthenticationReplyFlags, + server_msg_len: u16, + data_len: u16, +}; + +pub const AuthenticationContinueFlags = packed struct(u8) { + Abort: bool, + _p1: u7 = 0, + + pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) std.os.WriteError!void { + try writer.print("{s}{{ .Abort = {}, }}", .{ + @typeName(@This()), + value.Abort, + }); + } +}; + +pub const AuthenticationContinueHeader = struct { + user_msg: u16, + data_len: u16, + flags: AuthenticationContinueFlags, +}; + +pub const AuthorizationRequestHeader = struct { + authen_method: u8, + priv_lvl: u8, + authen_type: u8, + authen_service: u8, + user_len: u8, + port_len: u8, + rem_addr_len: u8, + arg_cnt: u8, +}; diff --git a/src/serde.zig b/src/serde.zig new file mode 100644 index 0000000..555b097 --- /dev/null +++ b/src/serde.zig @@ -0,0 +1,103 @@ +const std = @import("std"); + +pub const SerdeError = error{ + IllegalSize, + LeftoverBits, + NotEnoughBitsLeft, + NotEnoughBytesLeft, +}; + +pub const Reader = struct { + bytes: []const u8, + index: usize, + bits_left: usize, + + pub fn init(bytes: []const u8) Reader { + return .{ + .bytes = bytes, + .index = 0, + .bits_left = 8, + }; + } + + pub fn read(self: *Reader, comptime T: type) SerdeError!T { + if (self.index >= self.bytes.len) return error.NotEnoughBytesLeft; + return switch (@typeInfo(T)) { + .Int => try self.readInt(T), + .Bool => try self.readBool(T), + .Enum => try self.readEnum(T), + .Struct => try self.readStruct(T), + .Array => |array| { + var arr: [array.len]array.child = undefined; + var index: usize = 0; + while (index < array.len) : (index += 1) { + arr[index] = try self.read(array.child); + } + return arr; + }, + else => @compileError("unsupported type"), + }; + } + + fn readInt(self: *Reader, comptime T: type) SerdeError!T { + const bits = @typeInfo(T).Int.bits; + + if (bits < 8) { + if (bits > self.bits_left) return error.NotEnoughBitsLeft; + const shift: u3 = @truncate(self.bits_left - bits); + const mask = (1 << bits) - 1; + const b = (self.bytes[self.index] >> shift) & mask; + self.bits_left -= bits; + if (self.bits_left == 0) { + self.bits_left = 8; + self.index += 1; + } + + return @intCast(b); + } + + if (bits % 8 == 0 and self.bits_left != 8) return error.LeftoverBits; + if (bits % 8 != 0) return error.IllegalSize; + + const size = bits / 8; + + if (self.index + size > self.bytes.len) return error.NotEnoughBytesLeft; + + const slice = self.bytes[self.index .. self.index + size]; + const value = @as(*align(1) const T, @ptrCast(slice)).*; + + self.index += size; + return std.mem.bigToNative(T, value); + } + + fn readBool(self: *Reader, comptime T: type) SerdeError!T { + const x = try self.read(u1); + std.debug.print("bool {}\n", .{x}); + return x == 1; + } + + fn readEnum(self: *Reader, comptime T: type) SerdeError!T { + return @enumFromInt(try self.read(@typeInfo(T).Enum.tag_type)); + } + + fn readStruct(self: *Reader, comptime T: type) SerdeError!T { + if (@typeInfo(T).Struct.layout == .Packed) { + const value = try self.read(@typeInfo(T).Struct.backing_integer.?); + return @as(*const T, @ptrCast(&value)).*; + } + + const fields = std.meta.fields(T); + + var value: T = undefined; + + inline for (fields) |field| { + @field(value, field.name) = try self.read(field.type); + } + + return value; + } + + pub fn isComplete(self: *Reader) bool { + return self.index >= self.bytes.len; + } +};