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(); _ = it.next(); // password const uids = it.next(); const gids = it.next(); const gecos = it.next(); const home = it.next(); const shell = it.next(); // all fields are set if (shell == null) return err.returnf("too few user fields in line: {s}", .{line}, error.InvalidRecord); // the line must be exhaustive. if (it.next() != null) return error.returnf("too many fields in line", .{}, 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 uid: {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"; 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(max_line_len) catch unreachable; _ = fmt.bufPrint(result.slice(), 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 + 1]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: [*]const u8, pw_passwd: [*]const u8 = pw_passwd, pw_uid: u32, pw_gid: u32, pw_gecos: [*]const u8, pw_dir: [*]const u8, pw_shell: [*]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, }; const from_line_examples = [_]struct { line: []const u8, want: error{InvalidRecord}!User, wantErrStr: []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", .want = error.InvalidRecord, .wantErrStr = "invalid name 'žemas': invalid character at position 0", }, }; test "User.fromLine" { //if (true) return error.SkipZigTest; const allocator = testing.allocator; for (from_line_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); } else |want_err| { try testing.expectError( want_err, fromLine(allocator, &err, tt.line), ); try testing.expectEqualStrings( tt.wantErrStr, err.unwrap().constSlice(), ); } } } 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{ .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"); }