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
16pub 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 rusl::unistd::read(fd, buf)?;
31 loop {
33 let buf_view = unsafe { core::slice::from_raw_parts(buf.as_ptr(), buf.len()) };
35 let b = match search_from(uid, buf_view)? {
38 SearchRes::Pwd(p) => return Ok(Some(p)),
41 SearchRes::ReadUpTo(b) => b,
42 SearchRes::NotFound => return Ok(None),
43 };
44 let len = buf.len();
46 buf.copy_within(b.., 0);
47 rusl::unistd::read(fd, &mut buf[len - b..])?;
49 }
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#[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; } 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 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}