pub(crate) use self::serialization::SysusersData;
use crate::errors::{Context, SdError};
pub use parse::parse_from_reader;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::io::BufRead;
use std::path::PathBuf;
use std::str::FromStr;
mod format;
mod parse;
mod serialization;
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SysusersEntry {
AddRange(AddRange),
AddUserToGroup(AddUserToGroup),
CreateGroup(CreateGroup),
CreateUserAndGroup(CreateUserAndGroup),
}
impl SysusersEntry {
pub fn type_signature(&self) -> &str {
match self {
SysusersEntry::AddRange(v) => v.type_signature(),
SysusersEntry::AddUserToGroup(v) => v.type_signature(),
SysusersEntry::CreateGroup(v) => v.type_signature(),
SysusersEntry::CreateUserAndGroup(v) => v.type_signature(),
}
}
pub fn name(&self) -> &str {
match self {
SysusersEntry::AddRange(_) => "-",
SysusersEntry::AddUserToGroup(v) => &v.username,
SysusersEntry::CreateGroup(v) => &v.groupname,
SysusersEntry::CreateUserAndGroup(v) => &v.name,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[serde(try_from = "SysusersData")]
pub struct AddRange {
pub(crate) from: u32,
pub(crate) to: u32,
}
impl AddRange {
pub fn new(from: u32, to: u32) -> Result<Self, SdError> {
Ok(Self { from, to })
}
pub fn type_signature(&self) -> &str {
"r"
}
pub fn from(&self) -> u32 {
self.from
}
pub fn to(&self) -> u32 {
self.to
}
pub(crate) fn into_sysusers_entry(self) -> SysusersEntry {
SysusersEntry::AddRange(self)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
#[serde(try_from = "SysusersData")]
pub struct AddUserToGroup {
pub(crate) username: String,
pub(crate) groupname: String,
}
impl AddUserToGroup {
pub fn new(username: String, groupname: String) -> Result<Self, SdError> {
validate_name_strict(&username)?;
validate_name_strict(&groupname)?;
Ok(Self {
username,
groupname,
})
}
pub fn type_signature(&self) -> &str {
"m"
}
pub fn username(&self) -> &str {
&self.username
}
pub fn groupname(&self) -> &str {
&self.groupname
}
pub(crate) fn into_sysusers_entry(self) -> SysusersEntry {
SysusersEntry::AddUserToGroup(self)
}
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(try_from = "SysusersData")]
pub struct CreateGroup {
pub(crate) groupname: String,
pub(crate) gid: GidOrPath,
}
impl CreateGroup {
pub fn new(groupname: String) -> Result<Self, SdError> {
Self::impl_new(groupname, GidOrPath::Automatic)
}
pub fn new_with_gid(groupname: String, gid: u32) -> Result<Self, SdError> {
Self::impl_new(groupname, GidOrPath::Gid(gid))
}
pub fn new_with_path(groupname: String, path: PathBuf) -> Result<Self, SdError> {
Self::impl_new(groupname, GidOrPath::Path(path))
}
pub(crate) fn impl_new(groupname: String, gid: GidOrPath) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Ok(Self { groupname, gid })
}
pub fn type_signature(&self) -> &str {
"g"
}
pub fn groupname(&self) -> &str {
&self.groupname
}
pub fn has_dynamic_gid(&self) -> bool {
matches!(self.gid, GidOrPath::Automatic)
}
pub fn static_gid(&self) -> Option<u32> {
match self.gid {
GidOrPath::Gid(n) => Some(n),
_ => None,
}
}
pub(crate) fn into_sysusers_entry(self) -> SysusersEntry {
SysusersEntry::CreateGroup(self)
}
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(try_from = "SysusersData")]
pub struct CreateUserAndGroup {
pub(crate) name: String,
pub(crate) id: IdOrPath,
pub(crate) gecos: String,
pub(crate) home_dir: Option<PathBuf>,
pub(crate) shell: Option<PathBuf>,
}
impl CreateUserAndGroup {
pub fn new(
name: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Automatic)
}
pub fn new_with_id(
name: String,
id: u32,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Id(id))
}
pub fn new_with_uid_gid(
name: String,
uid: u32,
gid: u32,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::UidGid((uid, gid)))
}
pub fn new_with_uid_groupname(
name: String,
uid: u32,
groupname: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
validate_name_strict(&groupname)?;
Self::impl_new(
name,
gecos,
home_dir,
shell,
IdOrPath::UidGroupname((uid, groupname)),
)
}
pub fn new_with_path(
name: String,
path: PathBuf,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
) -> Result<Self, SdError> {
Self::impl_new(name, gecos, home_dir, shell, IdOrPath::Path(path))
}
pub(crate) fn impl_new(
name: String,
gecos: String,
home_dir: Option<PathBuf>,
shell: Option<PathBuf>,
id: IdOrPath,
) -> Result<Self, SdError> {
validate_name_strict(&name)?;
Ok(Self {
name,
id,
gecos,
home_dir,
shell,
})
}
pub fn type_signature(&self) -> &str {
"u"
}
pub fn name(&self) -> &str {
&self.name
}
pub fn has_dynamic_ids(&self) -> bool {
matches!(self.id, IdOrPath::Automatic)
}
pub fn static_uid(&self) -> Option<u32> {
match self.id {
IdOrPath::Id(n) => Some(n),
IdOrPath::UidGid((n, _)) => Some(n),
IdOrPath::UidGroupname((n, _)) => Some(n),
_ => None,
}
}
pub fn static_gid(&self) -> Option<u32> {
match self.id {
IdOrPath::Id(n) => Some(n),
IdOrPath::UidGid((_, n)) => Some(n),
_ => None,
}
}
pub(crate) fn into_sysusers_entry(self) -> SysusersEntry {
SysusersEntry::CreateUserAndGroup(self)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum IdOrPath {
Id(u32),
UidGid((u32, u32)),
UidGroupname((u32, String)),
Path(PathBuf),
Automatic,
}
impl FromStr for IdOrPath {
type Err = SdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value == "-" {
return Ok(IdOrPath::Automatic);
}
if value.starts_with('/') {
return Ok(IdOrPath::Path(value.into()));
}
if let Ok(single_id) = value.parse() {
return Ok(IdOrPath::Id(single_id));
}
let tokens: Vec<_> = value.split(':').filter(|s| !s.is_empty()).collect();
if tokens.len() == 2 {
let uid: u32 = tokens[0].parse().context("invalid user id")?;
let id = match tokens[1].parse() {
Ok(gid) => IdOrPath::UidGid((uid, gid)),
_ => {
let groupname = tokens[1].to_string();
validate_name_strict(&groupname).context("name failed validation")?;
IdOrPath::UidGroupname((uid, groupname))
}
};
return Ok(id);
}
Err(format!("unexpected user ID '{}'", value).into())
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum GidOrPath {
Gid(u32),
Path(PathBuf),
Automatic,
}
impl FromStr for GidOrPath {
type Err = SdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value == "-" {
return Ok(GidOrPath::Automatic);
}
if value.starts_with('/') {
return Ok(GidOrPath::Path(value.into()));
}
if let Ok(parsed_gid) = value.parse() {
return Ok(GidOrPath::Gid(parsed_gid));
}
Err(format!("unexpected group ID '{}'", value).into())
}
}
pub fn validate_name_strict(input: &str) -> Result<(), SdError> {
if input.is_empty() {
return Err(SdError::from("empty name"));
}
if input.len() > 31 {
let err_msg = format!(
"overlong sysusers name '{}' (more than 31 characters)",
input
);
return Err(SdError::from(err_msg));
}
for (index, ch) in input.char_indices() {
if index == 0 {
if !(ch.is_ascii_alphabetic() || ch == '_') {
let err_msg = format!(
"invalid starting character '{}' in sysusers name '{}'",
ch, input
);
return Err(SdError::from(err_msg));
}
} else if !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-') {
let err_msg = format!("invalid character '{}' in sysusers name '{}'", ch, input);
return Err(SdError::from(err_msg));
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_validate_name_strict() {
let err_cases = vec!["-foo", "10bar", "42"];
for entry in err_cases {
validate_name_strict(entry).unwrap_err();
}
let ok_cases = vec!["_authd", "httpd"];
for entry in ok_cases {
validate_name_strict(entry).unwrap();
}
}
}