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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
use rusl::platform::{Fd, GidT, OpenFlags, UidT};

use crate::error::{Error, Result};

#[derive(Debug, Copy, Clone)]
pub struct Passwd<'a> {
    pub name: &'a str,
    pub passwd: &'a str,
    pub uid: UidT,
    pub gid: GidT,
    pub gecos: &'a str,
    pub dir: &'a str,
    pub shell: &'a str,
}

/// Attempts to get a `Passwd` entry by pwuid
/// # Errors
/// `uid` isn't listed in `/etc/passwd`
/// `/etc/passwd` isn't readable.
pub fn getpwuid_r(uid: UidT, buf: &mut [u8]) -> Result<Option<Passwd>> {
    let fd =
        unsafe { rusl::unistd::open_raw("/etc/passwd\0".as_ptr() as usize, OpenFlags::O_RDONLY)? };
    search_pwd_fd(fd, uid, buf)
}

#[inline]
fn search_pwd_fd(fd: Fd, uid: UidT, buf: &mut [u8]) -> Result<Option<Passwd>> {
    // Compiler gets confused here, or I am causing UB, one of the two.
    // --- Mut borrow start
    rusl::unistd::read(fd, buf)?;
    // --- Borrow ends
    loop {
        // --- Immut borrow start
        let buf_view = unsafe { core::slice::from_raw_parts(buf.as_ptr(), buf.len()) };
        // When this returns we've dropped the immutable borrow and can overwrite the
        // bytes that we're discarding.
        let b = match search_from(uid, buf_view)? {
            // Returning bytes borrowed from `buf` but the compiler things this entry still claims
            // those bytes and doesn't allow us to mutate the buffer again
            SearchRes::Pwd(p) => return Ok(Some(p)),
            SearchRes::ReadUpTo(b) => b,
            SearchRes::NotFound => return Ok(None),
        };
        // --- Immut borrow ends
        let len = buf.len();
        buf.copy_within(b.., 0);
        // --- Mut borrow start
        rusl::unistd::read(fd, &mut buf[len - b..])?;
        // --- Mut borrow ends
    }
}

enum SearchRes<'a> {
    Pwd(Passwd<'a>),
    ReadUpTo(usize),
    NotFound,
}

#[inline]
fn search_from(uid: UidT, buf: &[u8]) -> Result<SearchRes> {
    if let Some(pwd) = find_by_uid(buf, uid)? {
        Ok(SearchRes::Pwd(pwd))
    } else if let Some(nl) = find_last_newline(buf) {
        Ok(SearchRes::ReadUpTo(nl + 1))
    } else {
        Ok(SearchRes::NotFound)
    }
}

#[inline]
fn find_by_uid(pwd_buf: &[u8], uid: UidT) -> Result<Option<Passwd>> {
    let mut offset = 0;
    loop {
        let Some(next) = next_line(&pwd_buf[offset..]) else {
            return Ok(None);
        };
        let res = try_pwd(next)?;
        if let Some(pwd) = res {
            if pwd.uid == uid {
                return Ok(Some(pwd));
            }
        }
        offset += next.len() + 1;
    }
}

#[inline]
fn next_line(buf: &[u8]) -> Option<&[u8]> {
    for i in 0..buf.len() {
        if buf[i] == b'\n' {
            return Some(&buf[..i]);
        }
    }
    None
}

