bootc_sysusers/nameservice/
group.rs1use anyhow::{Context, Result, anyhow};
5use cap_std_ext::cap_std::fs::Dir;
6use std::io::{BufRead, BufReader, Write};
7
8#[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 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 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 if input.is_empty() || input.starts_with('#') {
56 continue;
57 }
58 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}