const std = @import("std"); const pad = @import("padding.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const cast = std.math.cast; 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, // blobSize returns the length of the blob storing string values. pub fn blobLength(self: *const PackedUser) usize { var result: usize = self.realHomeLen(); if (!self.name_is_a_suffix) { result += self.realNameLen(); } result += self.realGecosLen(); if (self.shell_here) { result += self.realShellLen(); } return result; } pub fn realHomeLen(self: *const PackedUser) usize { return @as(u32, self.home_len) + 1; } pub fn realNameLen(self: *const PackedUser) usize { return @as(u32, self.name_len) + 1; } pub fn realShellLen(self: *const PackedUser) usize { return @as(u32, self.shell_len_or_idx) + 1; } pub fn realGecosLen(self: *const PackedUser) usize { return self.gecos_len; } }; const PackedUserAlignmentBits = 3; 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; appendTo: *ArrayList(u8), shellIndexFn: shellIndexFnType, pub fn init(appendTo: *ArrayList(u8), shellIndexFn: shellIndexFnType) UserWriter { return UserWriter{ .appendTo = appendTo, .shellIndexFn = shellIndexFn, }; } pub fn downCast(comptime T: type, n: u64) error{InvalidRecord}!T { return std.math.cast(T, n) catch |err| switch (err) { error.Overflow => { return error.InvalidRecord; }, }; } pub fn appendUser(self: *UserWriter, user: User) !void { const home_len = try downCast(u6, user.home.len - 1); const name_len = try downCast(u5, user.name.len - 1); const shell_len = try downCast(u6, user.shell.len - 1); const gecos_len = try downCast(u8, user.gecos.len); var puser = PackedUser{ .uid = user.uid, .gid = user.gid, .additional_gids_offset = std.math.maxInt(u29), // needs second pass .shell_here = self.shellIndexFn(user.shell) == null, .shell_len_or_idx = self.shellIndexFn(user.shell) orelse shell_len, .home_len = home_len, .name_is_a_suffix = std.mem.endsWith(u8, user.home, user.name), .name_len = name_len, .gecos_len = gecos_len, }; try self.appendTo.appendSlice(std.mem.asBytes(&puser)); try self.appendTo.appendSlice(user.home); if (!puser.name_is_a_suffix) { try self.appendTo.appendSlice(user.name); } try self.appendTo.appendSlice(user.gecos); if (puser.shell_here) { try self.appendTo.appendSlice(user.shell); } try self.appendTo.appendNTimes(0, pad.roundUpPadding( u64, PackedUserAlignmentBits, self.appendTo.items.len, )); } }; pub const UserReader = struct { const shellIndexProto = fn (u6) []const u8; blob: []u8, pub const PackedEntry = struct { packed_user: *PackedUser, blob: []const u8, }; pub fn init(blob: []u8) UserReader { return UserReader{ .blob = blob, }; } pub const PackedIterator = struct { ur: *UserReader, index: usize = 0, pub fn next(it: *PackedIterator) ?PackedEntry { if (it.index == it.ur.blob.len) return null; assert(it.index < it.ur.blob.len); // https://github.com/ziglang/zig/issues/1095 const packedUserSizeHere = @sizeOf(PackedUser); const endUser = it.index + packedUserSizeHere; var packedUser = std.mem.bytesAsValue( PackedUser, it.ur.blob[it.index..endUser][0..packedUserSizeHere], ); const startBlob = endUser; const endBlob = startBlob + packedUser.blobLength(); it.index = pad.roundUp(usize, PackedUserAlignmentBits, endBlob); return PackedEntry{ .packed_user = packedUser, .blob = it.ur.blob[startBlob..endBlob], }; } }; pub fn packedIterator(self: *UserReader) PackedIterator { return .{ .ur = self }; } pub const Iterator = struct { pit: PackedIterator, shellIndex: shellIndexProto, pub fn next(it: *Iterator) ?User { const entry = it.pit.next() orelse return null; const u = entry.packed_user; const home = entry.blob[0..u.realHomeLen()]; var name: []const u8 = undefined; var pos: usize = undefined; if (u.name_is_a_suffix) { const name_start = u.realHomeLen() - u.realNameLen(); name = entry.blob[name_start..u.realHomeLen()]; pos = u.realHomeLen(); } else { const name_start = u.realHomeLen(); name = entry.blob[name_start .. name_start + u.realNameLen()]; pos = name_start + u.realNameLen(); } const gecos = entry.blob[pos .. pos + u.realGecosLen()]; pos += u.realGecosLen(); var shell: []const u8 = undefined; if (u.shell_here) { shell = entry.blob[pos .. pos + u.realShellLen()]; } else { shell = it.shellIndex(u.shell_len_or_idx); } return User{ .uid = u.uid, .gid = u.gid, .name = name, .gecos = gecos, .home = home, .shell = shell, }; } }; pub fn iterator(self: *UserReader, shellIndex: shellIndexProto) Iterator { return .{ .pit = self.packedIterator(), .shellIndex = shellIndex, }; } }; const testing = std.testing; test "PackedUser alignment" { // byte-aligned try testing.expectEqual(0, @rem(@bitSizeOf(PackedUser), 8)); const bytes = @divExact(@bitSizeOf(PackedUser), 8); // External padding (PackedUserAlignmentBits) must be higher or equal to // the "internal" PackedUser alignment. By aligning PackedUser we are also // working around https://github.com/ziglang/zig/issues/10958 ; PackedUser // cannot be converted from/to [@bitSizeOf(PackedUser)/8]u8; // asBytes/bytesAsValue use @sizeOf, which is larger. Now we are putting no // more than 1, but it probably could be higher. try testing.expect(@sizeOf(PackedUser) - bytes <= 1); } 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; } fn testShell(index: u6) []const u8 { return switch (index) { 0 => "/bin/bash", 1 => "/bin/zsh", else => unreachable, }; } test "construct PackedUser blob" { var buf = ArrayList(u8).init(testing.allocator); defer buf.deinit(); var writer = UserWriter.init(&buf, testShellIndex); const users = [_]User{ User{ .uid = 1000, .gid = 1000, .name = "vidmantas", .gecos = "Vidmantas Kaminskas", .home = "/home/vidmantas", .shell = "/bin/bash", }, User{ .uid = 1001, .gid = 1001, .name = "svc-foo", .gecos = "Service Account", .home = "/home/service1", .shell = "/usr/bin/nologin", }, User{ .uid = 0, .gid = 4294967295, .name = "n" ** 32, .gecos = "g" ** 255, .home = "h" ** 64, .shell = "s" ** 64, } }; for (users) |user| { try writer.appendUser(user); } var rd = UserReader.init(buf.items); { var it = rd.packedIterator(); var i: u32 = 0; while (it.next()) |entry| : (i += 1) { try testing.expectEqual(users[i].uid, entry.packed_user.uid); try testing.expectEqual(users[i].gid, entry.packed_user.gid); } } { var it = rd.iterator(testShell); var i: u32 = 0; while (it.next()) |user| : (i += 1) { try testing.expectEqual(users[i].uid, user.uid); try testing.expectEqual(users[i].gid, user.gid); try testing.expectEqualStrings(users[i].name, user.name); try testing.expectEqualStrings(users[i].gecos, user.gecos); try testing.expectEqualStrings(users[i].home, user.home); try testing.expectEqualStrings(users[i].shell, user.shell); } } }