From 8bfc4a30cddc88306dde06ba675241e35a671cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= Date: Sat, 20 Aug 2022 19:08:04 +0300 Subject: [PATCH] wip turbonss-unix2systemd --- src/turbonss-unix2systemd.zig | 305 ++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 src/turbonss-unix2systemd.zig diff --git a/src/turbonss-unix2systemd.zig b/src/turbonss-unix2systemd.zig new file mode 100644 index 0000000..bb9aded --- /dev/null +++ b/src/turbonss-unix2systemd.zig @@ -0,0 +1,305 @@ +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); +}