bootc_sysusers/nameservice/
passwd.rs

1//! Helpers for [password 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, dirext::CapStdExtDirExt};
6use std::io::{BufRead, BufReader, Write};
7
8// Entry from passwd file.
9#[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    /// Parse a single passwd entry.
22    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    /// Serialize entry to writer, as a passwd line.
37    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        // Skip empty and comment lines
60        if input.is_empty() || input.starts_with('#') {
61            continue;
62        }
63        // Skip NSS compat lines, see "Compatibility mode" in
64        // https://man7.org/linux/man-pages/man5/nsswitch.conf.5.html
65        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}