const std = @import("std"); const io = std.io; const os = std.os; const mem = std.mem; const fmt = std.fmt; const ArrayList = std.ArrayList; const compress = @import("compress.zig"); const flags = @import("flags.zig"); const DB = @import("DB.zig"); const File = @import("File.zig"); const PackedUser = @import("PackedUser.zig"); const PackedGroup = @import("PackedGroup.zig"); const User = @import("User.zig"); pub const std_options = struct { pub const keep_sigpipe = true; }; const Mode = enum { group, passwd }; const usage = \\usage: turbonss-getent [OPTION]... group|passwd [key...] \\ \\ -h Print this help message and exit \\ --db PATH Path to the database (default: /etc/turbonss/db.turbo) \\ \\turbonss-getent resolves group or passwd (a.k.a. user) entries from the \\database file. If one or more key arguments are provided, then only \\entries that match the supplied keys will be displayed. If no key is \\provided, all entries will be displayed. \\ \\Exit codes: \\ \\ 0 command completed successfully \\ 1 invalid arguments or db could not be opened \\ 2 one or more supplied key could not be found in the database \\ 3 output error \\ ; pub fn main() !void { const stderr = io.getStdErr().writer(); const stdout = io.getStdOut().writer(); const return_code = execute(stdout, stderr, os.argv[1..]); os.exit(return_code); } fn execute( stdout: anytype, stderr: anytype, argv: []const [*:0]const u8, ) u8 { const myflags = flags.parse(argv, &[_]flags.Flag{ .{ .name = "-h", .kind = .boolean }, .{ .name = "--db", .kind = .arg }, }) catch { stderr.writeAll(usage) catch {}; return 1; }; if (myflags.boolFlag("-h")) { stdout.writeAll(usage) catch return 1; return 0; } if (myflags.args.len == 0) { stderr.writeAll(usage) catch {}; return 1; } const arg0 = mem.span(myflags.args[0]); const mode: Mode = blk: { if (mem.eql(u8, arg0, "passwd")) { break :blk .passwd; } else if (mem.eql(u8, arg0, "group")) { break :blk .group; } else { stderr.print("bad argument {s}: expected passwd or group\n", .{ myflags.args[0], }) catch return 3; return 1; } }; const db_file = myflags.argFlag("--db") orelse "/etc/turbonss/db.turbo"; var file = File.open(db_file) catch |err| { stderr.print( "ERROR {s}: file '{s}' is corrupted or cannot be read\n", .{ @errorName(err), db_file }, ) catch return 3; return 1; }; defer file.close(); return switch (mode) { .passwd => passwd(stdout, &file.db, myflags.args[1..]), .group => group(stdout, &file.db, myflags.args[1..]), }; } fn passwd(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) u8 { if (keys.len == 0) return passwdAll(stdout, db); var some_notfound = false; const shell_reader = db.shellReader(); for (keys) |key| { const keyZ = mem.span(key); const maybe_packed_user = if (fmt.parseUnsigned(u32, keyZ, 10)) |uid| db.getUserByUid(uid) else |_| db.getUserByName(keyZ); const packed_user = maybe_packed_user orelse { some_notfound = true; continue; }; const line = packed_user.toUser(shell_reader).toLine(); stdout.writeAll(line.constSlice()) catch return 3; } return if (some_notfound) 2 else 0; } fn passwdAll(stdout: anytype, db: *const DB) u8 { const shell_reader = db.shellReader(); var it = PackedUser.iterator(db.users, db.header.num_users, shell_reader); while (it.next()) |packed_user| { const line = packed_user.toUser(db.shellReader()).toLine(); stdout.writeAll(line.constSlice()) catch return 3; } return 0; } fn group(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) u8 { if (keys.len == 0) return groupAll(stdout, db); var some_notfound = false; for (keys) |key| { const keyZ = mem.span(key); const maybe_packed_group = if (fmt.parseUnsigned(u32, keyZ, 10)) |gid| db.getGroupByGid(gid) else |_| db.getGroupByName(keyZ); const g = maybe_packed_group orelse { some_notfound = true; continue; }; if (printGroup(stdout, db, &g)) |exit_code| return exit_code; } return if (some_notfound) 2 else 0; } fn groupAll(stdout: anytype, db: *const DB) u8 { var it = PackedGroup.iterator(db.groups, db.header.num_groups); while (it.next()) |g| if (printGroup(stdout, db, &g)) |exit_code| return exit_code; return 0; } fn printGroup(stdout: anytype, db: *const DB, g: *const PackedGroup) ?u8 { // not converting to Group to save a few memory allocations. stdout.print("{s}:x:{d}:", .{ g.name(), g.gid() }) catch return 3; // TODO: move member iteration from here and DB.packCGroup // to a common place. const members_slice = db.groupmembers[g.members_offset..]; var vit = compress.varintSliceIteratorMust(members_slice); var it = compress.deltaDecompressionIterator(&vit); // lines will be buffered, but flushed on every EOL. var line_writer = io.bufferedWriter(stdout); var i: usize = 0; while (it.nextMust()) |member_offset| : (i += 1) { const puser = PackedUser.fromBytes(@alignCast(8, db.users[member_offset << 3 ..])); const name = puser.user.name(); if (i != 0) _ = line_writer.write(",") catch return 3; _ = line_writer.write(name) catch return 3; } _ = line_writer.write("\n") catch return 3; line_writer.flush() catch return 3; return null; } const testing = std.testing; test "turbonss-getent passwd" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); var stdout = ArrayList(u8).init(testing.allocator); defer stdout.deinit(); var stderr = ArrayList(u8).init(testing.allocator); defer stderr.deinit(); const args = &[_][*:0]const u8{ "--db", tf.path, "passwd", "root", "doesnotexist", "vidmantas", "0", "1", }; const got = execute(stdout.writer(), stderr.writer(), args); try testing.expectEqual(got, 2); const want_root = "root:x:0:0::/root:/bin/bash\n"; try testing.expectEqualStrings(stdout.items[0..want_root.len], want_root); const want_vidmantas = "vidmantas:x:128:128:Vidmantas Kaminskas:/home/vidmantas:/bin/bash\n"; var offset: usize = want_root.len + want_vidmantas.len; try testing.expectEqualStrings(stdout.items[want_root.len..offset], want_vidmantas); try testing.expectEqualStrings(stdout.items[offset..], want_root); } test "turbonss-getent passwdAll" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); var stdout = ArrayList(u8).init(testing.allocator); defer stdout.deinit(); var stderr = ArrayList(u8).init(testing.allocator); defer stderr.deinit(); const args = &[_][*:0]const u8{ "--db", tf.path, "passwd", }; var buf: [User.max_line_len]u8 = undefined; const got = execute(stdout.writer(), stderr.writer(), args); try testing.expectEqual(got, 0); const want_names = &[_][]const u8{ "Name" ** 8, "nobody", "root", "svc-bar", "vidmantas", }; var i: usize = 0; var buf_stream = io.fixedBufferStream(stdout.items); var reader = buf_stream.reader(); while (try reader.readUntilDelimiterOrEof(buf[0..], '\n')) |line| { var name = mem.split(u8, line, ":"); try testing.expectEqualStrings(want_names[i], name.next().?); i += 1; } } test "turbonss-getent group" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); var stdout = ArrayList(u8).init(testing.allocator); defer stdout.deinit(); var stderr = ArrayList(u8).init(testing.allocator); defer stderr.deinit(); { const args = &[_][*:0]const u8{ "--db", tf.path, "group", "root", "doesnotexist", "service-group", "0", "1", }; const got = execute(stdout.writer(), stderr.writer(), args); try testing.expectEqual(got, 2); const want = \\root:x:0: \\service-group:x:100000:root,vidmantas \\root:x:0: \\ ; try testing.expectEqualStrings(want, stdout.items); } } test "turbonss-getent groupAll" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); var stdout = ArrayList(u8).init(testing.allocator); defer stdout.deinit(); var stderr = ArrayList(u8).init(testing.allocator); defer stderr.deinit(); { const args = &[_][*:0]const u8{ "--db", tf.path, "group", }; const got = execute(stdout.writer(), stderr.writer(), args); try testing.expectEqual(got, 0); const want = \\root:x:0: \\vidmantas:x:128: \\all:x:9999:NameNameNameNameNameNameNameName,root,svc-bar,vidmantas \\service-group:x:100000:root,vidmantas \\ ; try testing.expectEqualStrings(want, stdout.items); } }