#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedUser {
pub uid: u32,
pub gid: u32,
}
pub fn resolve_user(spec: &str, passwd: &str, group: &str) -> Result<Option<ResolvedUser>, String> {
let spec = spec.trim();
if spec.is_empty() {
return Ok(None);
}
let (user_part, group_part) = match spec.split_once(':') {
Some((u, g)) => (u, Some(g)),
None => (spec, None),
};
let (uid, passwd_gid) = if let Ok(uid) = user_part.parse::<u32>() {
(uid, passwd_entry_by_uid(passwd, uid).map(|(_, gid)| gid))
} else {
let (uid, gid) = passwd_entry_by_name(passwd, user_part)
.ok_or_else(|| format!("user '{user_part}' not found in the sandbox /etc/passwd"))?;
(uid, Some(gid))
};
let gid = match group_part {
None => passwd_gid.unwrap_or(0),
Some(g) => {
if let Ok(gid) = g.parse::<u32>() {
gid
} else {
group_entry_by_name(group, g)
.ok_or_else(|| format!("group '{g}' not found in the sandbox /etc/group"))?
}
}
};
Ok(Some(ResolvedUser { uid, gid }))
}
fn passwd_entry_by_name(passwd: &str, name: &str) -> Option<(u32, u32)> {
passwd_entries(passwd)
.find_map(|(entry_name, uid, gid)| (entry_name == name).then_some((uid, gid)))
}
fn passwd_entry_by_uid(passwd: &str, uid: u32) -> Option<(u32, u32)> {
passwd_entries(passwd).find_map(|(_, entry_uid, gid)| (entry_uid == uid).then_some((uid, gid)))
}
fn passwd_entries(passwd: &str) -> impl Iterator<Item = (&str, u32, u32)> {
passwd.lines().filter_map(|line| {
let mut fields = line.split(':');
let name = fields.next()?;
let _pw = fields.next()?;
let uid = fields.next()?.parse().ok()?;
let gid = fields.next()?.parse().ok()?;
Some((name, uid, gid))
})
}
fn group_entry_by_name(group: &str, name: &str) -> Option<u32> {
group.lines().find_map(|line| {
let mut fields = line.split(':');
let entry_name = fields.next()?;
let _pw = fields.next()?;
let gid = fields.next()?.parse().ok()?;
(entry_name == name).then_some(gid)
})
}
#[cfg(test)]
mod tests {
use super::*;
const PASSWD: &str = "root:x:0:0:root:/root:/bin/sh\nweb:x:1000:1001:web:/home/web:/bin/sh\n";
const GROUP: &str = "root:x:0:\nstaff:x:2000:web\n";
#[test]
fn empty_spec_is_none() {
assert_eq!(resolve_user("", PASSWD, GROUP).unwrap(), None);
assert_eq!(resolve_user(" ", PASSWD, GROUP).unwrap(), None);
}
#[test]
fn numeric_uid_uses_passwd_gid() {
let u = resolve_user("1000", PASSWD, GROUP).unwrap().unwrap();
assert_eq!(
u,
ResolvedUser {
uid: 1000,
gid: 1001
}
);
}
#[test]
fn numeric_uid_without_passwd_entry_falls_back_to_gid_zero() {
let u = resolve_user("4242", PASSWD, GROUP).unwrap().unwrap();
assert_eq!(u, ResolvedUser { uid: 4242, gid: 0 });
}
#[test]
fn numeric_uid_gid_pair() {
let u = resolve_user("7:9", PASSWD, GROUP).unwrap().unwrap();
assert_eq!(u, ResolvedUser { uid: 7, gid: 9 });
}
#[test]
fn named_user_resolves_both_ids() {
let u = resolve_user("web", PASSWD, GROUP).unwrap().unwrap();
assert_eq!(
u,
ResolvedUser {
uid: 1000,
gid: 1001
}
);
}
#[test]
fn named_user_with_named_group() {
let u = resolve_user("web:staff", PASSWD, GROUP).unwrap().unwrap();
assert_eq!(
u,
ResolvedUser {
uid: 1000,
gid: 2000
}
);
}
#[test]
fn unknown_named_user_errors() {
assert!(resolve_user("ghost", PASSWD, GROUP).is_err());
assert!(resolve_user("web:ghosts", PASSWD, GROUP).is_err());
}
}