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 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 stringdata = try allocator.alloc(u8, self.strlen()); const gecos_start = self.name.len; const home_start = gecos_start + self.gecos.len; const shell_start = home_start + self.home.len; mem.copy(u8, stringdata[0..self.name.len], self.name); mem.copy(u8, stringdata[gecos_start..], self.gecos); mem.copy(u8, stringdata[home_start..], self.home); mem.copy(u8, stringdata[shell_start..], self.shell); return User{ .uid = self.uid, .gid = self.gid, .name = stringdata[0..self.name.len], .gecos = stringdata[gecos_start .. gecos_start + self.gecos.len], .home = stringdata[home_start .. home_start + self.home.len], .shell = stringdata[shell_start .. shell_start + self.shell.len], }; } // 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 name = it.next() orelse return error.InvalidRecord; _ = it.next() orelse return error.InvalidRecord; // password const uids = it.next() orelse return error.InvalidRecord; const gids = 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, 10) catch return error.InvalidRecord; const gid = fmt.parseInt(u32, gids, 10) catch 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 + 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 { const slice = self.home.ptr[0..self.strlen()]; allocator.free(slice); self.* = undefined; } pub fn fromReader(allocator: Allocator, reader: anytype) ![]User { var users = ArrayList(User).init(allocator); var buf: [max_line_len + 1]u8 = undefined; // TODO: catch and interpret error while (try reader.readUntilDelimiterOrEof(buf[0..], '\n')) |line| { const user = try fromLine(allocator, 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, }; 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"); }