#[inline]
fn try_pwd(line: &[u8]) -> Result<Option<Passwd>> {
    let mut slices = [0, 0, 0, 0, 0, 0];
    for (ind, byte) in line.iter().enumerate() {
        if *byte == b':' {
            for i in &mut slices {
                if *i == 0 {
                    *i = ind;
                    break;
                }
            }
        }
    }
    if slices[5] == 0 {
        return Ok(None);
    }
    let mut shell_line = core::str::from_utf8(&line[slices[5] + 1..])
        .map_err(|_| Error::no_code("Failed to convert pwd shell to utf8"))?;
    if shell_line.ends_with('\n') {
        shell_line = shell_line.trim_end_matches('\n');
    }
    Ok(Some(Passwd {
        name: core::str::from_utf8(&line[..slices[0]])
            .map_err(|_| Error::no_code("Failed to convert pwd name to utf8"))?,
        passwd: core::str::from_utf8(&line[slices[0] + 1..slices[1]])
            .map_err(|_| Error::no_code("Failed to convert pwd passwd to utf8"))?,
        uid: try_parse_num(&line[slices[1] + 1..slices[2]])?,
        gid: try_parse_num(&line[slices[2] + 1..slices[3]])?,
        gecos: core::str::from_utf8(&line[slices[3] + 1..slices[4]])
            .map_err(|_| Error::no_code("Failed to convert pwd gecos to utf8"))?,
        dir: core::str::from_utf8(&line[slices[4] + 1..slices[5]])
            .map_err(|_| Error::no_code("Failed to convert pwd dir to utf8"))?,
        shell: shell_line,
    }))
}

const NUM_OUT_OF_RANGE: Error = Error::no_code("Number out of range");

// Just ascii numbers
#[inline]
#[allow(clippy::needless_range_loop)]
fn try_parse_num(buf: &[u8]) -> Result<u32> {
    let len = buf.len();
    let mut pow = u32::try_from(buf.len()).map_err(|_e| {
        Error::no_code("Tried to parse a number that had a digit-length larger than u32::MAX")
    })?;
    let mut sum: u32 = 0;
    for i in 0..len {
        pow -= 1;
        let digit = buf[i].checked_sub(48).ok_or(Error::no_code(
            "Unexpected value in buffer to parse as number.",
        ))?;
        sum = sum
            .checked_add(
                u32::from(digit)
                    .checked_mul(10u32.checked_pow(pow).ok_or(NUM_OUT_OF_RANGE)?)
                    .ok_or(NUM_OUT_OF_RANGE)?,
            )
            .ok_or(NUM_OUT_OF_RANGE)?;
    }
    Ok(sum)
}

