umanux/src/passwd.rs
2020-09-30 17:35:39 +02:00

525 lines
15 KiB
Rust

#![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<Vec<&'a str>>,
},
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<Self, UserLibError> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
Ok(Self {
uid: source.parse::<u32>().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<Self, Self::Error> {
Ok(Self {
gid: source.parse::<u32>().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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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)
);
}
}