bootc_sysusers/nameservice/
passwd.rs1use anyhow::{Context, Result, anyhow};
5use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
6use std::io::{BufRead, BufReader, Write};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub(crate) struct PasswdEntry {
11 pub(crate) name: String,
12 pub(crate) passwd: String,
13 pub(crate) uid: u32,
14 pub(crate) gid: u32,
15 pub(crate) gecos: String,
16 pub(crate) home_dir: String,
17 pub(crate) shell: String,
18}
19
20impl PasswdEntry {
21 pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
23 let mut parts = s.as_ref().splitn(7, ':');
24 let entry = Self {
25 name: parts.next()?.to_string(),
26 passwd: parts.next()?.to_string(),
27 uid: parts.next().and_then(|s| s.parse().ok())?,
28 gid: parts.next().and_then(|s| s.parse().ok())?,
29 gecos: parts.next()?.to_string(),
30 home_dir: parts.next()?.to_string(),
31 shell: parts.next()?.to_string(),
32 };
33 Some(entry)
34 }
35
36 pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
38 std::writeln!(
39 writer,
40 "{}:{}:{}:{}:{}:{}:{}",
41 self.name,
42 self.passwd,
43 self.uid,
44 self.gid,
45 self.gecos,
46 self.home_dir,
47 self.shell
48 )
49 .with_context(|| "failed to write passwd entry")
50 }
51}
52
53pub(crate) fn parse_passwd_content(content: impl BufRead) -> Result<Vec<PasswdEntry>> {
54 let mut passwds = vec![];
55 for (line_num, line) in content.lines().enumerate() {
56 let input =
57 line.with_context(|| format!("failed to read passwd entry at line {line_num}"))?;
58
59 if input.is_empty() || input.starts_with('#') {
61 continue;
62 }
63 if input.starts_with('+') || input.starts_with('-') {
66 continue;
67 }
68
69 let entry = PasswdEntry::parse_line(&input).ok_or_else(|| {
70 anyhow!(
71 "failed to parse passwd entry at line {}, content: {}",
72 line_num,
73 &input
74 )
75 })?;
76 passwds.push(entry);
77 }
78 Ok(passwds)
79}
80
81pub(crate) fn load_etc_passwd(rootfs: &Dir) -> Result<Option<Vec<PasswdEntry>>> {
82 if let Some(r) = rootfs.open_optional("etc/passwd")? {
83 parse_passwd_content(BufReader::new(r)).map(Some)
84 } else {
85 Ok(None)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::io::Cursor;
93
94 fn mock_passwd_entry() -> PasswdEntry {
95 PasswdEntry {
96 name: "someuser".to_string(),
97 passwd: "x".to_string(),
98 uid: 1000,
99 gid: 1000,
100 gecos: "Foo BAR,,,".to_string(),
101 home_dir: "/home/foobar".to_string(),
102 shell: "/bin/bash".to_string(),
103 }
104 }
105
106 #[test]
107 fn test_parse_lines() {
108 let content = r#"
109root:x:0:0:root:/root:/bin/bash
110
111+userA
112-userB
113
114daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
115systemd-coredump:x:1:1:systemd Core Dumper:/:/usr/sbin/nologin
116
117+@groupA
118-@groupB
119
120# Dummy comment
121someuser:x:1000:1000:Foo BAR,,,:/home/foobar:/bin/bash
122
123+
124"#;
125
126 let input = Cursor::new(content);
127 let groups = parse_passwd_content(input).unwrap();
128 assert_eq!(groups.len(), 4);
129 assert_eq!(groups[3], mock_passwd_entry());
130 }
131
132 #[test]
133 fn test_write_entry() {
134 let entry = mock_passwd_entry();
135 let expected = b"someuser:x:1000:1000:Foo BAR,,,:/home/foobar:/bin/bash\n";
136 let mut buf = Vec::new();
137 entry.to_writer(&mut buf).unwrap();
138 assert_eq!(&buf, expected);
139 }
140}