wip turbonss-unix2systemd
This commit is contained in:
parent
b350e8d096
commit
8bfc4a30cd
305
src/turbonss-unix2systemd.zig
Normal file
305
src/turbonss-unix2systemd.zig
Normal file
@ -0,0 +1,305 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const io = std.io;
|
||||
const mem = std.mem;
|
||||
const os = std.os;
|
||||
const heap = std.heap;
|
||||
const math = std.math;
|
||||
const fmt = std.fmt;
|
||||
const json = std.json;
|
||||
const ArrayList = std.ArrayList;
|
||||
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const StringArrayHashMap = std.StringArrayHashMap;
|
||||
|
||||
const flags = @import("flags.zig");
|
||||
const User = @import("User.zig");
|
||||
const PackedUser = @import("PackedUser.zig");
|
||||
const Group = @import("Group.zig");
|
||||
const Corpus = @import("Corpus.zig");
|
||||
const DB = @import("DB.zig");
|
||||
const ErrCtx = @import("ErrCtx.zig");
|
||||
|
||||
const usage =
|
||||
\\usage: turbonss-unix2systemd [OPTION]...
|
||||
\\
|
||||
\\Options:
|
||||
\\ -h Print this help message and exit
|
||||
\\ --passwd Path to passwd file (default: passwd)
|
||||
\\ --group Path to group file (default: group)
|
||||
\\ --outdir Path to output directory (default: ./userdb)
|
||||
\\
|
||||
;
|
||||
|
||||
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 gpa = heap.raw_c_allocator;
|
||||
|
||||
const stderr = io.getStdErr().writer();
|
||||
const stdout = io.getStdOut().writer();
|
||||
|
||||
const return_code = execute(gpa, stdout, stderr, argv[1..]);
|
||||
os.exit(return_code);
|
||||
}
|
||||
|
||||
fn execute(
|
||||
allocator: Allocator,
|
||||
stdout: anytype,
|
||||
stderr: anytype,
|
||||
argv: []const [*:0]const u8,
|
||||
) u8 {
|
||||
const result = flags.parse(argv, &[_]flags.Flag{
|
||||
.{ .name = "-h", .kind = .boolean },
|
||||
.{ .name = "--passwd", .kind = .arg },
|
||||
.{ .name = "--group", .kind = .arg },
|
||||
.{ .name = "--outdir", .kind = .arg },
|
||||
}) catch {
|
||||
stderr.writeAll(usage) catch {};
|
||||
return 1;
|
||||
};
|
||||
|
||||
if (result.boolFlag("-h")) {
|
||||
stdout.writeAll(usage) catch return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (result.args.len != 0) {
|
||||
stderr.print("ERROR: unknown option '{s}'\n", .{result.args[0]}) catch {};
|
||||
stderr.writeAll(usage) catch {};
|
||||
return 1;
|
||||
}
|
||||
|
||||
const passwd_fname = result.argFlag("--passwd") orelse "passwd";
|
||||
const group_fname = result.argFlag("--group") orelse "group";
|
||||
const outdir = result.argFlag("--outdir") orelse "./userdb";
|
||||
|
||||
// to catch an error set file.OpenError, wait for
|
||||
// https://github.com/ziglang/zig/issues/2473
|
||||
var errc = ErrCtx{};
|
||||
var passwd_file = fs.cwd().openFile(passwd_fname, .{ .mode = .read_only }) catch |err|
|
||||
return fail(errc.wrapf("open '{s}'", .{passwd_fname}), stderr, err);
|
||||
defer passwd_file.close();
|
||||
|
||||
var group_file = fs.cwd().openFile(group_fname, .{ .mode = .read_only }) catch |err|
|
||||
return fail(errc.wrapf("open '{s}'", .{group_fname}), stderr, err);
|
||||
defer group_file.close();
|
||||
|
||||
var passwdReader = io.bufferedReader(passwd_file.reader()).reader();
|
||||
var users = User.fromReader(allocator, &errc, passwdReader) catch |err|
|
||||
return fail(errc.wrap("read users"), stderr, err);
|
||||
defer {
|
||||
for (users) |*user| user.deinit(allocator);
|
||||
allocator.free(users);
|
||||
}
|
||||
|
||||
var groupReader = io.bufferedReader(group_file.reader()).reader();
|
||||
var groups = Group.fromReader(allocator, groupReader) catch |err|
|
||||
return fail(errc.wrap("read groups"), stderr, err);
|
||||
defer {
|
||||
for (groups) |*group| group.deinit(allocator);
|
||||
allocator.free(groups);
|
||||
}
|
||||
|
||||
const user2groups = StringArrayHashMap(ArrayListUnmanaged([]const u8)).init(allocator);
|
||||
defer {
|
||||
var it = user2groups.iterator();
|
||||
while (it.next()) |entry|
|
||||
entry.value_ptr.*.deinit(allocator);
|
||||
user2groups.deinit();
|
||||
}
|
||||
fillMemberships(allocator, groups, &user2groups);
|
||||
|
||||
try os.mkdirZ(outdir, 0o755) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => |err| return err,
|
||||
};
|
||||
|
||||
var dir = try fs.cwd().openDir(outdir, .{});
|
||||
|
||||
try makePasswd(dir, users.items);
|
||||
try makeGroups(dir, groups.items);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const JSONPasswd = struct {
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
userName: []const u8, // pw_name
|
||||
realName: []const u8, // pw_gecos
|
||||
homeDirectory: []const u8,
|
||||
shell: []const u8,
|
||||
memberOf: []const []const u8,
|
||||
};
|
||||
|
||||
fn makePasswd(
|
||||
dir: fs.Dir,
|
||||
users: []User,
|
||||
memberships: *const StringArrayHashMap(ArrayListUnmanaged([]const u8)),
|
||||
) !void {
|
||||
var namebuf: [PackedUser.max_name_len + ".user".len:0]u8 = undefined;
|
||||
var symlinkbuf: [fmt.count("{d}.user", math.maxInt(u32)):0]u8 = undefined;
|
||||
|
||||
for (users) |user| {
|
||||
const member_of = if (memberships.get(user.name)) |m|
|
||||
m.items
|
||||
else
|
||||
[]const []const u8{};
|
||||
|
||||
const u = JSONPasswd{
|
||||
.uid = user.uid,
|
||||
.gid = user.gid,
|
||||
.userName = user.name,
|
||||
.realName = user.gecos,
|
||||
.homeDirectory = user.home,
|
||||
.shell = user.shell,
|
||||
.memberOf = member_of,
|
||||
};
|
||||
|
||||
const fname = try fmt.bufPrintZ(namebuf, "{s}.user", user.name);
|
||||
var f = try dir.createFileZ(fname, .{});
|
||||
defer f.close();
|
||||
|
||||
var wr = io.bufferedWriter(f.writer());
|
||||
try json.stringify(u, .{}, wr);
|
||||
try wr.flush();
|
||||
|
||||
const symlinkname = try fmt.bufPrintZ(symlinkbuf, "{d}.user", user.uid);
|
||||
try os.symlinkatZ(fname, dir.fd, symlinkname);
|
||||
}
|
||||
}
|
||||
|
||||
fn makeGroups(dir: fs.Dir, groups: []Group) !void {
|
||||
_ = dir;
|
||||
_ = groups;
|
||||
}
|
||||
|
||||
fn fail(errc: *ErrCtx, stderr: anytype, err: anytype) u8 {
|
||||
const err_chain = errc.unwrap().constSlice();
|
||||
stderr.print("ERROR {s}: {s}\n", .{ @errorName(err), err_chain }) catch {};
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn fillMemberships(
|
||||
allocator: Allocator,
|
||||
groups: ArrayList(Group),
|
||||
user2groups: *StringArrayHashMap(ArrayListUnmanaged([]const u8)),
|
||||
) void {
|
||||
for (groups) |group| {
|
||||
for (group.members) |member| {
|
||||
const member_groups = try user2groups.getOrPut(allocator, member.name);
|
||||
if (!member_groups.found_existing)
|
||||
member_groups.value_ptr.* = ArrayListUnmanaged([]const u8){};
|
||||
member_groups.value_ptr.*.append(group.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "turbonss-unix2systemd invalid argument" {
|
||||
const allocator = testing.allocator;
|
||||
const args = &[_][*:0]const u8{"--invalid-argument"};
|
||||
var stderr = ArrayList(u8).init(allocator);
|
||||
defer stderr.deinit();
|
||||
var stdout = ArrayList(u8).init(allocator);
|
||||
defer stdout.deinit();
|
||||
|
||||
const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]);
|
||||
try testing.expectEqual(@as(u8, 1), exit_code);
|
||||
try testing.expect(mem.startsWith(
|
||||
u8,
|
||||
stderr.items,
|
||||
"ERROR: unknown option '--invalid-argument'",
|
||||
));
|
||||
}
|
||||
|
||||
test "turbonss-unix2systemd trivial error: missing passwd file" {
|
||||
const allocator = testing.allocator;
|
||||
const args = &[_][*:0]const u8{
|
||||
"--passwd",
|
||||
"/does/not/exist/passwd",
|
||||
"--group",
|
||||
"/does/not/exist/group",
|
||||
};
|
||||
var stderr = ArrayList(u8).init(allocator);
|
||||
defer stderr.deinit();
|
||||
var stdout = ArrayList(u8).init(allocator);
|
||||
defer stdout.deinit();
|
||||
|
||||
const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args[0..]);
|
||||
try testing.expectEqual(@as(u8, 1), exit_code);
|
||||
try testing.expectEqualStrings(stderr.items, "ERROR FileNotFound: open '/does/not/exist/passwd'\n");
|
||||
}
|
||||
|
||||
test "turbonss-unix2systemd fail" {
|
||||
var errc = ErrCtx{};
|
||||
var buf = ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
var wr = buf.writer();
|
||||
const exit_code = fail(errc.wrapf("invalid user 'foo'", .{}), wr, error.NotSure);
|
||||
try testing.expectEqual(exit_code, 1);
|
||||
try testing.expectEqualStrings(buf.items, "ERROR NotSure: invalid user 'foo'\n");
|
||||
}
|
||||
|
||||
test "turbonss-unix2db smoke test" {
|
||||
const allocator = testing.allocator;
|
||||
var stderr = ArrayList(u8).init(allocator);
|
||||
defer stderr.deinit();
|
||||
var stdout = ArrayList(u8).init(allocator);
|
||||
defer stdout.deinit();
|
||||
|
||||
var corpus = try Corpus.testCorpus(allocator);
|
||||
defer corpus.deinit();
|
||||
|
||||
var tmp = testing.tmpDir(.{});
|
||||
// TODO: defer
|
||||
errdefer tmp.cleanup();
|
||||
|
||||
const tmp_path = blk: {
|
||||
const relative_path = try fs.path.join(allocator, &[_][]const u8{
|
||||
"zig-cache",
|
||||
"tmp",
|
||||
tmp.sub_path[0..],
|
||||
});
|
||||
const real_path = try fs.realpathAlloc(allocator, relative_path);
|
||||
allocator.free(relative_path);
|
||||
break :blk real_path;
|
||||
};
|
||||
defer allocator.free(tmp_path);
|
||||
|
||||
const passwdPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "passwd" });
|
||||
defer allocator.free(passwdPath);
|
||||
const groupPath = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "group" });
|
||||
defer allocator.free(groupPath);
|
||||
const outDir = try fs.path.joinZ(allocator, &[_][]const u8{ tmp_path, "outdir" });
|
||||
defer allocator.free(outDir);
|
||||
|
||||
const passwd_fd = try os.open(passwdPath, os.O.CREAT | os.O.WRONLY, 0o644);
|
||||
const group_fd = try os.open(groupPath, os.O.CREAT | os.O.WRONLY, 0o644);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < corpus.users.len) : (i += 1) {
|
||||
const user = corpus.users.get(i);
|
||||
const line = user.toLine().constSlice();
|
||||
_ = try os.write(passwd_fd, line);
|
||||
}
|
||||
os.close(passwd_fd);
|
||||
|
||||
var group_writer = (fs.File{ .handle = group_fd }).writer();
|
||||
i = 0;
|
||||
while (i < corpus.groups.len) : (i += 1)
|
||||
try corpus.groups.get(i).writeTo(group_writer);
|
||||
os.close(group_fd);
|
||||
|
||||
const args = &[_][*:0]const u8{
|
||||
"--passwd", passwdPath,
|
||||
"--group", groupPath,
|
||||
"--outdir", outDir,
|
||||
};
|
||||
|
||||
const exit_code = execute(allocator, stdout.writer(), stderr.writer(), args);
|
||||
try testing.expectEqualStrings("total 1664 bytes. groups=5 users=4\n", stderr.items);
|
||||
try testing.expectEqual(@as(u8, 0), exit_code);
|
||||
}
|
Loading…
Reference in New Issue
Block a user