tiny_std/unix/passwd/
getpw_r.rs

1use rusl::platform::{Fd, GidT, OpenFlags, UidT};
2
3use crate::error::{Error, Result};
4
5#[derive(Debug, Copy, Clone)]
6pub struct Passwd<'a> {
7    pub name: &'a str,
8    pub passwd: &'a str,
9    pub uid: UidT,
10    pub gid: GidT,
11    pub gecos: &'a str,
12    pub dir: &'a str,
13    pub shell: &'a str,
14}
15
16/// Attempts to get a `Passwd` entry by pwuid
17/// # Errors
18/// `uid` isn't listed in `/etc/passwd`
19/// `/etc/passwd` isn't readable.
20pub fn getpwuid_r(uid: UidT, buf: &mut [u8]) -> Result<Option<Passwd>> {
21    let fd =
22        unsafe { rusl::unistd::open_raw(c"/etc/passwd".as_ptr() as usize, OpenFlags::O_RDONLY)? };
23    search_pwd_fd(fd, uid, buf)
24}
25
26#[inline]
27fn search_pwd_fd(fd: Fd, uid: UidT, buf: &mut [u8]) -> Result<Option<Passwd>> {
28    // Compiler gets confused here, or I am causing UB, one of the two.
29    // --- Mut borrow start
30    rusl::unistd::read(fd, buf)?;
31    // --- Borrow ends
32    loop {
33        // --- Immut borrow start
34        let buf_view = unsafe { core::slice::from_raw_parts(buf.as_ptr(), buf.len()) };
35        // When this returns we've dropped the immutable borrow and can overwrite the
36        // bytes that we're discarding.
37        let b = match search_from(uid, buf_view)? {
38            // Returning bytes borrowed from `buf` but the compiler things this entry still claims
39            // those bytes and doesn't allow us to mutate the buffer again
40            SearchRes::Pwd(p) => return Ok(Some(p)),
41            SearchRes::ReadUpTo(b) => b,
42            SearchRes::NotFound => return Ok(None),
43        };
44        // --- Immut borrow ends
45        let len = buf.len();
46        buf.copy_within(b.., 0);
47        // --- Mut borrow start
48        rusl::unistd::read(fd, &mut buf[len - b..])?;
49        // --- Mut borrow ends
50    }
51}
52
53enum SearchRes<'a> {
54    Pwd(Passwd<'a>),
55    ReadUpTo(usize),
56    NotFound,
57}
58
59#[inline]
60fn search_from(uid: UidT, buf: &[u8]) -> Result<SearchRes> {
61    if let Some(pwd) = find_by_uid(buf, uid)? {
62        Ok(SearchRes::Pwd(pwd))
63    } else if let Some(nl) = find_last_newline(buf) {
64        Ok(SearchRes::ReadUpTo(nl + 1))
65    } else {
66        Ok(SearchRes::NotFound)
67    }
68}
69
70#[inline]
71fn find_by_uid(pwd_buf: &[u8], uid: UidT) -> Result<Option<Passwd>> {
72    let mut offset = 0;
73    loop {
74        let Some(next) = next_line(&pwd_buf[offset..]) else {
75            return Ok(None);
76        };
77        let res = try_pwd(next)?;
78        if let Some(pwd) = res {
79            if pwd.uid == uid {
80                return Ok(Some(pwd));
81            }
82        }
83        offset += next.len() + 1;
84    }
85}
86
87#[inline]
88fn next_line(buf: &[u8]) -> Option<&[u8]> {
89    for i in 0..buf.len() {
90        if buf[i] == b'\n' {
91            return Some(&buf[..i]);
92        }
93    }
94    None
95}
96
97#[inline]
98fn try_pwd(line: &[u8]) -> Result<Option<Passwd>> {
99    let mut slices = [0, 0, 0, 0, 0, 0];
100    for (ind, byte) in line.iter().enumerate() {
101        if *byte == b':' {
102            for i in &mut slices {
103                if *i == 0 {
104                    *i = ind;
105                    break;
106                }
107            }
108        }
109    }
110    if slices[5] == 0 {
111        return Ok(None);
112    }
113    let mut shell_line = core::str::from_utf8(&line[slices[5] + 1..])
114        .map_err(|_| Error::no_code("Failed to convert pwd shell to utf8"))?;
115    if shell_line.ends_with('\n') {
116        shell_line = shell_line.trim_end_matches('\n');
117    }
118    Ok(Some(Passwd {
119        name: core::str::from_utf8(&line[..slices[0]])
120            .map_err(|_| Error::no_code("Failed to convert pwd name to utf8"))?,
121        passwd: core::str::from_utf8(&line[slices[0] + 1..slices[1]])
122            .map_err(|_| Error::no_code("Failed to convert pwd passwd to utf8"))?,
123        uid: try_parse_num(&line[slices[1] + 1..slices[2]])?,
124        gid: try_parse_num(&line[slices[2] + 1..slices[3]])?,
125        gecos: core::str::from_utf8(&line[slices[3] + 1..slices[4]])
126            .map_err(|_| Error::no_code("Failed to convert pwd gecos to utf8"))?,
127        dir: core::str::from_utf8(&line[slices[4] + 1..slices[5]])
128            .map_err(|_| Error::no_code("Failed to convert pwd dir to utf8"))?,
129        shell: shell_line,
130    }))
131}
132
133const NUM_OUT_OF_RANGE: Error = Error::no_code("Number out of range");
134
135// Just ascii numbers
136#[inline]
137#[expect(clippy::needless_range_loop)]
138fn try_parse_num(buf: &[u8]) -> Result<u32> {
139    let len = buf.len();
140    let mut pow = u32::try_from(buf.len()).map_err(|_e| {
141        Error::no_code("Tried to parse a number that had a digit-length larger than u32::MAX")
142    })?;
143    let mut sum: u32 = 0;
144    for i in 0..len {
145        pow -= 1;
146        let digit = buf[i].checked_sub(48).ok_or(Error::no_code(
147            "Unexpected value in buffer to parse as number.",
148        ))?;
149        sum = sum
150            .checked_add(
151                u32::from(digit)
152                    .checked_mul(10u32.checked_pow(pow).ok_or(NUM_OUT_OF_RANGE)?)
153                    .ok_or(NUM_OUT_OF_RANGE)?,
154            )
155            .ok_or(NUM_OUT_OF_RANGE)?;
156    }
157    Ok(sum)
158}
159
160#[inline]
161fn find_last_newline(buf: &[u8]) -> Option<usize> {
162    let len = buf.len();
163    for i in 1..len {
164        if buf[len - i] == b'\n' {
165            return Some(len - i);
166        }
167    }
168    None
169}
170
171#[cfg(test)]
172mod tests {
173    use rusl::platform::OpenFlags;
174    use rusl::string::unix_str::UnixStr;
175    use rusl::unistd::open;
176
177    use crate::unix::passwd::getpw_r::{
178        find_by_uid, next_line, search_pwd_fd, try_parse_num, try_pwd,
179    };
180
181    const EXAMPLE: &str = "root:x:0:0::/root:/bin/bash
182bin:x:1:1::/:/usr/bin/nologin
183daemon:x:2:2::/:/usr/bin/nologin
184mail:x:8:12::/var/spool/mail:/usr/bin/nologin
185ftp:x:14:11::/srv/ftp:/usr/bin/nologin
186http:x:33:33::/srv/http:/usr/bin/nologin
187nobody:x:65534:65534:Nobody:/:/usr/bin/nologin
188dbus:x:81:81:System Message Bus:/:/usr/bin/nologin
189systemd-journal-remote:x:981:981:systemd Journal Remote:/:/usr/bin/nologin
190systemd-network:x:980:980:systemd Network Management:/:/usr/bin/nologin
191systemd-oom:x:979:979:systemd Userspace OOM Killer:/:/usr/bin/nologin
192systemd-resolve:x:978:978:systemd Resolver:/:/usr/bin/nologin
193systemd-timesync:x:977:977:systemd Time Synchronization:/:/usr/bin/nologin
194systemd-coredump:x:976:976:systemd Core Dumper:/:/usr/bin/nologin
195uuidd:x:68:68::/:/usr/bin/nologin
196git:x:975:975:git daemon user:/:/usr/bin/git-shell
197dhcpcd:x:974:974:dhcpcd privilege separation:/:/usr/bin/nologin
198gramar:x:1000:1000::/home/gramar:/usr/bin/zsh
199nvidia-persis";
200
201    #[test]
202    fn line_by_line() {
203        let buf = EXAMPLE.as_bytes();
204        let mut offset = 0;
205        for line in EXAMPLE.lines() {
206            if let Some(next) = next_line(&buf[offset..]) {
207                assert_eq!(
208                    line.as_bytes(),
209                    next,
210                    "\n{line} vs {}",
211                    core::str::from_utf8(next).unwrap()
212                );
213                offset += line.len() + 1; // newline + 1
214            } else {
215                assert_eq!("nvidia-persis", line);
216            }
217        }
218    }
219
220    #[test]
221    fn pwd_line() {
222        let line = b"bin:x:1:1::/:/usr/bin/nologin\n";
223        let pwd = try_pwd(line).unwrap().unwrap();
224        assert_eq!("bin", pwd.name);
225        assert_eq!("x", pwd.passwd);
226        assert_eq!(1, pwd.uid);
227        assert_eq!(1, pwd.gid);
228        assert_eq!("", pwd.gecos);
229        assert_eq!("/", pwd.dir);
230        assert_eq!("/usr/bin/nologin", pwd.shell);
231    }
232
233    #[test]
234    fn parse_num() {
235        let my_num = "2048";
236        assert_eq!(2048, try_parse_num(my_num.as_bytes()).unwrap());
237        let my_num = "0";
238        assert_eq!(0, try_parse_num(my_num.as_bytes()).unwrap());
239        let my_num = "-5";
240        assert!(try_parse_num(my_num.as_bytes()).is_err());
241        // u64::Max
242        assert!(try_parse_num("18446744073709551615".as_bytes()).is_err());
243    }
244
245    #[test]
246    fn pwd_by_uid() {
247        let pwd = find_by_uid(EXAMPLE.as_bytes(), 1000).unwrap().unwrap();
248        assert_eq!(pwd.name, "gramar");
249        assert_eq!(pwd.passwd, "x");
250        assert_eq!(pwd.uid, 1000);
251        assert_eq!(pwd.gid, 1000);
252        assert_eq!(pwd.gecos, "");
253        assert_eq!(pwd.dir, "/home/gramar");
254        assert_eq!(pwd.shell, "/usr/bin/zsh");
255    }
256
257    #[test]
258    fn pwd_by_missing() {
259        assert!(find_by_uid(EXAMPLE.as_bytes(), 9959).unwrap().is_none());
260    }
261
262    #[test]
263    fn search_pwd_normal_sized_buf() {
264        let fd = open(
265            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
266            OpenFlags::O_RDONLY,
267        )
268        .unwrap();
269        let mut buf = [0u8; 1024];
270        let pwd = search_pwd_fd(fd, 1000, &mut buf).unwrap().unwrap();
271        assert_eq!(pwd.name, "gramar");
272        assert_eq!(pwd.passwd, "x");
273        assert_eq!(pwd.uid, 1000);
274        assert_eq!(pwd.gid, 1000);
275        assert_eq!(pwd.gecos, "");
276        assert_eq!(pwd.dir, "/home/gramar");
277        assert_eq!(pwd.shell, "/usr/bin/zsh");
278    }
279
280    #[test]
281    fn search_pwd_small_buf() {
282        let fd = open(
283            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
284            OpenFlags::O_RDONLY,
285        )
286        .unwrap();
287        let mut buf = [0u8; 256];
288        let pwd = search_pwd_fd(fd, 1000, &mut buf).unwrap().unwrap();
289        assert_eq!(pwd.name, "gramar");
290        assert_eq!(pwd.passwd, "x");
291        assert_eq!(pwd.uid, 1000);
292        assert_eq!(pwd.gid, 1000);
293        assert_eq!(pwd.gecos, "");
294        assert_eq!(pwd.dir, "/home/gramar");
295        assert_eq!(pwd.shell, "/usr/bin/zsh");
296    }
297
298    #[test]
299    fn last_entry_tiny_buf() {
300        let fd = open(
301            UnixStr::try_from_str("test-files/unix/passwd/pwd_test.txt\0").unwrap(),
302            OpenFlags::O_RDONLY,
303        )
304        .unwrap();
305        let mut buf = [0u8; 100];
306        let pwd = search_pwd_fd(fd, 110, &mut buf).unwrap().unwrap();
307        assert_eq!(pwd.name, "partimag");
308        assert_eq!(pwd.passwd, "x");
309        assert_eq!(pwd.uid, 110);
310        assert_eq!(pwd.gid, 110);
311        assert_eq!(pwd.gecos, "Partimage user");
312        assert_eq!(pwd.dir, "/");
313        assert_eq!(pwd.shell, "/usr/bin/nologin");
314    }
315}