use crate::Query;
use serde::{Deserialize, Serialize};
#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct User {
pub name: String,
pub uid: u64,
pub gid: u64,
pub comment: String,
pub home: String,
pub shell: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
Format,
Numeric(std::num::ParseIntError),
}
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{name}::{uid}:{gid}::{home}:{shell}",
name = self.name,
uid = self.uid,
gid = self.gid,
home = self.home,
shell = self.shell
)
}
}
#[derive(Clone, PartialEq, Debug)]
struct UserQuery(UserQueryBuilder);
#[derive(Default, Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct UserQueryBuilder {
pub name: Option<String>,
pub uid: Option<u64>,
pub gid: Option<u64>,
pub comment: Option<String>,
pub home: Option<String>,
pub shell: Option<String>,
}
#[allow(dead_code)]
impl UserQueryBuilder {
pub const fn new() -> Self {
UserQueryBuilder {
name: None,
uid: None,
gid: None,
comment: None,
home: None,
shell: None,
}
}
pub fn build(self) -> Option<impl Query<User>> {
if (self.name.is_none() && self.uid.is_none() && self.gid.is_none())
&& (self.comment.is_none() && self.home.is_none() && self.shell.is_none())
{
None
} else {
Some(UserQuery(self))
}
}
pub fn with_name(self, name: &str) -> Self {
Self {
name: Some(name.to_owned()),
..self
}
}
pub fn with_uid(self, uid: u64) -> Self {
Self { uid: Some(uid), ..self }
}
pub fn with_gid(self, gid: u64) -> Self {
Self { gid: Some(gid), ..self }
}
pub fn with_comment(self, comment: &str) -> Self {
Self {
comment: Some(comment.to_owned()),
..self
}
}
pub fn with_home(self, home: &str) -> Self {
Self {
home: Some(home.to_owned()),
..self
}
}
pub fn with_shell(self, shell: &str) -> Self {
Self {
shell: Some(shell.to_owned()),
..self
}
}
}
fn none_or_eq<T: PartialEq<Q>, Q>(a: &Option<T>, b: &Q) -> bool {
match a {
Some(ref a) => a == b,
None => true,
}
}
impl Query<User> for UserQuery {
fn is_match(&self, user: &User) -> bool {
none_or_eq(&self.0.name, &user.name)
&& none_or_eq(&self.0.uid, &user.uid)
&& none_or_eq(&self.0.gid, &user.gid)
&& none_or_eq(&self.0.comment, &user.comment)
&& none_or_eq(&self.0.home, &user.home)
&& none_or_eq(&self.0.shell, &user.shell)
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ParseError::Format => write!(f, "bad format; a passwd entry must be exactly seven colon-separted entries: \"$USERNAME:PASSWORD:UID:GID:GECOS:HOMESHELL\""),
ParseError::Numeric(err) => write!(f, "bad format: could not parse GID or UID: {}", err),
}
}
}
impl From<std::num::ParseIntError> for ParseError {
fn from(err: std::num::ParseIntError) -> Self {
ParseError::Numeric(err)
}
}
impl std::error::Error for ParseError {}
impl std::str::FromStr for User {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split(':');
match (
it.next(),
it.next(),
it.next(),
it.next(),
it.next(),
it.next(),
it.next(),
it.next(),
) {
(Some(name), _, Some(uid), Some(gid), _, Some(home), Some(shell), None) => Ok(User {
comment: "".to_string(),
name: name.to_string(),
uid: uid.parse()?,
gid: gid.parse()?,
home: home.to_string(),
shell: shell.to_string(),
}),
_ => Err(ParseError::Format),
}
}
}
#[test]
fn test_parse_and_display() {
let root = User {
name: "root".to_string(),
comment: "".to_string(),
uid: 0,
gid: 0,
home: "/root".to_string(),
shell: "bash".to_string(),
};
assert_eq!(root, format!("{}", root).parse::<User>().unwrap())
}