319 lines
9.4 KiB
Zig
319 lines
9.4 KiB
Zig
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);
|
|
}
|
|
}
|