use std::ffi::CString;
use std::path::PathBuf;
use nix::unistd::{Gid, Group, Uid, User};
use crate::error::DaemonizeError;
pub(crate) struct ResolvedUser {
pub(crate) name: String,
pub(crate) uid: Uid,
pub(crate) gid: Gid,
pub(crate) dir: PathBuf,
}
impl ResolvedUser {
pub(crate) fn cname(&self) -> Result<CString, DaemonizeError> {
CString::new(self.name.as_str())
.map_err(|e| DaemonizeError::UserNotFound(format!("invalid username: {e}")))
}
}
pub(crate) struct ResolvedIdentity {
user: Option<ResolvedUser>,
explicit_group: Option<Gid>,
}
impl ResolvedIdentity {
pub(crate) fn resolve(user: Option<&str>, group: Option<&str>) -> Result<Self, DaemonizeError> {
let user = match user {
Some(spec) => Some(resolve_user(spec)?),
None => None,
};
let explicit_group = match group {
Some(spec) => Some(resolve_group_gid(spec)?),
None => None,
};
Ok(Self {
user,
explicit_group,
})
}
pub(crate) fn user(&self) -> Option<&ResolvedUser> {
self.user.as_ref()
}
pub(crate) fn effective_gid(&self) -> Option<Gid> {
self.explicit_group
.or_else(|| self.user.as_ref().map(|u| u.gid))
}
pub(crate) fn chown_ids(&self) -> (Option<Uid>, Option<Gid>) {
(self.user.as_ref().map(|u| u.uid), self.effective_gid())
}
}
fn resolve_user(spec: &str) -> Result<ResolvedUser, DaemonizeError> {
let user = if let Ok(uid_num) = spec.parse::<u32>() {
let uid = Uid::from_raw(uid_num);
User::from_uid(uid)
.map_err(|e| DaemonizeError::UserNotFound(format!("getpwuid({uid_num}): {e}")))?
.ok_or_else(|| DaemonizeError::UserNotFound(format!("uid {uid_num}")))?
} else {
User::from_name(spec)
.map_err(|e| DaemonizeError::UserNotFound(format!("getpwnam({spec}): {e}")))?
.ok_or_else(|| DaemonizeError::UserNotFound(spec.to_string()))?
};
Ok(ResolvedUser {
name: user.name,
uid: user.uid,
gid: user.gid,
dir: user.dir,
})
}
fn resolve_group_gid(spec: &str) -> Result<Gid, DaemonizeError> {
if let Ok(gid_num) = spec.parse::<u32>() {
Ok(Gid::from_raw(gid_num))
} else {
let group = Group::from_name(spec)
.map_err(|e| DaemonizeError::GroupNotFound(format!("getgrnam({spec}): {e}")))?
.ok_or_else(|| DaemonizeError::GroupNotFound(spec.to_string()))?;
Ok(group.gid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_user_numeric() {
let user = resolve_user("0").unwrap();
assert_eq!(user.uid.as_raw(), 0);
assert_eq!(user.name, "root");
}
#[test]
fn resolve_user_name() {
let user = resolve_user("root").unwrap();
assert_eq!(user.uid.as_raw(), 0);
}
#[test]
fn resolve_user_nonexistent_name() {
if std::env::var("CI").is_ok() {
return;
}
let result = resolve_user("nonexistent_daemonize_test_user_xyz");
assert!(result.is_err());
}
#[test]
fn resolve_group_gid_numeric() {
let gid = resolve_group_gid("0").unwrap();
assert_eq!(gid.as_raw(), 0);
}
#[test]
fn resolve_group_gid_by_name() {
let result = resolve_group_gid("root").or_else(|_| resolve_group_gid("wheel"));
assert!(result.is_ok());
}
#[test]
fn resolve_group_gid_nonexistent_name() {
if std::env::var("CI").is_ok() {
return;
}
let result = resolve_group_gid("nonexistent_daemonize_test_group_xyz");
assert!(matches!(result, Err(DaemonizeError::GroupNotFound(_))));
}
#[test]
fn chown_ids_user_only() {
let id = ResolvedIdentity::resolve(Some("root"), None).unwrap();
let (uid, gid) = id.chown_ids();
assert_eq!(uid.unwrap().as_raw(), 0);
assert_eq!(gid.unwrap().as_raw(), 0); }
#[test]
fn chown_ids_neither_leaves_unchanged() {
let id = ResolvedIdentity::resolve(None, None).unwrap();
assert_eq!(id.chown_ids(), (None, None));
}
#[test]
fn chown_ids_group_only_numeric() {
let id = ResolvedIdentity::resolve(None, Some("0")).unwrap();
let (uid, gid) = id.chown_ids();
assert!(uid.is_none()); assert_eq!(gid.unwrap().as_raw(), 0);
}
#[test]
fn effective_gid_prefers_explicit_group() {
let id = ResolvedIdentity::resolve(Some("root"), Some("0")).unwrap();
assert_eq!(id.effective_gid().unwrap().as_raw(), 0);
}
}