bootc_sysusers/nameservice/
gshadow.rs1use anyhow::{Context, Result, anyhow};
5use std::io::{BufRead, Write};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct GshadowEntry {
11 pub name: String,
13 pub password: String,
15 pub admins: String,
17 pub members: String,
19}
20
21impl GshadowEntry {
22 pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
24 let mut parts = s.as_ref().splitn(4, ':');
25 let entry = Self {
26 name: parts.next()?.to_string(),
27 password: parts.next()?.to_string(),
28 admins: parts.next()?.to_string(),
29 members: parts.next()?.to_string(),
30 };
31 Some(entry)
32 }
33
34 pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
36 std::writeln!(
37 writer,
38 "{}:{}:{}:{}",
39 self.name,
40 self.password,
41 self.admins,
42 self.members,
43 )
44 .with_context(|| "failed to write gshadow entry")
45 }
46}
47
48pub fn parse_gshadow_content(content: impl BufRead) -> Result<Vec<GshadowEntry>> {
49 let mut entries = vec![];
50 for (line_num, line) in content.lines().enumerate() {
51 let input =
52 line.with_context(|| format!("failed to read gshadow entry at line {line_num}"))?;
53
54 if input.is_empty() || input.starts_with('#') {
56 continue;
57 }
58
59 let entry = GshadowEntry::parse_line(&input).ok_or_else(|| {
60 anyhow!(
61 "failed to parse gshadow entry at line {}, content: {}",
62 line_num,
63 &input
64 )
65 })?;
66 entries.push(entry);
67 }
68 Ok(entries)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::io::Cursor;
75
76 fn mock_gshadow_entry() -> GshadowEntry {
77 GshadowEntry {
78 name: "wheel".to_string(),
79 password: "!".to_string(),
80 admins: "admin1".to_string(),
81 members: "user1,user2".to_string(),
82 }
83 }
84
85 #[test]
86 fn test_parse_lines() {
87 let content = r#"
88root:*::
89daemon:*::
90
91# Dummy comment
92wheel:!:admin1:user1,user2
93plocate:!::
94"#;
95 let input = Cursor::new(content);
96 let entries = parse_gshadow_content(input).unwrap();
97 assert_eq!(entries.len(), 4);
98 assert_eq!(entries[2], mock_gshadow_entry());
99 }
100
101 #[test]
102 fn test_write_entry() {
103 let entry = mock_gshadow_entry();
104 let expected = b"wheel:!:admin1:user1,user2\n";
105 let mut buf = Vec::new();
106 entry.to_writer(&mut buf).unwrap();
107 assert_eq!(&buf, expected);
108 }
109
110 #[test]
111 fn test_duplicate_detection() {
112 let content = "plocate:!::\nplocate:!::\nwheel:!::\n";
113 let input = Cursor::new(content);
114 let entries = parse_gshadow_content(input).unwrap();
115 assert_eq!(entries.len(), 3);
116 let mut seen = std::collections::HashSet::new();
118 let has_dups = entries.iter().any(|e| !seen.insert(&e.name));
119 assert!(has_dups);
120 }
121}