summaryrefslogtreecommitdiff
path: root/src/os/passwd.zig
blob: e9bbff066b87e847de87403d9032c76a31988289 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
const std = @import("std");
const builtin = @import("builtin");
const internal_os = @import("main.zig");
const build_config = @import("../build_config.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const posix = std.posix;

const log = std.log.scoped(.passwd);

// We want to be extra sure since this will force bad symbols into our import table
comptime {
    if (builtin.target.cpu.arch.isWasm()) {
        @compileError("passwd is not available for wasm");
    }
}

/// Used to determine the default shell and directory on Unixes.
const c = if (builtin.os.tag != .windows) @cImport({
    @cInclude("sys/types.h");
    @cInclude("unistd.h");
    @cInclude("pwd.h");
}) else {};

// Entry that is retrieved from the passwd API. This only contains the fields
// we care about.
pub const Entry = struct {
    shell: ?[:0]const u8 = null,
    home: ?[:0]const u8 = null,
    name: ?[:0]const u8 = null,
};

/// Get the passwd entry for the currently executing user.
pub fn get(alloc: Allocator) !Entry {
    if (builtin.os.tag == .windows) @compileError("passwd is not available on windows");

    var buf: [1024]u8 = undefined;
    var pw: c.struct_passwd = undefined;
    var pw_ptr: ?*c.struct_passwd = null;
    const res = c.getpwuid_r(c.getuid(), &pw, &buf, buf.len, &pw_ptr);
    if (res != 0) {
        log.warn("error retrieving pw entry code={d}", .{res});
        return Entry{};
    }

    if (pw_ptr == null) {
        // Future: let's check if a better shell is available like zsh
        log.warn("no pw entry to detect default shell, will default to 'sh'", .{});
        return Entry{};
    }

    var result: Entry = .{};

    // If we're in flatpak then our entry is always empty so we grab it
    // by shelling out to the host. note that we do HAVE an entry in the
    // sandbox but only the username is correct.
    if (internal_os.isFlatpak()) flatpak: {
        if (comptime !build_config.flatpak) {
            log.warn("flatpak detected, but this build doesn't contain flatpak support", .{});
            break :flatpak;
        }

        log.info("flatpak detected, will use host command to get our entry", .{});

        // Note: we wrap our getent call in a /bin/sh login shell because
        // some operating systems (NixOS tested) don't set the PATH for various
        // utilities properly until we get a login shell.
        const Pty = @import("../pty.zig").Pty;
        var pty = try Pty.open(.{});
        defer pty.deinit();
        var cmd: internal_os.FlatpakHostCommand = .{
            .argv = &[_][]const u8{
                "/bin/sh",
                "-l",
                "-c",
                try std.fmt.allocPrint(
                    alloc,
                    "getent passwd {s}",
                    .{std.mem.sliceTo(pw.pw_name, 0)},
                ),
            },
            .stdin = pty.slave,
            .stdout = pty.slave,
            .stderr = pty.slave,
        };
        _ = try cmd.spawn(alloc);
        _ = try cmd.wait();

        // Once started, we can close the child side. We do this after
        // wait right now but that is fine too. This lets us read the
        // parent and detect EOF.
        _ = posix.close(pty.slave);

        // Read all of our output
        const output = output: {
            var output: std.ArrayListUnmanaged(u8) = .{};
            while (true) {
                const n = posix.read(pty.master, &buf) catch |err| {
                    switch (err) {
                        // EIO is triggered at the end since we closed our
                        // child side. This is just EOF for this. I'm not sure
                        // if I'm doing this wrong.
                        error.InputOutput => break,
                        else => return err,
                    }
                };

                try output.appendSlice(alloc, buf[0..n]);

                // Max total size is buf.len. We can do better here by trimming
                // the front and continuing reading but we choose to just exit.
                if (output.items.len > buf.len) break;
            }

            break :output try output.toOwnedSlice(alloc);
        };

        // Shell and home are the last two entries
        var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':');
        result.shell = if (it.next()) |v| try alloc.dupeZ(u8, v) else null;
        result.home = if (it.next()) |v| try alloc.dupeZ(u8, v) else null;
        return result;
    }

    if (pw.pw_shell) |ptr| {
        const source = std.mem.sliceTo(ptr, 0);
        const value = try alloc.dupeZ(u8, source);
        result.shell = value;
    }

    if (pw.pw_dir) |ptr| {
        const source = std.mem.sliceTo(ptr, 0);
        const value = try alloc.dupeZ(u8, source);
        result.home = value;
    }

    if (pw.pw_name) |ptr| {
        const source = std.mem.sliceTo(ptr, 0);
        const value = try alloc.dupeZ(u8, source);
        result.name = value;
    }

    return result;
}

test {
    if (builtin.os.tag == .windows) return error.SkipZigTest;
    const testing = std.testing;
    var arena = ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    // We should be able to get an entry
    const entry = try get(alloc);
    try testing.expect(entry.shell != null);
    try testing.expect(entry.home != null);
}