253 lines
8.0 KiB
Zig
253 lines
8.0 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 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).field_type,
|
|
endian: []const u8,
|
|
ptr_size: meta.fieldInfo(Header, .ptr_size).field_type,
|
|
getgr_bufsize: meta.fieldInfo(Header, .getgr_bufsize).field_type,
|
|
getpw_bufsize: meta.fieldInfo(Header, .getpw_bufsize).field_type,
|
|
users: meta.fieldInfo(Header, .num_users).field_type,
|
|
groups: meta.fieldInfo(Header, .num_groups).field_type,
|
|
shells: meta.fieldInfo(Header, .num_shells).field_type,
|
|
};
|
|
|
|
pub fn main() !void {
|
|
// This line is here because of https://github.com/ziglang/zig/issues/7807
|
|
const argv: []const [*:0]const u8 = os.argv;
|
|
|
|
const stderr = io.getStdErr().writer();
|
|
const stdout = io.getStdOut().writer();
|
|
|
|
const return_code = execute(stdout, stderr, 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.endian),
|
|
.ptr_size = db.header.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());
|
|
}
|
|
}
|