const std = @import("std"); const fs = std.fs; const io = std.io; const mem = std.mem; const os = std.os; const heap = std.heap; const math = std.math; const fmt = std.fmt; const json = std.json; const ArrayList = std.ArrayList; const ArrayListUnmanaged = std.ArrayListUnmanaged; const Allocator = std.mem.Allocator; const StringArrayHashMap = std.StringArrayHashMap; const flags = @import("flags.zig"); const User = @import("User.zig"); const PackedUser = @import("PackedUser.zig"); const Group = @import("Group.zig"); const Corpus = @import("Corpus.zig"); const DB = @import("DB.zig"); const ErrCtx = @import("ErrCtx.zig"); const usage = \\usage: turbonss-unix2systemd [OPTION]... \\ \\Options: \\ -h Print this help message and exit \\ --passwd Path to passwd file (default: passwd) \\ --group Path to group file (default: group) \\ --outdir Path to output directory (default: ./userdb) \\ ; pub fn main() !void { // This line is here because of https://github.com/ziglang/zig/issues/7807 const argv: []const [*:0]const u8 = os.argv; const gpa = heap.raw_c_allocator; const stderr = io.getStdErr().writer(); const stdout = io.getStdOut().writer(); const return_code = execute(gpa, stdout, stderr, argv[1..]); os.exit(return_code); } fn execute( allocator: Allocator, stdout: anytype, stderr: anytype, argv: []const [*:0]const u8, ) u8 { const result = flags.parse(argv, &[_]flags.Flag{ .{ .name = "-h", .kind = .boolean }, .{ .name = "--passwd", .kind = .arg }, .{ .name = "--group", .kind = .arg }, .{ .name = "--outdir", .kind = .arg }, }) catch { stderr.writeAll(usage) catch {}; return 1; }; if (result.boolFlag("-h")) { stdout.writeAll(usage) catch return 1; return 0; } if (result.args.len != 0) { stderr.print("ERROR: unknown option '{s}'\n", .{result.args[0]}) catch {}; stderr.writeAll(usage) catch {}; return 1; } const passwd_fname = result.argFlag("--passwd") orelse "passwd"; const group_fname = result.argFlag("--group") orelse "group"; const outdir = result.argFlag("--outdir") orelse "./userdb"; // to catch an error set file.OpenError, wait for // https://github.com/ziglang/zig/issues/2473 var errc = ErrCtx{}; var passwd_file = fs.cwd().openFile(passwd_fname, .{ .mode = .read_only }) catch |err| return fail(errc.wrapf("open '{s}'", .{passwd_fname}), stderr, err); defer passwd_file.close(); var group_file = fs.cwd().openFile(group_fname, .{ .mode = .read_only }) catch |err| return fail(errc.wrapf("open '{s}'", .{group_fname}), stderr, err); defer group_file.close(); var passwdReader = io.bufferedReader(passwd_file.reader()).reader(); var users = User.fromReader(allocator, &errc, passwdReader) catch |err| return fail(errc.wrap("read users"), stderr, err); defer { for (users) |*user| user.deinit(allocator); allocator.free(users); } var groupReader = io.bufferedReader(group_file.reader()).reader(); var groups = Group.fromReader(allocator, groupReader) catch |err| return fail(errc.wrap("read groups"), stderr, err); defer { for (groups) |*group| group.deinit(allocator); allocator.free(groups); } const user2groups = StringArrayHashMap(ArrayListUnmanaged([]const u8)).init(allocator); defer { var it = user2groups.iterator(); while (it.next()) |entry| entry.value_ptr.*.deinit(allocator); user2groups.deinit(); } fillMemberships(allocator, groups, &user2groups); try os.mkdirZ(outdir, 0o755) catch |err| switch (err) { error.PathAlreadyExists => {}, else => |err| return err, }; var dir = try fs.cwd().openDir(outdir, .{}); try makePasswd(dir, users.items); try makeGroups(dir, groups.items); return 0; } const JSONPasswd = struct { uid: u32, gid: u32, userName: []const u8, // pw_name realName: []const u8, // pw_gecos homeDirectory: []const u8, shell: []const u8, memberOf: []const []const u8, }; fn makePasswd( dir: fs.Dir, users: []User, memberships: *const StringArrayHashMap(ArrayListUnmanaged([]const u8)), ) !void { var namebuf: [PackedUser.max_name_len + ".user".len:0]u8 = undefined; var symlinkbuf: [fmt.count("{d}.user", math.maxInt(u32)):0]u8 = undefined; for (users) |user| { const member_of = if (memberships.get(user.name)) |m| m.items else []const []const u8{}; const u = JSONPasswd{ .uid = user.uid, .gid = user.gid, .userName = user.name, .realName = user.gecos, .homeDirectory = user.home, .shell = user.shell, .memberOf = member_of, }; const fname = try fmt.bufPrintZ(namebuf, "{s}.user", user.name); var f = try dir.createFileZ(fname, .{}); defer f.close(); var wr = io.bufferedWriter(f.writer()); try json.stringify(u, .{}, wr); try wr.flush(); const symlinkname = try fmt.bufPrintZ(symlinkbuf, "{d}.user", user.uid); try os.symlinkatZ(fname, dir.fd, symlinkname); } } fn makeGroups(dir: fs.Dir, groups: []Group) !void { _ = dir; _ = groups; } fn fail(errc: *ErrCtx, stderr: anytype, err: anytype) u8 { const err_chain = errc.unwrap().constSlice(); stderr.print("ERROR {s}: {s}\n", .{ @errorName(err), err_chain }) catch {}; return 1; } fn fillMemberships( allocator: Allocator, groups: ArrayList(Group), user2groups: *StringArrayHashMap(ArrayListUnmanaged([]const u8)), ) void { for (groups) |group| { for (group.members) |member| { const member_groups = try user2groups.getOrPut(allocator, member.name); if (!member_groups.found_existing) member_groups.value_ptr.* = ArrayListUnmanaged([]const u8){}; member_groups.value_ptr.*.append(group.name); } } } const testing = std.testing; test "turbonss-unix2systemd invalid argument" { const allocator = testing.allocator; const args = &[_][*:0]const u8{"--invalid-argument"}; var stderr = ArrayList(u8).init(allocator); defer stderr.deinit(); var stdout = ArrayList(u8).init(allocator); defer stdout.deinit(); const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]); try testing.expectEqual(@as(u8, 1), exit_code); try testing.expect(mem.startsWith( u8, stderr.items, "ERROR: unknown option '--invalid-argument'", )); } test "turbonss-unix2systemd trivial error: missing passwd file" { const allocator = testing.allocator; const args = &[_][*:0]const u8{ "--passwd", "/does/not/exist/passwd", "--group", "/does/not/exist/group", }; var stderr = ArrayList(u8).init(allocator); defer stderr.deinit(); var stdout = ArrayList(u8).init(allocator); defer stdout.deinit(); const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]); try testing.expectEqual(@as(u8, 1), exit_code); try testing.expectEqualStrings(stderr.items, "ERROR FileNotFound: open '/does/not/exist/passwd'\n"); } test "turbonss-unix2systemd fail" { var errc = ErrCtx{}; var buf = ArrayList(u8).init(testing.allocator); defer buf.deinit(); var wr = buf.writer(); const exit_code = fail(errc.wrapf("invalid user 'foo'", .{}), wr, error.NotSure); try testing.expectEqual(exit_code, 1); try testing.expectEqualStrings(buf.items, "ERROR NotSure: invalid user 'foo'\n"); } test "turbonss-unix2db smoke test" { const allocator = testing.allocator; var stderr = ArrayList(u8).init(allocator); defer stderr.deinit(); var stdout = ArrayList(u8).init(allocator); defer stdout.deinit(); var corpus = try Corpus.testCorpus(allocator); defer corpus.deinit(); var tmp = testing.tmpDir(.{}); // TODO: defer errdefer tmp.cleanup(); const tmp_path = blk: { const relative_path = try fs.path.join(allocator, &[_][]const u8{ "zig-cache", "tmp", tmp.sub_path[0..], }); const real_path = try fs.realpathAlloc(allocator, relative_path); allocator.free(relative_path); break :blk real_path; }; defer allocator.free(tmp_path); const passwdPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "passwd" }); defer allocator.free(passwdPath); const groupPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "group" }); defer allocator.free(groupPath); const outDir = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "outdir" }); defer allocator.free(outDir); const passwd_fd = try os.open(passwdPath, os.O.CREAT | os.O.WRONLY, 0o644); const group_fd = try os.open(groupPath, os.O.CREAT | os.O.WRONLY, 0o644); var i: usize = 0; while (i < corpus.users.len) : (i += 1) { const user = corpus.users.get(i); const line = user.toLine().constSlice(); _ = try os.write(passwd_fd, line); } os.close(passwd_fd); var group_writer = (fs.File{ .handle = group_fd }).writer(); i = 0; while (i < corpus.groups.len) : (i += 1) try corpus.groups.get(i).writeTo(group_writer); os.close(group_fd); const args = &[_][*:0]const u8{ "--passwd", passwdPath, "--group", groupPath, "--outdir", outDir, }; const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args); try testing.expectEqualStrings("total 1664 bytes. groups=5 users=4\n", stderr.items); try testing.expectEqual(@as(u8, 0), exit_code); }