1
Fork 0
turbonss/src/turbonss-analyze.zig

251 lines
7.9 KiB
Zig

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;
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]... <PATH>
\\
\\Arguments:
\\ <PATH> 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;
var remaining: usize = str.len;
for (str) |c| {
result.append(c) catch unreachable;
remaining -= 1;
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());
}
}