From 0a74bf2c3a708485ad9352b0a16fc25157ce3c50 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 22 Jan 2024 14:47:36 +0100 Subject: [PATCH] nd/conf: add lnd config (de)serializer and use it conf gen the change is based on the previously added ini parser. this makes lnd config gen more robust and allows to presist config modifications in the future, such as changing node alias. --- build.zig | 3 + src/lightning.zig | 7 + src/lightning/LndConf.zig | 288 ++++++++++++++++++++++++++++++++ src/{ => lightning}/lndhttp.zig | 2 +- src/nd/Config.zig | 157 +++++++++++------ src/nd/Daemon.zig | 2 +- src/test.zig | 11 ++ src/types.zig | 14 ++ 8 files changed, 432 insertions(+), 52 deletions(-) create mode 100644 src/lightning.zig create mode 100644 src/lightning/LndConf.zig rename src/{ => lightning}/lndhttp.zig (99%) diff --git a/build.zig b/build.zig index 529fdc3..7c768cc 100644 --- a/build.zig +++ b/build.zig @@ -21,6 +21,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); const libnif = libnif_dep.artifact("nif"); + // ini file format parser + const libini = b.addModule("ini", .{ .source_file = .{ .path = "lib/ini/src/ini.zig" } }); const common_cflags = .{ "-Wall", @@ -119,6 +121,7 @@ pub fn build(b: *std.Build) void { }); tests.addOptions("build_options", buildopts); tests.addModule("nif", libnif_dep.module("nif")); + tests.addModule("ini", libini); tests.linkLibrary(libnif); const run_tests = b.addRunArtifact(tests); diff --git a/src/lightning.zig b/src/lightning.zig new file mode 100644 index 0000000..038ff37 --- /dev/null +++ b/src/lightning.zig @@ -0,0 +1,7 @@ +pub const LndConf = @import("lightning/LndConf.zig"); +pub const lndhttp = @import("lightning/lndhttp.zig"); + +test { + const std = @import("std"); + std.testing.refAllDecls(@This()); +} diff --git a/src/lightning/LndConf.zig b/src/lightning/LndConf.zig new file mode 100644 index 0000000..06f949c --- /dev/null +++ b/src/lightning/LndConf.zig @@ -0,0 +1,288 @@ +//! lnd config parser and serializer based on github.com/jessevdk/go-flags, +//! specifically https://pkg.go.dev/github.com/jessevdk/go-flags#IniParser.Parse +//! +//! see https://docs.lightning.engineering/lightning-network-tools/lnd/lnd.conf +//! for lnd config docs. + +const std = @import("std"); +const ini = @import("ini"); + +const logger = std.log.scoped(.lndconf); + +sections: std.ArrayList(Section), +// holds section allocations and their key/value pairs. +// initialized at `init` and dropped at `deinit`. +arena: *std.heap.ArenaAllocator, + +// case-insensitive default group name according to source doc comments +// at github.com/jessevdk/go-flags +pub const MainSection = "application options"; + +// a section key/value pairs (properties) followed by its declaration +// in square brackets like "[section name]". +pub const Section = struct { + name: []const u8, // section name without square brackets + props: std.StringArrayHashMap(PropValue), + + /// holds all allocations throughout the lifetime of the section. + /// initialized in `appendSection`. + alloc: std.mem.Allocator, + + /// key and value are dup'ed by `Section.alloc`. + /// if an existing property exist, it necessarily becomes an array and the + /// new value is appended to the array. + pub fn appendPropStr(self: *Section, key: []const u8, value: []const u8) !void { + const vdup = try self.alloc.dupe(u8, value); + errdefer self.alloc.free(vdup); + + var res = try self.props.getOrPut(try self.alloc.dupe(u8, key)); + if (!res.found_existing) { + res.value_ptr.* = .{ .str = vdup }; + return; + } + + switch (res.value_ptr.*) { + .str => |s| { + var list = try std.ArrayList([]const u8).initCapacity(self.alloc, 2); + try list.append(s); // s is already owned by arena backing self.alloc + try list.append(vdup); + res.value_ptr.* = .{ .astr = try list.toOwnedSlice() }; + }, + .astr => |a| { + var list = std.ArrayList([]const u8).fromOwnedSlice(self.alloc, a); + try list.append(vdup); + res.value_ptr.* = .{ .astr = try list.toOwnedSlice() }; + }, + } + } + + /// replaces any existing property by the given key with the new value. + /// value is duplicated in the owned memory. + pub fn setPropStr(self: *Section, key: []const u8, value: []const u8) !void { + const vdup = try self.alloc.dupe(u8, value); + errdefer self.alloc.free(vdup); + var res = try self.props.getOrPut(try self.alloc.dupe(u8, key)); + if (res.found_existing) { + res.value_ptr.free(self.alloc); + } + res.value_ptr.* = .{ .str = vdup }; + } + + /// formats value using `std.fmt.format` and calls `setPropStr`. + /// the resulting formatted value cannot exceed 512 characters. + pub fn setPropFmt(self: *Section, key: []const u8, comptime fmt: []const u8, args: anytype) !void { + var buf = [_]u8{0} ** 512; + const val = try std.fmt.bufPrint(&buf, fmt, args); + return self.setPropStr(key, val); + } +}; + +/// the value part of a key/value pair. +pub const PropValue = union(enum) { + str: []const u8, // a "string" + astr: [][]const u8, // an array of strings, all values of repeated keys + + /// used internally when replacing an existing value in functions such as + /// `Section.setPropStr` + fn free(self: PropValue, allocator: std.mem.Allocator) void { + switch (self) { + .str => |s| allocator.free(s), + .astr => |a| { + for (a) |s| allocator.free(s); + allocator.free(a); + }, + } + } +}; + +const LndConf = @This(); + +/// creates a empty config, ready to be populated start with `appendSection`. +/// the object is serialized with `dumpWriter`. `deinit` when no long used. +/// to parse an existing config use `load` or `loadReader`. +pub fn init(allocator: std.mem.Allocator) !LndConf { + var arena = try allocator.create(std.heap.ArenaAllocator); + arena.* = std.heap.ArenaAllocator.init(allocator); + return .{ + .arena = arena, + .sections = std.ArrayList(Section).init(arena.allocator()), + }; +} + +// frees all resources used by the config object. +pub fn deinit(self: LndConf) void { + const allocator = self.arena.child_allocator; + self.arena.deinit(); + allocator.destroy(self.arena); +} + +/// parses an existing config at the specified file path. +/// a thin wrapper around `loadReader` passing it a file reader. +pub fn load(allocator: std.mem.Allocator, filepath: []const u8) !LndConf { + const f = try std.fs.cwd().openFile(filepath, .{ .mode = .read_only }); + defer f.close(); + return loadReader(allocator, f.reader()); +} + +/// parses config contents from reader `r`. +/// makes no section deduplication: all sections are simply appended to `sections` +/// in the encountered order, with ascii characters of the name converted to lower case. +/// values of identical key names are grouped into `PropValue.astr`. +pub fn loadReader(allocator: std.mem.Allocator, r: anytype) !LndConf { + var parser = ini.parse(allocator, r); + defer parser.deinit(); + + var conf = try LndConf.init(allocator); + errdefer conf.deinit(); + + var currsect: ?*Section = null; + while (try parser.next()) |record| { + switch (record) { + .section => |name| currsect = try conf.appendSection(name), + .property => |kv| { + if (currsect == null) { + currsect = try conf.appendSection(MainSection); + } + try currsect.?.appendPropStr(kv.key, kv.value); + }, + .enumeration => |v| logger.warn("ignoring key without value: {s}", .{v}), + } + } + + return conf; +} + +/// serializes the config into writer `w`. +pub fn dumpWriter(self: LndConf, w: anytype) !void { + for (self.sections.items, 0..) |*sec, i| { + if (i > 0) { + try w.writeByte('\n'); + } + try w.print("[{s}]\n", .{sec.name}); + var it = sec.props.iterator(); + while (it.next()) |kv| { + const key = kv.key_ptr.*; + switch (kv.value_ptr.*) { + .str => |s| try w.print("{s}={s}\n", .{ key, s }), + .astr => |a| for (a) |s| try w.print("{s}={s}\n", .{ key, s }), + } + } + } +} + +/// makes no deduplication: callers must do it themselves. +pub fn appendDefaultSection(self: *LndConf) !*Section { + return self.appendSection(MainSection); +} + +/// creates a new confg section, owning a name copy dup'ed using the `arena` allocator. +/// the section name ascii is converted to lower case. +pub fn appendSection(self: *LndConf, name: []const u8) !*Section { + const alloc = self.arena.allocator(); + var name_dup = try alloc.dupe(u8, name); + toLower(name_dup); + try self.sections.append(.{ + .name = name_dup, + .props = std.StringArrayHashMap(PropValue).init(alloc), + .alloc = alloc, + }); + return &self.sections.items[self.sections.items.len - 1]; +} + +/// returns a section named `MainSection`, if any. +pub fn mainSection(self: LndConf) ?*Section { + return self.findSection(MainSection); +} + +/// O(n); name must be in lower case. +pub fn findSection(self: *const LndConf, name: []const u8) ?*Section { + for (self.sections.items) |*sec| { + if (std.mem.eql(u8, sec.name, name)) { + return sec; + } + } + return null; +} + +fn toLower(s: []u8) void { + for (s, 0..) |c, i| { + switch (c) { + 'A'...'Z' => s[i] = c | 0b00100000, + else => {}, + } + } +} + +test "lnd: conf load dump" { + const t = std.testing; + const tt = @import("../test.zig"); + + var tmp = try tt.TempDir.create(); + defer tmp.cleanup(); + try tmp.dir.writeFile("conf.ini", + \\; top comment + \\[application options] + \\foo = bar + \\; line comment + \\baz = quix1 ; inline comment + \\baz = quix2 + \\ + \\[AutopiloT] + \\autopilot.active=false + ); + const clean_conf = + \\[application options] + \\foo=bar + \\baz=quix1 + \\baz=quix2 + \\ + \\[autopilot] + \\autopilot.active=false + \\ + ; + + const conf = try LndConf.load(t.allocator, try tmp.join(&.{"conf.ini"})); + defer conf.deinit(); + + var dump = std.ArrayList(u8).init(t.allocator); + defer dump.deinit(); + try conf.dumpWriter(dump.writer()); + try t.expectEqualStrings(clean_conf, dump.items); + + const sec = conf.mainSection().?; + try t.expectEqualStrings("bar", sec.props.get("foo").?.str); + const bazval: []const []const u8 = &.{ "quix1", "quix2" }; + try tt.expectDeepEqual(bazval, sec.props.get("baz").?.astr); +} + +test "lnd: conf append and dump" { + const t = std.testing; + + var conf = try LndConf.init(t.allocator); + defer conf.deinit(); + var sec = try conf.appendSection(MainSection); + try sec.appendPropStr("foo", "bar"); + + var buf = std.ArrayList(u8).init(t.allocator); + defer buf.deinit(); + try conf.dumpWriter(buf.writer()); + try t.expectEqualStrings("[application options]\nfoo=bar\n", buf.items); + + try sec.setPropStr("foo", "baz"); + var sec2 = try conf.appendSection("test"); + try sec2.setPropStr("bar", "quix1"); + try sec2.appendPropStr("bar", "quix2"); + + buf.clearAndFree(); + try conf.dumpWriter(buf.writer()); + const want_conf = + \\[application options] + \\foo=baz + \\ + \\[test] + \\bar=quix1 + \\bar=quix2 + \\ + ; + try t.expectEqualStrings(want_conf, buf.items); +} diff --git a/src/lndhttp.zig b/src/lightning/lndhttp.zig similarity index 99% rename from src/lndhttp.zig rename to src/lightning/lndhttp.zig index a6c435e..656c0c3 100644 --- a/src/lndhttp.zig +++ b/src/lightning/lndhttp.zig @@ -3,7 +3,7 @@ const std = @import("std"); const base64enc = std.base64.standard.Encoder; -const types = @import("types.zig"); +const types = @import("../types.zig"); /// safe for concurrent use as long as Client.allocator is. pub const Client = struct { diff --git a/src/nd/Config.zig b/src/nd/Config.zig index 2f85cef..588ac37 100644 --- a/src/nd/Config.zig +++ b/src/nd/Config.zig @@ -2,6 +2,8 @@ //! the structure is defined in `Data`. const std = @import("std"); +const lightning = @import("../lightning.zig"); +const types = @import("../types.zig"); const logger = std.log.scoped(.config); @@ -270,7 +272,7 @@ pub fn lndConnectWaitMacaroonFile(self: Config, allocator: std.mem.Allocator, ty /// returns the bytes printed to outbuf. pub fn makeWalletUnlockFile(self: Config, outbuf: []u8, comptime raw_size: usize) ![]const u8 { const filepath = LND_WALLETUNLOCK_PATH; - const lnduser = try std.process.getUserInfo(LND_OS_USER); + const lnduser = try types.getUserInfo(LND_OS_USER); const allocator = self.arena.child_allocator; const opt = .{ .mode = 0o400 }; @@ -290,64 +292,70 @@ pub fn makeWalletUnlockFile(self: Config, outbuf: []u8, comptime raw_size: usize return hex; } -/// creates or overwrites existing lnd config file at `LND_CONF_PATH`. -pub fn genLndConfig(self: Config, opt: struct { autounlock: bool }) !void { - const confpath = LND_CONF_PATH; - const lnduser = try std.process.getUserInfo(LND_OS_USER); +/// options for genLndConfig. +pub const LndConfOpt = struct { + autounlock: bool, + path: ?[]const u8 = null, // defaults to LND_CONF_PATH +}; + +/// creates or overwrites existing lnd config file on disk. +pub fn genLndConfig(self: Config, opt: LndConfOpt) !void { + const confpath = opt.path orelse LND_CONF_PATH; + const lnduser = try types.getUserInfo(LND_OS_USER); const allocator = self.arena.child_allocator; - const file = try std.io.BufferedAtomicFile.create(allocator, std.fs.cwd(), confpath, .{ .mode = 0o400 }); - defer file.destroy(); // frees resources; does NOT delete the file - const w = file.writer(); - - // main app settings - try w.writeAll("[Application Options]\n"); - try w.writeAll("debuglevel=info\n"); - try w.writeAll("maxpendingchannels=10\n"); - try w.writeAll("maxlogfiles=3\n"); - try w.writeAll("listen=[::]:9735\n"); // or 0.0.0.0:9735 - try w.writeAll("rpclisten=0.0.0.0:10009\n"); - try w.writeAll("restlisten=0.0.0.0:10010\n"); // TODO: replace with 127.0.0.1 and no-rest-tls=true? - try std.fmt.format(w, "alias={s}\n", .{"nakamochi"}); // TODO: make alias configurable - try std.fmt.format(w, "datadir={s}\n", .{LND_DATA_DIR}); - try std.fmt.format(w, "logdir={s}\n", .{LND_LOG_DIR}); + var conf = try lightning.LndConf.init(allocator); + defer conf.deinit(); + var sec = try conf.appendDefaultSection(); + try sec.setPropStr("debuglevel", "info"); + try sec.setPropStr("maxpendingchannels", "10"); + try sec.setPropStr("maxlogfiles", "3"); + try sec.setPropStr("listen", "[::]:9735"); // or 0.0.0.0:9735 + try sec.setPropStr("rpclisten", "0.0.0.0:10009"); + try sec.setPropStr("restlisten", "0.0.0.0:10010"); + try sec.setPropStr("alias", "nakamochi"); // TODO: make alias configurable + try sec.setPropStr("datadir", LND_DATA_DIR); + try sec.setPropStr("logdir", LND_LOG_DIR); if (self.static.lnd_tor_hostname) |torhost| { - try std.fmt.format(w, "tlsextradomain={s}\n", .{torhost}); - try std.fmt.format(w, "externalhosts={s}\n", .{torhost}); + try sec.setPropStr("tlsextradomain", torhost); + try sec.setPropStr("externalhosts", torhost); } if (opt.autounlock) { - try std.fmt.format(w, "wallet-unlock-password-file={s}\n", .{LND_WALLETUNLOCK_PATH}); + try sec.setPropStr("wallet-unlock-password-file", LND_WALLETUNLOCK_PATH); } // bitcoin chain settings - try w.writeAll("\n[bitcoin]\n"); - try std.fmt.format(w, "bitcoin.chaindir={s}/chain/mainnet\n", .{LND_DATA_DIR}); - try w.writeAll("bitcoin.active=true\n"); - try w.writeAll("bitcoin.mainnet=True\n"); - try w.writeAll("bitcoin.testnet=False\n"); - try w.writeAll("bitcoin.regtest=False\n"); - try w.writeAll("bitcoin.simnet=False\n"); - try w.writeAll("bitcoin.node=bitcoind\n"); - try w.writeAll("\n[bitcoind]\n"); - try w.writeAll("bitcoind.zmqpubrawblock=tcp://127.0.0.1:8331\n"); - try w.writeAll("bitcoind.zmqpubrawtx=tcp://127.0.0.1:8330\n"); - try w.writeAll("bitcoind.rpchost=127.0.0.1\n"); - try w.writeAll("bitcoind.rpcuser=rpc\n"); + sec = try conf.appendSection("bitcoin"); + try sec.setPropFmt("bitcoin.chaindir", "{s}/chain/mainnet", .{LND_DATA_DIR}); + try sec.setPropStr("bitcoin.active", "true"); + try sec.setPropStr("bitcoin.mainnet", "true"); + try sec.setPropStr("bitcoin.testnet", "false"); + try sec.setPropStr("bitcoin.regtest", "false"); + try sec.setPropStr("bitcoin.simnet", "false"); + try sec.setPropStr("bitcoin.node", "bitcoind"); + sec = try conf.appendSection("bitcoind"); + try sec.setPropStr("bitcoind.zmqpubrawblock", "tcp://127.0.0.1:8331"); + try sec.setPropStr("bitcoind.zmqpubrawtx", "tcp://127.0.0.1:8330"); + try sec.setPropStr("bitcoind.rpchost", "127.0.0.1"); + try sec.setPropStr("bitcoind.rpcuser", "rpc"); if (self.static.bitcoind_rpc_pass) |rpcpass| { - try std.fmt.format(w, "bitcoind.rpcpass={s}\n", .{rpcpass}); + try sec.setPropStr("bitcoind.rpcpass", rpcpass); } else { return error.GenLndConfigNoBitcoindRpcPass; } // other settings - try w.writeAll("\n[autopilot]\n"); - try w.writeAll("autopilot.active=false\n"); - try w.writeAll("\n[tor]\n"); - try w.writeAll("tor.active=true\n"); - try w.writeAll("tor.skip-proxy-for-clearnet-targets=true\n"); + sec = try conf.appendSection("autopilot"); + try sec.setPropStr("autopilot.active", "false"); + sec = try conf.appendSection("tor"); + try sec.setPropStr("tor.active", "true"); + try sec.setPropStr("tor.skip-proxy-for-clearnet-targets", "true"); - // persist the file in the correct location. - try file.finish(); + // dump config into the file. + const file = try std.io.BufferedAtomicFile.create(allocator, std.fs.cwd(), confpath, .{ .mode = 0o400 }); + defer file.destroy(); // frees resources; does NOT delete the file + try conf.dumpWriter(file.writer()); + try file.finish(); // persist the file in the correct location // change file ownership to that of the lnd system user. const f = try std.fs.cwd().openFile(confpath, .{}); @@ -355,7 +363,7 @@ pub fn genLndConfig(self: Config, opt: struct { autounlock: bool }) !void { try f.chown(lnduser.uid, lnduser.gid); } -test "init existing" { +test "ndconfig: init existing" { const t = std.testing; const tt = @import("../test.zig"); @@ -375,7 +383,7 @@ test "init existing" { try t.expectEqualStrings("/sysupdates/run.sh", conf.data.sysrunscript); } -test "init null" { +test "ndconfig: init null" { const t = std.testing; const conf = try init(t.allocator, "/non/existent/config/file"); @@ -385,7 +393,7 @@ test "init null" { try t.expectEqualStrings(SYSUPDATES_RUN_SCRIPT_PATH, conf.data.sysrunscript); } -test "dump" { +test "ndconfig: dump" { const t = std.testing; const tt = @import("../test.zig"); @@ -416,7 +424,7 @@ test "dump" { try t.expectEqualStrings("runscript.sh", parsed.value.sysrunscript); } -test "switch sysupdates and infer" { +test "ndconfig: switch sysupdates and infer" { const t = std.testing; const tt = @import("../test.zig"); @@ -448,11 +456,11 @@ test "switch sysupdates and infer" { try t.expectEqual(SysupdatesChannel.dev, inferSysupdatesChannel(cronscript)); } -test "switch sysupdates with .run=true" { +test "ndconfig: switch sysupdates with .run=true" { const t = std.testing; const tt = @import("../test.zig"); - // no arena deinit: expecting Config to + // no arena deinit here: expecting Config to auto-deinit. var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator); conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator); var tmp = try tt.TempDir.create(); @@ -492,3 +500,52 @@ fn testLoadConfigData(path: []const u8) !std.json.Parsed(Data) { const jopt = .{ .ignore_unknown_fields = true, .allocate = .alloc_always }; return try std.json.parseFromSlice(Data, allocator, bytes, jopt); } + +test "ndconfig: genLndConfig" { + const t = std.testing; + const tt = @import("../test.zig"); + + // Config auto-deinits the arena. + var conf_arena = try std.testing.allocator.create(std.heap.ArenaAllocator); + conf_arena.* = std.heap.ArenaAllocator.init(std.testing.allocator); + var tmp = try tt.TempDir.create(); + defer tmp.cleanup(); + + var conf = Config{ + .arena = conf_arena, + .confpath = undefined, // unused + .data = .{ + .syschannel = .master, // unused + .syscronscript = undefined, // unused + .sysrunscript = undefined, // unused + }, + .static = .{ + .lnd_tor_hostname = "test.onion", + .bitcoind_rpc_pass = "test secret", + }, + }; + defer conf.deinit(); + + const confpath = try tmp.join(&.{"lndconf.ini"}); + try conf.genLndConfig(.{ .autounlock = false, .path = confpath }); + + const bytes = try std.fs.cwd().readFileAlloc(t.allocator, confpath, 1 << 20); + defer t.allocator.free(bytes); + try tt.expectSubstring("tlsextradomain=test.onion\n", bytes); + try tt.expectSubstring("externalhosts=test.onion\n", bytes); + try tt.expectNoSubstring("wallet-unlock-password-file", bytes); + try tt.expectSubstring("bitcoind.rpcpass=test secret\n", bytes); + + try conf.genLndConfig(.{ .autounlock = true, .path = confpath }); + const bytes2 = try std.fs.cwd().readFileAlloc(t.allocator, confpath, 1 << 20); + defer t.allocator.free(bytes2); + try tt.expectSubstring("wallet-unlock-password-file=", bytes2); + + const lndconf = try lightning.LndConf.load(t.allocator, confpath); + defer lndconf.deinit(); + try t.expect(lndconf.mainSection() != null); + try t.expect(lndconf.findSection("bitcoin") != null); + try t.expect(lndconf.findSection("bitcoind") != null); + try t.expect(lndconf.findSection("autopilot") != null); + try t.expect(lndconf.findSection("tor") != null); +} diff --git a/src/nd/Daemon.zig b/src/nd/Daemon.zig index 155d6cf..9fe0413 100644 --- a/src/nd/Daemon.zig +++ b/src/nd/Daemon.zig @@ -18,7 +18,7 @@ const time = std.time; const bitcoindrpc = @import("../bitcoindrpc.zig"); const comm = @import("../comm.zig"); const Config = @import("Config.zig"); -const lndhttp = @import("../lndhttp.zig"); +const lndhttp = @import("../lightning.zig").lndhttp; const network = @import("network.zig"); const screen = @import("../ui/screen.zig"); const SysService = @import("SysService.zig"); diff --git a/src/test.zig b/src/test.zig index b885375..0fa528c 100644 --- a/src/test.zig +++ b/src/test.zig @@ -315,11 +315,22 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void { } } +pub fn expectSubstring(needle: []const u8, haystack: []const u8) !void { + const n = std.mem.count(u8, haystack, needle); + try std.testing.expect(n > 0); +} + +pub fn expectNoSubstring(needle: []const u8, haystack: []const u8) !void { + const n = std.mem.count(u8, haystack, needle); + try std.testing.expect(n == 0); +} + test { _ = @import("nd.zig"); _ = @import("nd/Daemon.zig"); _ = @import("nd/SysService.zig"); _ = @import("ngui.zig"); + _ = @import("lightning.zig"); std.testing.refAllDecls(@This()); } diff --git a/src/types.zig b/src/types.zig index b226c69..7fe3956 100644 --- a/src/types.zig +++ b/src/types.zig @@ -9,11 +9,25 @@ pub usingnamespace if (builtin.is_test) struct { pub const Timer = tt.TestTimer; pub const ChildProcess = tt.TestChildProcess; pub const WpaControl = tt.TestWpaControl; + + /// always returns caller's (current process) user/group IDs. + /// atm works only on linux via getuid syscalls. + pub fn getUserInfo(name: []const u8) !std.process.UserInfo { + _ = name; + return .{ + .uid = std.os.linux.getuid(), + .gid = std.os.linux.getgid(), + }; + } } else struct { // regular types for production code. pub const Timer = std.time.Timer; pub const ChildProcess = std.ChildProcess; pub const WpaControl = nif.wpa.Control; + + pub fn getUserInfo(name: []const u8) !std.process.UserInfo { + return std.process.getUserInfo(name); + } }; /// prefer this type over the std.ArrayList(u8) just to ensure consistency