diff --git a/lib/PackedUser.zig b/lib/PackedUser.zig index 27603b7..2181480 100644 --- a/lib/PackedUser.zig +++ b/lib/PackedUser.zig @@ -237,23 +237,6 @@ const test_shell_reader = shellImport.ShellReader{ .index = &[_]u16{ 0, 9, 17 }, }; -const max_user = User{ - .uid = 0, - .gid = math.maxInt(u32), - .name = "Name" ** 8, - .gecos = "realname" ** 255 ++ "realnam", - .home = "Home" ** 16, - .shell = "She.LllL" ** 32, -}; - -test "max_user and max_str_len are consistent" { - const total_len = max_user.name.len + - max_user.gecos.len + - max_user.home.len + - max_user.shell.len; - try testing.expectEqual(total_len, max_str_len); -} - test "pack max_user" { var arr = ArrayList(u8).init(testing.allocator); defer arr.deinit(); @@ -261,7 +244,7 @@ test "pack max_user" { var idx_noop = StringHashMap(u8).init(testing.allocator); defer idx_noop.deinit(); - try packTo(&arr, max_user, 0, idx_noop); + try packTo(&arr, User.max_user, 0, idx_noop); } test "construct PackedUser section" { @@ -282,7 +265,7 @@ test "construct PackedUser section" { .gecos = "Service Account", .home = "/home/service1", .shell = "/usr/bin/nologin", - }, max_user, User{ + }, User.max_user, User{ .uid = 1002, .gid = 1002, .name = "svc-bar", diff --git a/lib/User.zig b/lib/User.zig index 4152ab6..c290fb2 100644 --- a/lib/User.zig +++ b/lib/User.zig @@ -1,10 +1,14 @@ const std = @import("std"); const mem = std.mem; +const fmt = std.fmt; +const maxInt = std.math.maxInt; const Allocator = mem.Allocator; const ArrayList = std.ArrayList; -const maxInt = std.math.maxInt; +const BoundedArray = std.BoundedArray; const User = @This(); +const PackedUser = @import("PackedUser.zig"); +const validate = @import("validate.zig"); uid: u32, gid: u32, @@ -37,6 +41,61 @@ pub fn clone( }; } +const line_fmt = "{s}:x:{d}:{d}:{s}:{s}:{s}\n"; + +const max_line_len = fmt.count(line_fmt, .{ + max_user.uid, + max_user.gid, + max_user.gecos, + max_user.home, + max_user.shell, +}); + +// toPasswdLine formats the user in /etc/passwd format, including the EOL +fn toLine(self: *const User) BoundedArray(u8, max_line_len) { + var result = BoundedArray(u8, max_line_len); + fmt.bufPrint(result.slice(), line_fmt, .{ + self.uid, + self.gid, + self.gecos, + self.home, + self.shell, + }); + return result; +} + +// fromLine accepts a line of /etc/passwd (with or without the EOL) and makes a +// User. +fn fromLine(allocator: Allocator, line: []const u8) error{ InvalidRecord, OutOfMemory }!User { + var it = mem.split(u8, line, ":"); + const uids = it.next() orelse return error.InvalidRecord; + const gids = it.next() orelse return error.InvalidRecord; + const name = it.next() orelse return error.InvalidRecord; + const gecos = it.next() orelse return error.InvalidRecord; + const home = it.next() orelse return error.InvalidRecord; + const shell = it.next() orelse return error.InvalidRecord; + // the line must be exhaustive. + if (it.next() != null) return error.InvalidRecord; + + const uid = fmt.parseInt(u32, uids) orelse return error.InvalidRecord; + const gid = fmt.parseInt(u32, gids) orelse return error.InvalidRecord; + + try validate.utf8(name); + try validate.utf8(gecos); + try validate.utf8(home); + try validate.utf8(shell); + + const user = User{ + .uid = uid, + .gid = gid, + .name = name, + .gecos = gecos, + .home = home, + .shell = shell, + }; + return try user.clone(allocator); +} + fn strlen(self: *const User) usize { return self.name.len + self.gecos.len + @@ -63,6 +122,23 @@ pub fn fromReader(allocator: Allocator, reader: anytype) ![]User { const testing = std.testing; +pub const max_user = User{ + .uid = maxInt(u32), + .gid = maxInt(u32), + .name = "Name" ** 8, + .gecos = "realname" ** 255 ++ "realnam", + .home = "Home" ** 16, + .shell = "She.LllL" ** 32, +}; + +test "max_user and max_str_len are consistent" { + const total_len = max_user.name.len + + max_user.gecos.len + + max_user.home.len + + max_user.shell.len; + try testing.expectEqual(total_len, PackedUser.max_str_len); +} + test "User.clone" { var allocator = testing.allocator; const user = User{