bootc_sysusers/nameservice/
group.rs

1//! Helpers for [user passwd file](https://man7.org/linux/man-pages/man5/passwd.5.html).
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use anyhow::{Context, Result, anyhow};
5use cap_std_ext::cap_std::fs::Dir;
6use std::io::{BufRead, BufReader, Write};
7
8// Entry from group file.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub(crate) struct GroupEntry {
11    pub(crate) name: String,
12    pub(crate) passwd: String,
13    pub(crate) gid: u32,
14    pub(crate) users: Vec<String>,
15}
16
17impl GroupEntry {
18    /// Parse a single group entry.
19    pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
20        let mut parts = s.as_ref().splitn(4, ':');
21        let entry = Self {
22            name: parts.next()?.to_string(),
23            passwd: parts.next()?.to_string(),
24            gid: parts.next().and_then(|s| s.parse().ok())?,
25            users: {
26                let users = parts.next()?;
27                users.split(',').map(String::from).collect()
28            },
29        };
30        Some(entry)
31    }
32
33    /// Serialize entry to writer, as a group line.
34    pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
35        let users: String = self.users.join(",");
36        std::writeln!(
37            writer,
38            "{}:{}:{}:{}",
39            self.name,
40            self.passwd,
41            self.gid,
42            users,
43        )
44        .with_context(|| "failed to write passwd entry")
45    }
46}
47
48pub(crate) fn parse_group_content(content: impl BufRead) -> Result<Vec<GroupEntry>> {
49    let mut groups = vec![];
50    for (line_num, line) in content.lines().enumerate() {
51        let input =
52            line.with_context(|| format!("failed to read group entry at line {line_num}"))?;
53
54        // Skip empty and comment lines
55        if input.is_empty() || input.starts_with('#') {
56            continue;
57        }
58        // Skip NSS compat lines, see "Compatibility mode" in
59        // https://man7.org/linux/man-pages/man5/nsswitch.conf.5.html
60        if input.starts_with('+') || input.starts_with('-') {
61            continue;
62        }
63
64        let entry = GroupEntry::parse_line(&input).ok_or_else(|| {
65            anyhow!(
66                "failed to parse group entry at line {}, content: {}",
67                line_num,
68                &input
69            )
70        })?;
71        groups.push(entry);
72    }
73    Ok(groups)
74}
75
76pub(crate) fn load_etc_group(rootfs: &Dir) -> Result<Vec<GroupEntry>> {
77    let r = rootfs.open("etc/group").map(BufReader::new)?;
78    parse_group_content(r)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::io::Cursor;
85
86    fn mock_group_entry() -> GroupEntry {
87        GroupEntry {
88            name: "staff".to_string(),
89            passwd: "x".to_string(),
90            gid: 50,
91            users: vec!["operator".to_string()],
92        }
93    }
94
95    #[test]
96    fn test_parse_lines() {
97        let content = r#"
98+groupA
99-groupB
100
101root:x:0:
102daemon:x:1:
103bin:x:2:
104sys:x:3:
105adm:x:4:
106www-data:x:33:
107backup:x:34:
108operator:x:37:
109
110# Dummy comment
111staff:x:50:operator
112
113+
114"#;
115
116        let input = Cursor::new(content);
117        let groups = parse_group_content(input).unwrap();
118        assert_eq!(groups.len(), 9);
119        assert_eq!(groups[8], mock_group_entry());
120    }
121
122    #[test]
123    fn test_write_entry() {
124        let entry = mock_group_entry();
125        let expected = b"staff:x:50:operator\n";
126        let mut buf = Vec::new();
127        entry.to_writer(&mut buf).unwrap();
128        assert_eq!(&buf, expected);
129    }
130}