1#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
8use std::ffi::CString;
9use std::path::PathBuf;
10#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
11use std::{
12 os::unix::ffi::OsStrExt,
13 path::Path,
14};
15
16#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
17use nix::errno::Errno;
18use nix::unistd::{
19 Gid,
20 Group,
21 Uid,
22 User,
23};
24
25#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
26const ETC_PASSWD: &str = "/etc/passwd";
27#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
28const ETC_GROUP: &str = "/etc/group";
29
30#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
31pub fn user_from_uid(uid: Uid) -> nix::Result<Option<User>> {
32 User::from_uid(uid)
33}
34
35#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
36pub fn user_from_uid(uid: Uid) -> nix::Result<Option<User>> {
37 find_user_in_passwd(&read_account_file(ETC_PASSWD)?, |user| user.uid == uid)
38}
39
40#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
41pub fn user_from_name(name: &str) -> nix::Result<Option<User>> {
42 User::from_name(name)
43}
44
45#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
46pub fn user_from_name(name: &str) -> nix::Result<Option<User>> {
47 find_user_in_passwd(&read_account_file(ETC_PASSWD)?, |user| user.name == name)
48}
49
50#[cfg(not(all(target_env = "gnu", target_feature = "crt-static")))]
51pub fn group_from_gid(gid: Gid) -> nix::Result<Option<Group>> {
52 Group::from_gid(gid)
53}
54
55#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
56pub fn group_from_gid(gid: Gid) -> nix::Result<Option<Group>> {
57 find_group_in_group(&read_account_file(ETC_GROUP)?, |group| group.gid == gid)
58}
59
60pub fn current_shell() -> nix::Result<Option<PathBuf>> {
61 user_from_uid(nix::unistd::getuid()).map(|user| user.map(|user| user.shell))
62}
63
64#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
68pub fn parse_supplementary_gids(
69 etc_group_content: &str,
70 username: &str,
71 primary_gid: Gid,
72) -> Vec<Gid> {
73 let mut gids = vec![primary_gid];
74 for line in etc_group_content.lines() {
75 let Some(group) = parse_group_line(line) else {
76 continue;
77 };
78 if group.mem.iter().any(|member| member == username) && !gids.contains(&group.gid) {
79 gids.push(group.gid);
80 }
81 }
82 gids
83}
84
85#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
86pub fn supplementary_gids(username: &str, primary_gid: Gid) -> nix::Result<Vec<Gid>> {
87 Ok(parse_supplementary_gids(
88 &read_account_file(ETC_GROUP)?,
89 username,
90 primary_gid,
91 ))
92}
93
94#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
95fn read_account_file(path: &str) -> nix::Result<String> {
96 std::fs::read_to_string(path).map_err(io_error_to_errno)
97}
98
99#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
100fn io_error_to_errno(err: std::io::Error) -> Errno {
101 err.raw_os_error().map_or(Errno::EIO, Errno::from_raw)
102}
103
104#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
105fn find_user_in_passwd(
106 content: &str,
107 predicate: impl Fn(&User) -> bool,
108) -> nix::Result<Option<User>> {
109 for line in content.lines() {
110 let Some(user) = parse_passwd_line(line)? else {
111 continue;
112 };
113 if predicate(&user) {
114 return Ok(Some(user));
115 }
116 }
117 Ok(None)
118}
119
120#[cfg(all(target_env = "gnu", target_feature = "crt-static"))]
121fn find_group_in_group(
122 content: &str,
123 predicate: impl Fn(&Group) -> bool,
124) -> nix::Result<Option<Group>> {
125 for line in content.lines() {
126 let Some(group) = parse_group_line(line) else {
127 continue;
128 };
129 if predicate(&group) {
130 return Ok(Some(group));
131 }
132 }
133 Ok(None)
134}
135
136#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
137fn parse_passwd_line(line: &str) -> nix::Result<Option<User>> {
138 let line = line.trim();
139 if line.is_empty() || line.starts_with('#') {
140 return Ok(None);
141 }
142
143 let mut fields = line.split(':');
144 let Some(name) = fields.next() else {
145 return Ok(None);
146 };
147 let Some(passwd) = fields.next() else {
148 return Ok(None);
149 };
150 let Some(uid) = fields.next().and_then(|uid| uid.parse::<u32>().ok()) else {
151 return Ok(None);
152 };
153 let Some(gid) = fields.next().and_then(|gid| gid.parse::<u32>().ok()) else {
154 return Ok(None);
155 };
156 let Some(gecos) = fields.next() else {
157 return Ok(None);
158 };
159 let Some(dir) = fields.next() else {
160 return Ok(None);
161 };
162 let Some(shell) = fields.next() else {
163 return Ok(None);
164 };
165 if fields.next().is_some() {
166 return Ok(None);
167 }
168
169 Ok(Some(User {
170 name: name.to_owned(),
171 passwd: cstring(passwd)?,
172 uid: Uid::from_raw(uid),
173 gid: Gid::from_raw(gid),
174 gecos: cstring(gecos)?,
175 dir: pathbuf_from_bytes(dir),
176 shell: pathbuf_from_bytes(shell),
177 }))
178}
179
180#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
181fn parse_group_line(line: &str) -> Option<Group> {
182 let line = line.trim();
183 if line.is_empty() || line.starts_with('#') {
184 return None;
185 }
186
187 let mut fields = line.split(':');
188 let name = fields.next()?;
189 let passwd = fields.next()?;
190 let gid = fields.next()?.parse::<u32>().ok()?;
191 let members = fields.next().unwrap_or("");
192 if fields.next().is_some() {
193 return None;
194 }
195
196 Some(Group {
197 name: name.to_owned(),
198 passwd: CString::new(passwd).ok()?,
199 gid: Gid::from_raw(gid),
200 mem: members
201 .split(',')
202 .map(str::trim)
203 .filter(|member| !member.is_empty())
204 .map(str::to_owned)
205 .collect(),
206 })
207}
208
209#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
210fn cstring(s: &str) -> nix::Result<CString> {
211 CString::new(s).map_err(|_| Errno::EINVAL)
212}
213
214#[cfg(any(test, all(target_env = "gnu", target_feature = "crt-static")))]
215fn pathbuf_from_bytes(s: &str) -> PathBuf {
216 Path::new(std::ffi::OsStr::from_bytes(s.as_bytes())).to_path_buf()
217}
218
219#[cfg(test)]
220mod tests {
221 use test_that::prelude::*;
222
223 use super::*;
224
225 #[test]
226 fn test_parse_passwd_line_basic() {
227 let user = parse_passwd_line("alice:x:1000:100:Alice:/home/alice:/bin/bash")
228 .unwrap()
229 .unwrap();
230 assert_eq!(user.name, "alice");
231 assert_eq!(user.uid, Uid::from_raw(1000));
232 assert_eq!(user.gid, Gid::from_raw(100));
233 assert_eq!(user.shell, PathBuf::from("/bin/bash"));
234 }
235
236 #[test]
237 fn test_parse_passwd_line_skips_malformed_lines() {
238 assert_that!(parse_passwd_line("# comment").unwrap(), none());
239 assert_that!(parse_passwd_line("missing:fields").unwrap(), none());
240 assert_that!(
241 parse_passwd_line("alice:x:not-a-uid:100:Alice:/home/alice:/bin/bash").unwrap(),
242 none()
243 );
244 assert_that!(
245 parse_passwd_line("alice:x:1000:100:Alice:/home/alice:/bin/bash:extra").unwrap(),
246 none()
247 );
248 }
249
250 #[test]
251 fn test_parse_supplementary_gids_basic() {
252 let content =
253 "root:x:0:\ndaemon:x:1:\nusers:x:100:alice,bob\ndocker:x:999:alice\nwheel:x:10:bob\n";
254 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
255 assert_eq!(
256 gids,
257 vec![Gid::from_raw(1000), Gid::from_raw(100), Gid::from_raw(999)]
258 );
259 }
260
261 #[test]
262 fn test_parse_supplementary_gids_primary_gid_deduped() {
263 let content = "users:x:1000:alice\n";
264 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
265 assert_eq!(gids, vec![Gid::from_raw(1000)]);
266 }
267
268 #[test]
269 fn test_parse_supplementary_gids_no_members() {
270 let content = "root:x:0:\nusers:x:100:\n";
271 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
272 assert_eq!(gids, vec![Gid::from_raw(1000)]);
273 }
274
275 #[test]
276 fn test_parse_supplementary_gids_skips_malformed_lines() {
277 let content = "root:x:0:\nmalformed_line\n:x:abc:alice\nusers:x:100:alice\n";
278 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
279 assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
280 }
281
282 #[test]
283 fn test_parse_supplementary_gids_skips_comments_and_empty() {
284 let content = "# this is a comment\n\nusers:x:100:alice\n";
285 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
286 assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
287 }
288
289 #[test]
290 fn test_parse_supplementary_gids_no_partial_match() {
291 let content = "group1:x:100:alice2,malice\ngroup2:x:200:alice\n";
292 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
293 assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(200)]);
294 }
295
296 #[test]
297 fn test_parse_supplementary_gids_whitespace_in_members() {
298 let content = "group1:x:100: alice , bob \n";
299 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
300 assert_eq!(gids, vec![Gid::from_raw(1000), Gid::from_raw(100)]);
301 }
302
303 #[test]
304 fn test_parse_supplementary_gids_no_user_list_field() {
305 let content = "nogroup:x:65534\n";
306 let gids = parse_supplementary_gids(content, "alice", Gid::from_raw(1000));
307 assert_eq!(gids, vec![Gid::from_raw(1000)]);
308 }
309}