const std = @import("std"); const fs = std.fs; const io = std.io; const os = std.os; const fmt = std.fmt; const mem = std.mem; const heap = std.heap; const math = std.math; const meta = std.meta; const ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; const BoundedArray = std.BoundedArray; pub const std_options = struct { pub const keep_sigpipe = true; }; const flags = @import("flags.zig"); const compress = @import("compress.zig"); const DB = @import("DB.zig"); const File = @import("File.zig"); const PackedUser = @import("PackedUser.zig"); const PackedGroup = @import("PackedGroup.zig"); const Header = @import("header.zig").Header; const HeaderHost = @import("header.zig").Host; const section_length_bits = @import("header.zig").section_length_bits; const usage = \\usage: turbonss-analyze [OPTION]... \\ \\Arguments: \\ Path to the database (default: /etc/turbonss/db.turbo) \\ \\Options: \\ -h Print this help message and exit \\ ; const Info = struct { fname: []const u8, size_file: []const u8, version: meta.fieldInfo(Header, .version).type, endian: []const u8, ptr_size: meta.fieldInfo(HeaderHost, .ptr_size).type, getgr_bufsize: meta.fieldInfo(Header, .getgr_bufsize).type, getpw_bufsize: meta.fieldInfo(Header, .getpw_bufsize).type, users: meta.fieldInfo(Header, .num_users).type, groups: meta.fieldInfo(Header, .num_groups).type, shells: meta.fieldInfo(Header, .num_shells).type, }; 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 }, }) catch { stderr.writeAll(usage) catch return 3; return 1; }; if (myflags.boolFlag("-h")) { stdout.writeAll(usage) catch return 3; return 0; } const db_file = switch (myflags.args.len) { 0 => "/etc/turbonss/db.turbo", 1 => mem.span(myflags.args[0]), else => { stderr.print("ERROR: too many arguments\n", .{}) catch return 3; stderr.writeAll(usage) catch return 3; return 1; }, }; const file_size_bytes = blk: { const fd = os.open(db_file, os.O.RDONLY, 0) catch |err| { stderr.print("ERROR: failed to open '{s}': {s}\n", .{ db_file, @errorName(err), }) catch {}; return 1; }; defer os.close(fd); const stat = os.fstat(fd) catch |err| { stderr.print("ERROR: fstat '{s}': {s}\n", .{ db_file, @errorName(err), }) catch {}; return 1; }; break :blk stat.size; }; 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 1; }; defer file.close(); const db = file.db; const info = Info{ .fname = db_file, .size_file = splitInt(@intCast(u64, file_size_bytes)).constSlice(), .version = db.header.version, .endian = @tagName(db.header.host.endian), .ptr_size = db.header.host.ptr_size, .getgr_bufsize = db.header.getgr_bufsize, .getpw_bufsize = db.header.getpw_bufsize, .users = db.header.num_users, .groups = db.header.num_groups, .shells = db.header.num_shells, }; const template = \\File: {[fname]s} \\Size: {[size_file]s} bytes \\Version: {[version]d} \\Endian: {[endian]s} \\Pointer size: {[ptr_size]} bytes \\getgr buffer size: {[getgr_bufsize]d} \\getpw buffer size: {[getpw_bufsize]d} \\Users: {[users]d} \\Groups: {[groups]d} \\Shells: {[shells]d} \\ ; stdout.print(template, info) catch return 3; // Popularity contest: // - user with most groups // - TODO: group with most users. Not trivial, because // group memberships do not include users whose primary // gid is the target one. if (db.header.num_users > 0) { const Name = BoundedArray(u8, 32); const Popular = struct { name: Name = Name.init(0) catch unreachable, score: u64 = 0, }; var popUser = Popular{}; var it = PackedUser.iterator( db.users, db.header.num_users, db.shellReader(), ); while (it.next()) |packed_user| { const offset = packed_user.additional_gids_offset; const additional_gids = db.additional_gids[offset..]; const vit = compress.varintSliceIteratorMust(additional_gids); // the primary gid of the user is never in "additional gids" const ngroups = vit.remaining + 1; if (ngroups > popUser.score) { const name = packed_user.name(); popUser.name = Name.fromSlice(name) catch unreachable; popUser.score = ngroups; } } stdout.print("Most memberships: {s} ({d})\n", .{ popUser.name.constSlice(), popUser.score, }) catch return 3; } var lengths = DB.fieldLengths(db.header); var offsets = DB.fieldOffsets(lengths); stdout.writeAll( \\Sections: \\ Name Begin End Size bytes \\ ) catch return 3; inline for (meta.fields(DB.DBNumbers)) |field| { const length = @field(lengths, field.name); const start = @field(offsets, field.name); const end = start + @field(lengths, field.name); stdout.print(" {s:<21}{x:0>8} {x:0>8} {s:>14}\n", .{ field.name, start << section_length_bits, end << section_length_bits, splitInt(length << section_length_bits).constSlice(), }) catch return 3; } return 0; } const testing = std.testing; test "turbonss-analyze trivial error: db file" { const args = &[_][*:0]const u8{"/does/not/exist.turbo"}; const allocator = testing.allocator; var stderr = ArrayList(u8).init(allocator); defer stderr.deinit(); var stdout = ArrayList(u8).init(allocator); defer stdout.deinit(); const exit_code = execute(stdout.writer(), stderr.writer(), args[0..]); try testing.expectEqual(@as(u8, 1), exit_code); try testing.expectEqualStrings( stderr.items, "ERROR: failed to open '/does/not/exist.turbo': FileNotFound\n", ); } const max_len = fmt.count("{d}", .{math.maxInt(u64)}) * 2; fn splitInt(n: u64) BoundedArray(u8, max_len) { var result = BoundedArray(u8, max_len).init(0) catch unreachable; var buf: [max_len]u8 = undefined; const str = fmt.bufPrint(buf[0..], "{d}", .{n}) catch unreachable; for (str, str.len..0) |c, remaining| { result.append(c) catch unreachable; if (remaining > 0 and remaining % 3 == 0) result.append(',') catch unreachable; } return result; } test "turbonss-analyze separators" { const tests = [_]struct { input: u64, want: []const u8, }{ .{ .input = 0, .want = "0" }, .{ .input = 999, .want = "999" }, .{ .input = 1000, .want = "1,000" }, .{ .input = 9999, .want = "9,999" }, .{ .input = 19999, .want = "19,999" }, .{ .input = 19999, .want = "19,999" }, .{ .input = 18446744073709551615, .want = "18,446,744,073,709,551,615" }, }; for (tests) |tt| { const got = splitInt(tt.input); try testing.expectEqualStrings(tt.want, got.constSlice()); } }