const std = @import("std"); const os = std.os; const fmt = std.fmt; const log = std.log; const mem = std.mem; const process = std.process; const once = std.once; const DB = @import("DB.zig"); const File = @import("File.zig"); const ErrCtx = @import("ErrCtx.zig"); const CGroup = @import("Group.zig").CGroup; const PackedGroup = @import("PackedGroup.zig"); const CUser = @import("User.zig").CUser; const PackedUser = @import("PackedUser.zig"); const c = @cImport({ @cInclude("nss.h"); }); const ENV_DB = "TURBONSS_DB"; const ENV_LOGLEVEL = "TURBONSS_LOGLEVEL"; const ENV_OMIT_MEMBERS = "TURBONSS_OMIT_MEMBERS"; export var turbonss_db_path: [:0]const u8 = "/etc/turbonss/db.turbo"; pub var log_level: std.log.Level = .err; // State is a type of the global variable holding the process state: // the DB handle and all the iterators. const State = struct { file: ?File, getpwent_iterator: ?PackedUser.Iterator = null, getgrent_iterator: ?PackedGroup.Iterator = null, omit_members: bool, }; // global_state is initialized on first call to an nss function var global_state: State = undefined; var global_init = once(init); // assigns State from environment variables et al fn init() void { if (os.getenvZ(ENV_LOGLEVEL)) |env| { const got = mem.sliceTo(env, 0); if (mem.eql(u8, got, "0")) { log_level = .err; } else if (mem.eql(u8, got, "1")) { log_level = .warn; } else if (mem.eql(u8, got, "2")) { log_level = .info; } else if (mem.eql(u8, got, "3")) { log_level = .debug; } else { std.debug.print( "warning: unrecognized {s}={s}. Expected between 0 and 3\n", .{ ENV_LOGLEVEL, got }, ); } } const omit_members = blk: { if (os.getenvZ(ENV_OMIT_MEMBERS)) |env| { const got = mem.sliceTo(env, 0); if (mem.eql(u8, got, "1")) { break :blk true; } else if (mem.eql(u8, got, "0")) { break :blk false; } else if (mem.eql(u8, got, "auto")) { // not set, do autodiscover } else { std.debug.print( "warning: unrecognized {s}={s}. Expected 0, 1 or auto\n", .{ ENV_OMIT_MEMBERS, got }, ); } } if (os.argv.len == 0) break :blk false; break :blk mem.eql(u8, mem.sliceTo(os.argv[0], 0), "id"); }; log.debug("omitting members from getgr* calls: {any}\n", .{omit_members}); const fname = os.getenvZ(ENV_DB) orelse turbonss_db_path; log.debug("opening '{s}'", .{fname}); const file = File.open(fname) catch |err| { log.warn("open '{s}': {s}", .{ fname, @errorName(err) }); return; }; log.debug("turbonss database opened", .{}); global_state = State{ .file = file, .omit_members = omit_members, }; } export fn _nss_turbo_getpwuid_r( uid: c_uint, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDb(errnop) orelse return c.NSS_STATUS_UNAVAIL; if (db.getpwuid(uid, buffer[0..buflen])) |maybe_cuser| { if (maybe_cuser) |cuser| { passwd.* = cuser; return c.NSS_STATUS_SUCCESS; } else { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; } } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } export fn _nss_turbo_getpwnam_r( name: [*:0]const u8, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDb(errnop) orelse return c.NSS_STATUS_UNAVAIL; const nameSlice = mem.sliceTo(name, 0); if (db.getpwnam(nameSlice, buffer[0..buflen])) |maybe_cuser| { if (maybe_cuser) |cuser| { passwd.* = cuser; return c.NSS_STATUS_SUCCESS; } else { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; } } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } export fn _nss_turbo_getgrgid_r( gid: c_uint, gr: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDb(errnop) orelse return c.NSS_STATUS_UNAVAIL; if (db.getgrgid(gid, buffer[0..buflen])) |maybe_cgroup| { if (maybe_cgroup) |cgroup| { gr.* = cgroup; return c.NSS_STATUS_SUCCESS; } else { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; } } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } export fn _nss_turbo_getgrnam_r( name: [*:0]const u8, group: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDb(errnop) orelse return c.NSS_STATUS_UNAVAIL; const nameSlice = mem.sliceTo(name, 0); if (db.getgrnam(nameSlice, buffer[0..buflen])) |maybe_cgroup| { if (maybe_cgroup) |cgroup| { group.* = cgroup; return c.NSS_STATUS_SUCCESS; } else { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; } } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } fn getDb(errnop: *c_int) ?DB { global_init.call(); if (global_state.file) |file| { return file.db; } else { errnop.* = @enumToInt(os.E.AGAIN); return null; } } const testing = std.testing; test "getpwuid_r and getpwnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const turbonss_db_path_old = turbonss_db_path; turbonss_db_path = tf.path; defer { turbonss_db_path = turbonss_db_path_old; } var buffer: [1024]u8 = undefined; var errno: c_int = 0; var passwd: CUser = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getpwuid_r(128, &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getpwnam_r("vidmantas", &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_NOTFOUND, _nss_turbo_getpwnam_r("does not exist", &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.NOENT), @intCast(u16, errno)); passwd = undefined; var small_buffer: [1]u8 = undefined; try testing.expectEqual( c.NSS_STATUS_TRYAGAIN, _nss_turbo_getpwuid_r(0, &passwd, &small_buffer, small_buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.RANGE), @intCast(u16, errno)); } fn testVidmantas(u: CUser) !void { try testing.expectEqualStrings("vidmantas", mem.sliceTo(u.pw_name, 0)); try testing.expectEqual(@as(u32, 128), u.pw_uid); try testing.expectEqual(@as(u32, 128), u.pw_gid); try testing.expectEqualStrings("Vidmantas Kaminskas", mem.sliceTo(u.pw_gecos, 0)); try testing.expectEqualStrings("/bin/bash", mem.sliceTo(u.pw_shell, 0)); } test "getgrgid_r and getgrnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const turbonss_db_path_old = turbonss_db_path; turbonss_db_path = tf.path; defer { turbonss_db_path = turbonss_db_path_old; } var buffer: [1024]u8 = undefined; var errno: c_int = 0; var group: CGroup = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getgrgid_r(128, &group, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantasGroup(group); } fn testVidmantasGroup(g: CGroup) !void { try testing.expectEqual(@as(u32, 128), g.gid); const members = g.members; try testing.expect(members[0] != null); }