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 BoundedArray = std.BoundedArray; const pw_passwd = "x\x00"; const User = @This(); const ErrCtx = @import("ErrCtx.zig"); const PackedUser = @import("PackedUser.zig"); const validate = @import("validate.zig"); uid: u32, gid: u32, name: []const u8, gecos: []const u8, home: []const u8, shell: []const u8, // deep-clones a User record with a given Allocator. pub fn clone( self: *const User, allocator: Allocator, ) error{OutOfMemory}!User { const name = try allocator.dupe(u8, self.name); errdefer allocator.free(name); const gecos = try allocator.dupe(u8, self.gecos); errdefer allocator.free(gecos); const home = try allocator.dupe(u8, self.home); errdefer allocator.free(home); const shell = try allocator.dupe(u8, self.shell); errdefer allocator.free(shell); return User{ .uid = self.uid, .gid = self.gid, .name = name, .gecos = gecos, .home = home, .shell = shell, }; } // fromLine accepts a line of /etc/passwd (with or without the EOL) and makes a // User. fn fromLine(allocator: Allocator, err: *ErrCtx, line: []const u8) error{ InvalidRecord, OutOfMemory }!User { var it = mem.split(u8, line, ":"); const name = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); _ = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); // password const uids = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); const gids = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); const gecos = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); const home = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); const shell = it.next() orelse return err.returnf("too few fields", .{}, error.InvalidRecord); if (it.next() != null) return err.returnf("too many fields", .{}, error.InvalidRecord); const uid = fmt.parseInt(u32, uids, 10) catch return err.returnf("bad uid: {s}", .{uids}, error.InvalidRecord); const gid = fmt.parseInt(u32, gids, 10) catch return err.returnf("bad gid: {s}", .{gids}, error.InvalidRecord); validate.name(name, err) catch return err.returnf("invalid name '{s}'", .{name}, error.InvalidRecord); validate.gecos(gecos, err) catch return err.returnf("invalid gecos '{s}'", .{gecos}, error.InvalidRecord); validate.path(home, err) catch return err.returnf("invalid home '{s}'", .{home}, error.InvalidRecord); validate.path(shell, err) catch return err.returnf("invalid shell '{s}'", .{shell}, error.InvalidRecord); 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 + self.home.len + self.shell.len; } const line_fmt = "{s}:x:{d}:{d}:{s}:{s}:{s}\n"; pub const max_line_len = fmt.count(line_fmt, .{ max_user.name, max_user.uid, max_user.gid, max_user.gecos, max_user.home, max_user.shell, }); // toLine formats the user in /etc/passwd format, including the EOL pub fn toLine(self: *const User) BoundedArray(u8, max_line_len) { var result = BoundedArray(u8, max_line_len).init(0) catch unreachable; result.writer().print(line_fmt, .{ self.name, self.uid, self.gid, self.gecos, self.home, self.shell, }) catch unreachable; return result; } // length of all string-data fields, assuming they are zero-terminated. // Does not include password, since that's always static 'x\00'. pub fn strlenZ(self: *const User) usize { return self.strlen() + 4; // '\0' of name, gecos, home and shell } pub fn deinit(self: *User, allocator: Allocator) void { allocator.free(self.name); allocator.free(self.gecos); allocator.free(self.home); allocator.free(self.shell); self.* = undefined; } pub fn fromReader(allocator: Allocator, err: *ErrCtx, reader: anytype) ![]User { var users = ArrayList(User).init(allocator); errdefer { for (users.items) |*user| user.deinit(allocator); users.deinit(); } var buf: [max_line_len]u8 = undefined; while (try reader.readUntilDelimiterOrEof(buf[0..], '\n')) |line| { var user = try fromLine(allocator, err, line); try users.append(user); } return users.toOwnedSlice(); } pub const CUser = extern struct { pw_name: [*:0]const u8, pw_passwd: [*:0]const u8 = pw_passwd, pw_uid: u32, pw_gid: u32, pw_gecos: [*:0]const u8, pw_dir: [*:0]const u8, pw_shell: [*:0]const u8, }; 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 "User.fromLine" { const examples = [_]struct { line: []const u8, want: ?User = null, wantErr: []const u8 = &[_]u8{}, }{ .{ .line = "root:x:0:0:root:/root:/bin/bash", .want = User{ .uid = 0, .gid = 0, .name = "root", .gecos = "root", .home = "/root", .shell = "/bin/bash", }, }, .{ .line = "žemas:x:0:0:root:/root:/bin/bash", .wantErr = "invalid name 'žemas': invalid character 0xC5 at position 0", }, .{ .line = "root:x:-1:0:root:/root:/bin/bash", .wantErr = "bad uid: -1", }, .{ .line = "root:x:0:-1:root:/root:/bin/bash", .wantErr = "bad gid: -1", }, .{ .line = "root:x:0:0:root:/root:bin/bash", .wantErr = "invalid shell 'bin/bash': must start with /", }, .{ .line = "root:x:0:0:root:/root:/bin/bash:redundant", .wantErr = "too many fields", }, .{ .line = "", .wantErr = "too few fields", }, .{ .line = "root:x:0:0:root:/root", .wantErr = "too few fields", }, }; const allocator = testing.allocator; for (examples) |tt| { var err = ErrCtx{}; if (tt.want) |want_user| { var got = try fromLine(allocator, &err, tt.line); defer got.deinit(allocator); try testing.expectEqual(want_user.uid, got.uid); try testing.expectEqual(want_user.gid, got.gid); try testing.expectEqualStrings(want_user.name, got.name); try testing.expectEqualStrings(want_user.gecos, got.gecos); try testing.expectEqualStrings(want_user.home, got.home); try testing.expectEqualStrings(want_user.shell, got.shell); continue; } const got = fromLine(allocator, &err, tt.line); try testing.expectError(error.InvalidRecord, got); try testing.expectEqualStrings(tt.wantErr, err.unwrap().constSlice()); } } test "User 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{ .uid = 1000, .gid = 1000, .name = "vidmantas", .gecos = "Vidmantas Kaminskas", .home = "/home/vidmantas", .shell = "/bin/bash", }; var user2 = try user.clone(allocator); defer user2.deinit(allocator); try testing.expectEqualStrings(user.shell, "/bin/bash"); }