#[inline]
fn find_last_newline(buf: &[u8]) -> Option<usize> {
    let len = buf.len();
    for i in 1..len {
        if buf[len - i] == b'\n' {
            return Some(len - i);
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use rusl::platform::OpenFlags;
    use rusl::string::unix_str::UnixStr;
    use rusl::unistd::open;

    use crate::unix::passwd::getpw_r::{
        find_by_uid, next_line, search_pwd_fd, try_parse_num, try_pwd,
    };

    const EXAMPLE: &str = "root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin
ftp:x:14:11::/srv/ftp:/usr/bin/nologin
http:x:33:33::/srv/http:/usr/bin/nologin
nobody:x:65534:65534:Nobody:/:/usr/bin/nologin
dbus:x:81:81:System Message Bus:/:/usr/bin/nologin
systemd-journal-remote:x:981:981:systemd Journal Remote:/:/usr/bin/nologin
systemd-network:x:980:980:systemd Network Management:/:/usr/bin/nologin
systemd-oom:x:979:979:systemd Userspace OOM Killer:/:/usr/bin/nologin
systemd-resolve:x:978:978:systemd Resolver:/:/usr/bin/nologin
systemd-timesync:x:977:977:systemd Time Synchronization:/:/usr/bin/nologin
systemd-coredump:x:976:976:systemd Core Dumper:/:/usr/bin/nologin
uuidd:x:68:68::/:/usr/bin/nologin
git:x:975:975:git daemon user:/:/usr/bin/git-shell
dhcpcd:x:974:974:dhcpcd privilege separation:/:/usr/bin/nologin
gramar:x:1000:1000::/home/gramar:/usr/bin/zsh
nvidia-persis";

    #[test]
    fn line_by_line() {
        let buf = EXAMPLE.as_bytes();
        let mut offset = 0;
        for line in EXAMPLE.lines() {
            if let Some(next) = next_line(&buf[offset..]) {
                assert_eq!(
                    line.as_bytes(),
                    next,
                    "\n{line} vs {}",
                    core::str::from_utf8(next).unwrap()
                );
                offset += line.as_bytes().len() + 1; // newline + 1
            } else {
                assert_eq!("nvidia-persis", line);
            }
        }
    }

    #[test]
    fn pwd_line() {
        let line = b"bin:x:1:1::/:/usr/bin/nologin\n";
        let pwd = try_pwd(line).unwrap().unwrap();
        assert_eq!("bin", pwd.name);
        assert_eq!("x", pwd.passwd);
        assert_eq!(1, pwd.uid);
        assert_eq!(1, pwd.gid);
        assert_eq!("", pwd.gecos);
        assert_eq!("/", pwd.dir);
        assert_eq!("/usr/bin/nologin", pwd.shell);
    }

    #[test]
    fn parse_num() {
        let my_num = "2048";
        assert_eq!(2048, try_parse_num(my_num.as_bytes()).unwrap());
        let my_num = "0";
        assert_eq!(0, try_parse_num(my_num.as_bytes()).unwrap());
        let my_num = "-5";
        assert!(try_parse_num(my_num.as_bytes()).is_err());
        // u64::Max
        assert!(try_parse_num("18446744073709551615".as_bytes()).is_err());
    }

    #[test]
    fn pwd_by_uid() {
        let pwd = find_by_uid(EXAMPLE.as_bytes(), 1000).unwrap().unwrap();
        assert_eq!(pwd.name, "gramar");
        assert_eq!(pwd.passwd, "x");
        assert_eq!(pwd.uid, 1000);
        assert_eq!(pwd.gid, 1000);
        assert_eq!(pwd.gecos, "");
        assert_eq!(pwd.dir, "/home/gramar");
        assert_eq!(pwd.shell, "/usr/bin/zsh");
    }

    #[test]
    fn pwd_by_missing() {
        assert!(find_by_uid(EXAMPLE.as_bytes(), 9959).unwrap().is_none());
    }

    #[test]
    fn search_pwd_normal_sized_buf() {
        let fd = open(
            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
            OpenFlags::O_RDONLY,
        )
        .unwrap();
        let mut buf = [0u8; 1024];
        let pwd = search_pwd_fd(fd, 1000, &mut buf).unwrap().unwrap();
        assert_eq!(pwd.name, "gramar");
        assert_eq!(pwd.passwd, "x");
        assert_eq!(pwd.uid, 1000);
        assert_eq!(pwd.gid, 1000);
        assert_eq!(pwd.gecos, "");
        assert_eq!(pwd.dir, "/home/gramar");
        assert_eq!(pwd.shell, "/usr/bin/zsh");
    }

    #[test]
    fn search_pwd_small_buf() {
        let fd = open(
            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
            OpenFlags::O_RDONLY,
        )
        .unwrap();
        let mut buf = [0u8; 256];
        let pwd = search_pwd_fd(fd, 1000, &mut buf).unwrap().unwrap();
        assert_eq!(pwd.name, "gramar");
        assert_eq!(pwd.passwd, "x");
        assert_eq!(pwd.uid, 1000);
        assert_eq!(pwd.gid, 1000);
        assert_eq!(pwd.gecos, "");
        assert_eq!(pwd.dir, "/home/gramar");
        assert_eq!(pwd.shell, "/usr/bin/zsh");
    }

    #[test]
    fn last_entry_tiny_buf() {
        let fd = open(
            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
            OpenFlags::O_RDONLY,
        )
        .unwrap();
        let mut buf = [0u8; 100];
        let pwd = search_pwd_fd(fd, 110, &mut buf).unwrap().unwrap();
        assert_eq!(pwd.name, "partimag");
        assert_eq!(pwd.passwd, "x");
        assert_eq!(pwd.uid, 110);
        assert_eq!(pwd.gid, 110);
        assert_eq!(pwd.gecos, "Partimage user");
        assert_eq!(pwd.dir, "/");
        assert_eq!(pwd.shell, "/usr/bin/nologin");
    }
}