#![warn( clippy::all, /* clippy::restriction,*/ clippy::pedantic, clippy::nursery, clippy::cargo )] #![allow(clippy::non_ascii_literal)] use crate::userlib_error::UserLibError; use std::cmp::Eq; use std::convert::TryFrom; use std::fmt::{self, Display}; /// The username of the current user /// /// When done the validity will automatically be checked in the `trait TryFrom`. /// /// In the future some extra fields might be added. #[derive(Debug, PartialEq, Eq)] pub struct Username<'a> { /// The username value username: &'a str, } #[derive(Debug, PartialEq, Eq)] pub struct Password<'a> { password: &'a str, } #[derive(Debug, PartialEq, Eq)] pub struct Uid { uid: u32, } #[derive(Debug, PartialEq, Eq)] pub struct Gid { gid: u32, } #[derive(Debug, PartialEq, Eq)] pub enum Gecos<'a> { Detail { full_name: &'a str, room: &'a str, phone_work: &'a str, phone_home: &'a str, other: Option>, }, Simple { comment: &'a str, }, } #[derive(Debug, PartialEq, Eq)] pub struct HomeDir<'a> { dir: &'a str, } #[derive(Debug, PartialEq, Eq)] pub struct ShellDir<'a> { shell: &'a str, } /// A record in the user database `/etc/passwd`. #[derive(Debug, PartialEq, Eq)] pub struct Passwd<'a> { username: Username<'a>, /* Username. */ password: Password<'a>, /* Hashed passphrase, if shadow database not in use (see shadow.h). */ uid: Uid, /* User ID. */ gid: Gid, /* Group ID. */ gecos: Gecos<'a>, /* Real name. */ home_dir: HomeDir<'a>, /* Home directory. */ shell_dir: ShellDir<'a>, /* Shell program. */ } impl<'a> Passwd<'a> { /// Parse a line formatted like one in `/etc/passwd` and construct a matching `Passwd` instance /// /// # Example /// ``` /// let pwd = adduser::passwd::Passwd::new_from_string( /// "testuser:testpassword:1001:1001:full Name,,,,:/home/test:/bin/test" /// ).unwrap(); /// assert_eq!(pwd.get_username(), "testuser"); /// ``` /// /// # Errors /// When parsing fails this function returns a `UserLibError::Message` containing some information as to why the function failed. pub fn new_from_string(line: &'a str) -> Result { let elements: Vec<&str> = line.split(':').collect(); if elements.len() == 7 { Ok(Passwd { username: Username::try_from(*elements.get(0).unwrap())?, password: Password::try_from(*elements.get(1).unwrap())?, uid: Uid::try_from(*elements.get(2).unwrap())?, gid: Gid::try_from(*elements.get(3).unwrap())?, gecos: Gecos::try_from(*elements.get(4).unwrap())?, home_dir: HomeDir::try_from(*elements.get(5).unwrap())?, shell_dir: ShellDir::try_from(*elements.get(6).unwrap())?, }) } else { Err("Failed to parse: not enough elements".into()) } } #[must_use] pub const fn get_username(&self) -> &'a str { self.username.username } #[must_use] pub const fn get_password(&self) -> &'a str { self.password.password } #[must_use] pub const fn get_uid(&self) -> u32 { self.uid.uid } #[must_use] pub const fn get_gid(&self) -> u32 { self.gid.gid } #[must_use] pub const fn get_comment(&self) -> &Gecos { &self.gecos } #[must_use] pub const fn get_home_dir(&self) -> &'a str { self.home_dir.dir } #[must_use] pub const fn get_shell_dir(&self) -> &'a str { self.shell_dir.shell } } impl<'a> Gecos<'a> { #[must_use] pub const fn get_comment(&'a self) -> Option<&'a str> { match *self { Gecos::Simple { comment, .. } => Some(comment), Gecos::Detail { .. } => None, } } #[must_use] pub const fn get_full_name(&'a self) -> Option<&'a str> { match *self { Gecos::Simple { .. } => None, Gecos::Detail { full_name, .. } => { if full_name.is_empty() { None } else { Some(full_name) } } } } #[must_use] pub const fn get_room(&'a self) -> Option<&'a str> { match *self { Gecos::Simple { .. } => None, Gecos::Detail { room, .. } => { if room.is_empty() { None } else { Some(room) } } } } #[must_use] pub const fn get_phone_work(&'a self) -> Option<&'a str> { match *self { Gecos::Simple { .. } => None, Gecos::Detail { phone_work, .. } => { if phone_work.is_empty() { None } else { Some(phone_work) } } } } #[must_use] pub const fn get_phone_home(&'a self) -> Option<&'a str> { match *self { Gecos::Simple { .. } => None, Gecos::Detail { phone_home, .. } => { if phone_home.is_empty() { None } else { Some(phone_home) } } } } #[must_use] pub const fn get_other(&'a self) -> Option<&Vec<&'a str>> { match self { Gecos::Simple { .. } => None, Gecos::Detail { other, .. } => match other { None => None, Some(comments) => Some(comments), }, } } } impl Default for Passwd<'_> { fn default() -> Self { Passwd { username: Username { username: "defaultuser", }, password: Password { password: "notencrypted", }, uid: Uid { uid: 1001 }, gid: Gid { gid: 1001 }, gecos: Gecos::Simple { comment: "gecos default comment", }, home_dir: HomeDir { dir: "/home/default", }, shell_dir: ShellDir { shell: "/bin/bash" }, } } } impl Display for Passwd<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}:{}:{}:{}:{}:{}:{}", self.username, self.password, self.uid, self.gid, self.gecos, self.home_dir, self.shell_dir ) } } impl Display for Username<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.username,) } } impl<'a> TryFrom<&'a str> for Username<'a> { type Error = UserLibError; fn try_from(source: &'a str) -> std::result::Result { Ok(Self { username: source }) } } impl Display for Password<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.password,) } } impl<'a> TryFrom<&'a str> for Password<'a> { type Error = UserLibError; fn try_from(source: &'a str) -> std::result::Result { Ok(Self { password: source }) } } impl Display for Uid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.uid,) } } impl TryFrom<&str> for Uid { type Error = UserLibError; fn try_from(source: &str) -> std::result::Result { Ok(Self { uid: source.parse::().unwrap(), }) } } impl Display for Gid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.gid,) } } impl TryFrom<&str> for Gid { type Error = UserLibError; fn try_from(source: &str) -> std::result::Result { Ok(Self { gid: source.parse::().unwrap(), }) } } impl Display for Gecos<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self { Gecos::Simple { comment } => write!(f, "{}", comment), Gecos::Detail { full_name, room, phone_work, phone_home, other, } => write!( f, "{},{},{},{}{}", full_name, room, phone_work, phone_home, match other { None => "".to_string(), Some(cont) => format!(",{}", cont.join(",")), } ), } } } impl<'a> TryFrom<&'a str> for Gecos<'a> { type Error = UserLibError; fn try_from(source: &'a str) -> std::result::Result { let vals: Vec<&str> = source.split(',').collect(); if vals.len() > 3 { Ok(Gecos::Detail { full_name: vals[0], room: vals[1], phone_work: vals[2], phone_home: vals[3], other: if vals.len() == 4 { None } else { Some(vals[4..].to_vec()) }, }) } else if vals.len() == 1 { Ok(Gecos::Simple { comment: vals.get(0).unwrap(), }) } else { panic!(format!("Could not parse this string: {}", source)) } } } impl Display for HomeDir<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.dir,) } } impl<'a> TryFrom<&'a str> for HomeDir<'a> { type Error = UserLibError; fn try_from(source: &'a str) -> std::result::Result { Ok(Self { dir: source }) } } impl Display for ShellDir<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.shell,) } } impl<'a> TryFrom<&'a str> for ShellDir<'a> { type Error = UserLibError; fn try_from(source: &'a str) -> std::result::Result { Ok(Self { shell: source }) } } // Tests ---------------------------------------------------------------------- #[test] fn test_default_user() { // Check if a user can be created. let pwd = Passwd::default(); assert_eq!(pwd.username.username, "defaultuser"); assert_eq!(pwd.home_dir.dir, "/home/default"); assert_eq!(pwd.uid.uid, 1001); } #[test] fn test_parse_gecos() { // test if the Gecos field can be parsed and the resulting struct is populated correctly. let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com"; let gcsimple = "A böring comment →"; let gc_no_other: &str = "systemd Network Management,,,"; let res_detail = Gecos::try_from(gcdetail).unwrap(); let res_simple = Gecos::try_from(gcsimple).unwrap(); let res_no_other = Gecos::try_from(gc_no_other).unwrap(); match res_simple { Gecos::Simple { comment } => assert_eq!(comment, "A böring comment →"), _ => unreachable!(), } match res_detail { Gecos::Detail { full_name, room, phone_work, phone_home, other, } => { assert_eq!(full_name, "Full Name"); assert_eq!(room, "504"); assert_eq!(phone_work, "11345342"); assert_eq!(phone_home, "ä1-2312"); assert_eq!(other.unwrap()[0], "myemail@test.com"); } _ => unreachable!(), } match res_no_other { Gecos::Detail { full_name, room, phone_work, phone_home, other, } => { assert_eq!(full_name, "systemd Network Management"); assert_eq!(room, ""); assert_eq!(phone_work, ""); assert_eq!(phone_home, ""); assert_eq!(other, None); } _ => unreachable!(), } } #[test] fn test_gecos_getters() { // test if the Gecos field can be parsed and the resulting struct is populated correctly. let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com"; let gcsimple = "A böring comment →"; let gc_no_other: &str = "systemd Network Management,,,"; let res_detail = Gecos::try_from(gcdetail).unwrap(); let res_simple = Gecos::try_from(gcsimple).unwrap(); let res_no_other = Gecos::try_from(gc_no_other).unwrap(); assert_eq!(res_simple.get_comment(), Some("A böring comment →")); assert_eq!(res_detail.get_comment(), None); println!("{:?}", res_detail); assert_eq!(res_detail.get_full_name(), Some("Full Name")); assert_eq!(res_detail.get_room(), Some("504")); assert_eq!(res_detail.get_phone_work(), Some("11345342")); assert_eq!(res_detail.get_phone_home(), Some("ä1-2312")); assert_eq!(res_detail.get_other(), Some(&vec!["myemail@test.com"])); assert_eq!( res_no_other.get_full_name(), Some("systemd Network Management") ); assert_eq!(res_no_other.get_room(), None); assert_eq!(res_no_other.get_phone_work(), None); assert_eq!(res_no_other.get_phone_home(), None); assert_eq!(res_no_other.get_other(), None); } #[test] fn test_new_from_string() { // Test if a single line can be parsed and if the resulting struct is populated correctly. let fail = Passwd::new_from_string("").err().unwrap(); assert_eq!( fail, UserLibError::Message("Failed to parse: not enough elements".into()) ); let pwd = Passwd::new_from_string("testuser:testpassword:1001:1001:testcomment:/home/test:/bin/test") .unwrap(); let pwd2 = Passwd::new_from_string("testuser:testpassword:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test") .unwrap(); assert_eq!(pwd.username.username, "testuser"); assert_eq!(pwd.home_dir.dir, "/home/test"); assert_eq!(pwd.uid.uid, 1001); match pwd.gecos { Gecos::Simple { comment } => assert_eq!(comment, "testcomment"), _ => unreachable!(), } match pwd2.gecos { Gecos::Detail { full_name, room, phone_work, phone_home, other, } => { assert_eq!(full_name, "full Name"); assert_eq!(room, "004"); assert_eq!(phone_work, "000342"); assert_eq!(phone_home, "001-2312"); assert_eq!(other.unwrap()[0], "myemail@test.com"); } _ => unreachable!(), } } #[test] fn test_parse_passwd() { // Test wether the passwd file can be parsed and recreated without throwing an exception use std::fs::File; use std::io::{prelude::*, BufReader}; let file = File::open("/etc/passwd").unwrap(); let reader = BufReader::new(file); for line in reader.lines() { let lineorig: String = line.unwrap(); let linecopy = lineorig.clone(); let pass_struc = Passwd::new_from_string(&linecopy).unwrap(); assert_eq!( // ignoring the numbers of `,` since the implementation does not (yet) reproduce a missing comment field. lineorig, format!("{}", pass_struc) ); } }