const std = @import("std"); const Allocator = std.mem.Allocator; const cast = std.math.cast; pub const PackedUserSize = @divExact(@bitSizeOf(PackedUser), 8); pub const PackedUser = packed struct { uid: u32, gid: u32, additional_gids_offset: u29, shell_here: bool, shell_len_or_idx: u6, home_len: u6, name_is_a_suffix: bool, name_len: u5, gecos_len: u8, }; pub const User = struct { uid: u32, gid: u32, name: []const u8, gecos: []const u8, home: []const u8, shell: []const u8, }; // UserWriter accepts a naive User struct and returns a PackedUser pub const UserWriter = struct { // shellIndexFnType is a signature for a function that accepts a shell // string and returns it's index in the global shell section. Passing a // function makes tests easier, and removes the Shell dependency of this // module. const shellIndexFnType = fn ([]const u8) ?u6; allocator: Allocator, shellIndexFn: shellIndexFnType, pub fn init(allocator: Allocator, shellIndexFn: shellIndexFnType) UserWriter { return UserWriter{ .allocator = allocator, .shellIndexFn = shellIndexFn, }; } const fromUserErr = std.mem.Allocator.Error || error{InvalidRecord}; pub fn fromUser(self: *UserWriter, user: User) fromUserErr![]const u8 { const home_len = std.math.cast(u6, user.home.len - 1) catch return error.InvalidRecord; const name_len = cast(u5, user.name.len - 1) catch return error.InvalidRecord; const shell_len = cast(u6, user.shell.len - 1) catch return error.InvalidRecord; const gecos_len = cast(u8, user.gecos.len) catch return error.InvalidRecord; var bindata_len: u32 = home_len; var puser = PackedUser{ .uid = @as(u32, user.uid), .gid = @as(u32, user.gid), .additional_gids_offset = std.math.maxInt(u29), // needs second pass .shell_here = undefined, .shell_len_or_idx = undefined, .home_len = home_len, .name_is_a_suffix = undefined, .name_len = name_len, .gecos_len = gecos_len, }; if (std.mem.endsWith(u8, user.home, user.name)) { puser.name_is_a_suffix = true; } else { puser.name_is_a_suffix = false; bindata_len += name_len; } bindata_len += gecos_len; if (self.shellIndexFn(user.shell)) |idx| { puser.shell_here = false; puser.shell_len_or_idx = idx; } else { puser.shell_here = true; puser.shell_len_or_idx = shell_len; bindata_len += shell_len; } var result = try self.allocator.alloc(u8, PackedUserSize + bindata_len); const userPointer = @ptrCast([*]const u8, &puser); { var i: u32 = 0; while (i < PackedUserSize) { result[i] = userPointer[i]; i += 1; } } std.mem.copy(u8, result, user.home); if (!puser.name_is_a_suffix) { std.mem.copy(u8, result, user.name); } std.mem.copy(u8, result, user.gecos); if (puser.shell_here) { std.mem.copy(u8, result, user.shell); } return result; } }; const testing = std.testing; test "PackedUser is byte-aligned" { try testing.expectEqual(0, @rem(@bitSizeOf(PackedUser), 8)); } fn testShellIndex(shell: []const u8) ?u6 { if (std.mem.eql(u8, shell, "/bin/bash")) { return 0; } else if (std.mem.eql(u8, shell, "/bin/zsh")) { return 1; } return null; } test "construct PackedUser blob" { var writer = UserWriter.init(testing.allocator, testShellIndex); const user1 = User{ .uid = 1000, .gid = 1000, .name = "vidmantas", .gecos = "Vidmantas Kaminskas", .home = "/home/vidmantas", .shell = "/bin/bash", }; const user2 = User{ .uid = 1001, .gid = 1001, .name = "svc-foo", .gecos = "Service Account", .home = "/home/service1", .shell = "/usr/bin/nologin", }; const puser1 = try writer.fromUser(user1); const puser2 = try writer.fromUser(user2); defer testing.allocator.free(puser1); defer testing.allocator.free(puser2); }