diff --git a/src/user.zig b/src/user.zig index a98c875..fcaa4f4 100644 --- a/src/user.zig +++ b/src/user.zig @@ -20,243 +20,253 @@ pub const User = struct { shell: []const u8, }; -pub const PackedUser = struct { - const AlignmentBits = 3; - const shellIndexFn = fn ([]const u8) ?u6; +pub const PackedUserMut = packedUser(false); +pub const PackedUserConst = packedUser(true); - const InnerSize = @divExact(@bitSizeOf(Inner), 8); - const Inner = 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, +fn packedUser(immutable: bool) type { + return struct { + const Self = @This(); - fn homeLen(self: *const Inner) usize { - return @as(u32, self.home_len) + 1; - } + const AlignmentBits = 3; + const shellIndexFn = fn ([]const u8) ?u6; - fn nameStart(self: *const Inner) usize { - const name_len = self.nameLen(); - if (self.name_is_a_suffix) { - return self.homeLen() - name_len; - } else { - return self.homeLen(); + const InnerSize = @divExact(@bitSizeOf(Inner), 8); + const Inner = 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, + + fn homeLen(self: *const Inner) usize { + return @as(u32, self.home_len) + 1; } - } - fn nameLen(self: *const Inner) usize { - return @as(u32, self.name_len) + 1; - } - - fn gecosStart(self: *const Inner) usize { - if (self.name_is_a_suffix) { - return self.homeLen(); - } else { - return self.homeLen() + self.nameLen(); + fn nameStart(self: *const Inner) usize { + const name_len = self.nameLen(); + if (self.name_is_a_suffix) { + return self.homeLen() - name_len; + } else { + return self.homeLen(); + } } - } - fn gecosLen(self: *const Inner) usize { - return self.gecos_len; - } - - fn maybeShellStart(self: *const Inner) usize { - assert(self.shell_here); - return self.gecosStart() + self.gecosLen(); - } - - fn shellLen(self: *const Inner) usize { - return @as(u32, self.shell_len_or_idx) + 1; - } - - // blobLength returns the length of the blob storing string values. - fn blobLength(self: *const Inner) usize { - var result: usize = self.homeLen() + self.gecosLen(); - if (!self.name_is_a_suffix) { - result += self.nameLen(); + fn nameLen(self: *const Inner) usize { + return @as(u32, self.name_len) + 1; } - if (self.shell_here) { - result += self.shellLen(); + + fn gecosStart(self: *const Inner) usize { + if (self.name_is_a_suffix) { + return self.homeLen(); + } else { + return self.homeLen() + self.nameLen(); + } } - return result; - } - }; - // PackedUser does not allocate; it re-interprets the "bytes" blob field. - // Both of those fields are pointers to "our representation" of that field. - inner: *const Inner, - userdata: []const u8, + fn gecosLen(self: *const Inner) usize { + return self.gecos_len; + } - pub const Entry = struct { - user: PackedUser, - next: ?[]const u8, - }; + fn maybeShellStart(self: *const Inner) usize { + assert(self.shell_here); + return self.gecosStart() + self.gecosLen(); + } - pub fn fromBytes(bytes: []const u8) Entry { - const inner = std.mem.bytesAsValue( - Inner, - // Should use InnerSize instead of sizeOf, see - // https://github.com/ziglang/zig/issues/10958 - bytes[0..@sizeOf(Inner)], - ); + fn shellLen(self: *const Inner) usize { + return @as(u32, self.shell_len_or_idx) + 1; + } - const startBlob = InnerSize; - const endBlob = startBlob + inner.blobLength(); - - const nextStart = pad.roundUp(usize, AlignmentBits, endBlob); - var next: ?[]const u8 = null; - if (nextStart < bytes.len) { - next = bytes[nextStart..]; - } - - return Entry{ - .user = PackedUser{ - .inner = inner, - .userdata = bytes[startBlob..endBlob], - }, - .next = next, + // blobLength returns the length of the blob storing string values. + fn blobLength(self: *const Inner) usize { + var result: usize = self.homeLen() + self.gecosLen(); + if (!self.name_is_a_suffix) { + result += self.nameLen(); + } + if (self.shell_here) { + result += self.shellLen(); + } + return result; + } }; - } - pub const errInvalid = error{InvalidRecord}; - fn downCast(comptime T: type, n: u64) errInvalid!T { - return std.math.cast(T, n) catch |err| switch (err) { - error.Overflow => { + const Bytes = if (immutable) []const u8 else []u8; + const InnerPointer = if (immutable) *const Inner else *Inner; + + // PackedUser does not allocate; it re-interprets the "bytes" blob field. + // Both of those fields are pointers to "our representation" of that field. + inner: InnerPointer, + userdata: Bytes, + + pub const Entry = struct { + user: Self, + next: ?Bytes, + }; + + pub fn fromBytes(bytes: Bytes) Entry { + const inner = std.mem.bytesAsValue( + Inner, + // Should use InnerSize instead of sizeOf, see + // https://github.com/ziglang/zig/issues/10958 + bytes[0..@sizeOf(Inner)], + ); + + const startBlob = InnerSize; + const endBlob = startBlob + inner.blobLength(); + + const nextStart = pad.roundUp(usize, AlignmentBits, endBlob); + var next: ?Bytes = null; + if (nextStart < bytes.len) { + next = bytes[nextStart..]; + } + + return Entry{ + .user = Self{ + .inner = inner, + .userdata = bytes[startBlob..endBlob], + }, + .next = next, + }; + } + + pub const errInvalid = error{InvalidRecord}; + fn downCast(comptime T: type, n: u64) errInvalid!T { + return std.math.cast(T, n) catch |err| switch (err) { + error.Overflow => { + return error.InvalidRecord; + }, + }; + } + + fn validateUtf8(s: []const u8) errInvalid!void { + if (!std.unicode.utf8ValidateSlice(s)) { return error.InvalidRecord; - }, + } + } + + // packTo packs the User record and copies it to the given byte slice. The + // slice must have at least maxRecordSize() bytes available. + // The slice is passed as a pointer, so it can be mutated. + const packErr = errInvalid || Allocator.Error; + pub fn packTo( + arr: *ArrayList(u8), + user: User, + idxFn: shellIndexFn, + ) packErr!void { + // function arguments are consts. We need to mutate the underlying + // slice, so passing it via pointer instead. + 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); + + try validateUtf8(user.home); + try validateUtf8(user.name); + try validateUtf8(user.shell); + try validateUtf8(user.gecos); + + const inner = Inner{ + .uid = user.uid, + .gid = user.gid, + .additional_gids_offset = std.math.maxInt(u29), + .shell_here = idxFn(user.shell) == null, + .shell_len_or_idx = idxFn(user.shell) orelse shell_len, + .home_len = home_len, + .name_is_a_suffix = mem.endsWith(u8, user.home, user.name), + .name_len = name_len, + .gecos_len = gecos_len, + }; + const innerBytes = mem.asBytes(&inner); + + // innerBytes.len is longer than InnerSize. We want to copy + // only the InnerSize-number of bytes. + try arr.*.appendSlice(innerBytes[0..InnerSize]); + try arr.*.appendSlice(user.home); + + if (!inner.name_is_a_suffix) { + try arr.*.appendSlice(user.name); + } + try arr.*.appendSlice(user.gecos); + + if (inner.shell_here) { + try arr.*.appendSlice(user.shell); + } + + try pad.arrayList(arr, AlignmentBits); + } + + // maxSize is the maximum number of records a PackedUser can take + // (struct + userdata). + pub fn maxSize() usize { + comptime { + const unpadded = InnerSize + + std.math.maxInt(u6) + 1 + // home + std.math.maxInt(u5) + 1 + // name + std.math.maxInt(u6) + 1 + // shell + std.math.maxInt(u8); // gecos + return pad.roundUp(u64, AlignmentBits, unpadded); + } + } + + pub fn uid(self: Self) u32 { + return self.inner.uid; + } + + pub fn gid(self: Self) u32 { + return self.inner.gid; + } + + pub fn home(self: Self) []const u8 { + return self.userdata[0..self.inner.homeLen()]; + } + + pub fn name(self: Self) []const u8 { + const name_pos = self.inner.nameStart(); + const name_len = self.inner.nameLen(); + return self.userdata[name_pos .. name_pos + name_len]; + } + + pub fn gecos(self: Self) []const u8 { + const gecos_pos = self.inner.gecosStart(); + const gecos_len = self.inner.gecosLen(); + return self.userdata[gecos_pos .. gecos_pos + gecos_len]; + } + + pub fn shell(self: Self, idxFn: ShellIndexFn) []const u8 { + if (self.inner.shell_here) { + const shell_pos = self.inner.maybeShellStart(); + const shell_len = self.inner.shellLen(); + return self.userdata[shell_pos .. shell_pos + shell_len]; + } + return idxFn(self.inner.shell_len_or_idx); + } + + pub const Iterator = struct { + section: ?Bytes, + shellIndex: ShellIndexFn, + + pub fn next(it: *Iterator) ?Self { + if (it.section) |section| { + const entry = Self.fromBytes(section); + it.section = entry.next; + return entry.user; + } + return null; + } }; - } - fn validateUtf8(s: []const u8) errInvalid!void { - if (!std.unicode.utf8ValidateSlice(s)) { - return error.InvalidRecord; + pub fn iterator(section: Bytes, idxFn: ShellIndexFn) Iterator { + return Iterator{ + .section = section, + .shellIndex = idxFn, + }; } - } - - // packTo packs the User record and copies it to the given byte slice. The - // slice must have at least maxRecordSize() bytes available. - // The slice is passed as a pointer, so it can be mutated. - const packErr = errInvalid || Allocator.Error; - pub fn packTo( - arr: *ArrayList(u8), - user: User, - idxFn: shellIndexFn, - ) packErr!void { - // function arguments are consts. We need to mutate the underlying - // slice, so passing it via pointer instead. - 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); - - try validateUtf8(user.home); - try validateUtf8(user.name); - try validateUtf8(user.shell); - try validateUtf8(user.gecos); - - const inner = Inner{ - .uid = user.uid, - .gid = user.gid, - .additional_gids_offset = std.math.maxInt(u29), - .shell_here = idxFn(user.shell) == null, - .shell_len_or_idx = idxFn(user.shell) orelse shell_len, - .home_len = home_len, - .name_is_a_suffix = mem.endsWith(u8, user.home, user.name), - .name_len = name_len, - .gecos_len = gecos_len, - }; - const innerBytes = mem.asBytes(&inner); - - // innerBytes.len is longer than InnerSize. We want to copy - // only the InnerSize-number of bytes. - try arr.*.appendSlice(innerBytes[0..InnerSize]); - try arr.*.appendSlice(user.home); - - if (!inner.name_is_a_suffix) { - try arr.*.appendSlice(user.name); - } - try arr.*.appendSlice(user.gecos); - - if (inner.shell_here) { - try arr.*.appendSlice(user.shell); - } - - try pad.arrayList(arr, AlignmentBits); - } - - // maxSize is the maximum number of records a PackedUser can take - // (struct + userdata). - pub fn maxSize() usize { - comptime { - const unpadded = InnerSize + - std.math.maxInt(u6) + 1 + // home - std.math.maxInt(u5) + 1 + // name - std.math.maxInt(u6) + 1 + // shell - std.math.maxInt(u8); // gecos - return pad.roundUp(u64, AlignmentBits, unpadded); - } - } - - pub fn uid(self: *const PackedUser) u32 { - return self.inner.uid; - } - - pub fn gid(self: *const PackedUser) u32 { - return self.inner.gid; - } - - pub fn home(self: *const PackedUser) []const u8 { - return self.userdata[0..self.inner.homeLen()]; - } - - pub fn name(self: *const PackedUser) []const u8 { - const name_pos = self.inner.nameStart(); - const name_len = self.inner.nameLen(); - return self.userdata[name_pos .. name_pos + name_len]; - } - - pub fn gecos(self: *const PackedUser) []const u8 { - const gecos_pos = self.inner.gecosStart(); - const gecos_len = self.inner.gecosLen(); - return self.userdata[gecos_pos .. gecos_pos + gecos_len]; - } - - pub fn shell(self: *const PackedUser, idxFn: ShellIndexFn) []const u8 { - if (self.inner.shell_here) { - const shell_pos = self.inner.maybeShellStart(); - const shell_len = self.inner.shellLen(); - return self.userdata[shell_pos .. shell_pos + shell_len]; - } - return idxFn(self.inner.shell_len_or_idx); - } -}; - -pub fn userIterator(section: []const u8, idxFn: ShellIndexFn) Iterator { - return Iterator{ - .section = section, - .shellIndex = idxFn, }; } -pub const Iterator = struct { - section: ?[]const u8, - shellIndex: ShellIndexFn, - - pub fn next(it: *Iterator) ?PackedUser { - if (it.section) |section| { - const entry = PackedUser.fromBytes(section); - it.section = entry.next; - return entry.user; - } - return null; - } -}; - const testing = std.testing; const ArrayList = std.ArrayList; @@ -267,7 +277,7 @@ test "PackedUser internal and external alignment" { // 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) * 8 - @bitSizeOf(PackedUser) <= 8); + try testing.expect(@sizeOf(PackedUserMut.Inner) * 8 - @bitSizeOf(PackedUserMut.Inner) <= 8); } fn testShellIndex(shell: []const u8) ?u6 { @@ -321,11 +331,11 @@ test "construct PackedUser section" { .shell = "/", } }; for (users) |user| { - try buf.ensureUnusedCapacity(PackedUser.maxSize()); - try PackedUser.packTo(&buf, user, testShellIndex); + try buf.ensureUnusedCapacity(PackedUserMut.maxSize()); + try PackedUserMut.packTo(&buf, user, testShellIndex); } - var it = userIterator(buf.items, testShell); + var it = PackedUserMut.iterator(buf.items, testShell); var i: u32 = 0; while (it.next()) |user| : (i += 1) { try testing.expectEqual(users[i].uid, user.uid()); @@ -341,7 +351,7 @@ test "construct PackedUser section" { test "PackedUser.maxSize()" { // TODO(motiejus) try using a slice that points to an array in stack. // As of writing, I am getting a stack smashing error. - var buf = try ArrayList(u8).initCapacity(testing.allocator, PackedUser.maxSize()); + var buf = try ArrayList(u8).initCapacity(testing.allocator, PackedUserConst.maxSize()); defer buf.deinit(); const largeUser = User{ @@ -352,6 +362,6 @@ test "PackedUser.maxSize()" { .home = "Home" ** 16, // 64 .shell = "She.LllL" ** 8, // 64 }; - try PackedUser.packTo(&buf, largeUser, testShellIndex); - try testing.expectEqual(PackedUser.maxSize(), buf.items.len); + try PackedUserConst.packTo(&buf, largeUser, testShellIndex); + try testing.expectEqual(PackedUserConst.maxSize(), buf.items.len); }