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 ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; const flags = @import("flags.zig"); const User = @import("User.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-unix2db [OPTION]... \\ \\Options: \\ -h Print this help message and exit \\ --passwd Path to passwd file (default: passwd) \\ --group Path to group file (default: group) \\ --output Path to output file (default: db.turbo) \\ ; pub fn main() !void { const gpa = heap.raw_c_allocator; const stderr = io.getStdErr().writer(); const stdout = io.getStdOut().writer(); const return_code = execute(gpa, stdout, stderr, os.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 = "--output", .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 out_fname = result.argFlag("--output") orelse "db.turbo"; // 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 passwd_buf = io.bufferedReader(passwd_file.reader()); var passwd_reader = passwd_buf.reader(); var users = User.fromReader(allocator, &errc, passwd_reader) catch |err| return fail(errc.wrap("read users"), stderr, err); defer { for (users) |*user| user.deinit(allocator); allocator.free(users); } var group_buf = io.bufferedReader(group_file.reader()); var group_reader = group_buf.reader(); var groups = Group.fromReader(allocator, group_reader) catch |err| return fail(errc.wrap("read groups"), stderr, err); defer { for (groups) |*group| group.deinit(allocator); allocator.free(groups); } var corpus = Corpus.init(allocator, users, groups, &errc) catch |err| return fail(errc.wrap("init corpus"), stderr, err); defer corpus.deinit(); var db = DB.fromCorpus(allocator, &corpus, &errc) catch |err| return fail(errc.wrap("construct DB from corpus"), stderr, err); defer db.deinit(allocator); const fd = os.open(out_fname, os.O.WRONLY | os.O.TRUNC | os.O.CREAT, 0o644) catch |err| return fail(errc.wrapf("open for writing {s}", .{out_fname}), stderr, err); errdefer os.close(fd); const len = os.writev(fd, db.iov().constSlice()) catch |err| return fail(errc.wrapf("writev to {s}", .{out_fname}), stderr, err); os.fsync(fd) catch |err| return fail(errc.wrapf("fsync {s}", .{out_fname}), stderr, err); os.close(fd); stderr.print("total {d} bytes. groups={d} users={d}\n", .{ len, users.len, groups.len, }) catch return 1; return 0; } 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; } const testing = std.testing; test "turbonss-unix2db 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-unix2db 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-unix2db 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(.{}); defer 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 outPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "tmp.turbo" }); defer allocator.free(outPath); 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, "--output", outPath, }; 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); }