Skip to main content

bootc_sysusers/nameservice/
gshadow.rs

1//! Helpers for [GShadow file](https://man7.org/linux/man-pages/man5/gshadow.5.html).
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use anyhow::{Context, Result, anyhow};
5use std::io::{BufRead, Write};
6
7/// Entry from gshadow file.
8/// Format: name:password:admins:members
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct GshadowEntry {
11    /// group name
12    pub name: String,
13    /// encrypted password (or ! or empty)
14    pub password: String,
15    /// comma-separated list of group administrators
16    pub admins: String,
17    /// comma-separated list of group members
18    pub members: String,
19}
20
21impl GshadowEntry {
22    /// Parse a single gshadow entry.
23    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    /// Serialize entry to writer, as a gshadow line.
35    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        // Skip empty and comment lines
55        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        // Verify we can detect duplicates
117        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}