bootc_sysusers/nameservice/
shadow.rs1use anyhow::{Context, Result, anyhow};
6use std::io::{BufRead, Write};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub(crate) struct ShadowEntry {
13 pub(crate) namp: String,
15 pub(crate) pwdp: String,
17 pub(crate) lstchg: Option<u32>,
19 pub(crate) min: Option<u32>,
21 pub(crate) max: Option<u32>,
23 pub(crate) warn: Option<u32>,
25 pub(crate) inact: Option<u32>,
27 pub(crate) expire: Option<u32>,
29 pub(crate) flag: String,
31}
32
33fn u32_or_none(value: &str) -> Result<Option<u32>, std::num::ParseIntError> {
34 if value.is_empty() {
35 Ok(None)
36 } else {
37 Ok(Some(value.parse()?))
38 }
39}
40
41fn number_or_empty(value: Option<u32>) -> String {
42 if let Some(number) = value {
43 format!("{number}")
44 } else {
45 "".to_string()
46 }
47}
48
49impl ShadowEntry {
50 pub fn parse_line(s: impl AsRef<str>) -> Option<Self> {
52 let mut parts = s.as_ref().splitn(9, ':');
53
54 let entry = Self {
55 namp: parts.next()?.to_string(),
56 pwdp: parts.next()?.to_string(),
57 lstchg: u32_or_none(parts.next()?).ok()?,
58 min: u32_or_none(parts.next()?).ok()?,
59 max: u32_or_none(parts.next()?).ok()?,
60 warn: u32_or_none(parts.next()?).ok()?,
61 inact: u32_or_none(parts.next()?).ok()?,
62 expire: u32_or_none(parts.next()?).ok()?,
63 flag: parts.next()?.to_string(),
64 };
65 Some(entry)
66 }
67
68 pub fn to_writer(&self, writer: &mut impl Write) -> Result<()> {
70 std::writeln!(
71 writer,
72 "{}:{}:{}:{}:{}:{}:{}:{}:{}",
73 self.namp,
74 self.pwdp,
75 number_or_empty(self.lstchg),
76 number_or_empty(self.min),
77 number_or_empty(self.max),
78 number_or_empty(self.warn),
79 number_or_empty(self.inact),
80 number_or_empty(self.expire),
81 self.flag
82 )
83 .with_context(|| "failed to write shadow entry")
84 }
85}
86
87pub(crate) fn parse_shadow_content(content: impl BufRead) -> Result<Vec<ShadowEntry>> {
88 let mut entries = vec![];
89 for (line_num, line) in content.lines().enumerate() {
90 let input =
91 line.with_context(|| format!("failed to read shadow entry at line {line_num}"))?;
92
93 if input.is_empty() || input.starts_with('#') {
95 continue;
96 }
97
98 let entry = ShadowEntry::parse_line(&input).ok_or_else(|| {
99 anyhow!(
100 "failed to parse shadow entry at line {}, content: {}",
101 line_num,
102 &input
103 )
104 })?;
105 entries.push(entry);
106 }
107 Ok(entries)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::io::Cursor;
114
115 fn salted() -> String {
116 let obfuscated = "24362473616c7453616c7453414c544e61436c247865354c5a47466c656b35334350467765327069495065536947414e6f596f696e5544755157307179645879766f594b566d4c3257524c71445a48586b626e706f4148714c3079616c6939344e526355527445616f51";
118 String::from_utf8(hex::decode(obfuscated).unwrap()).unwrap()
119 }
120
121 fn mock_shadow_entry() -> ShadowEntry {
122 ShadowEntry {
123 namp: "salty".to_string(),
124 pwdp: salted(),
125 lstchg: Some(18912),
126 min: Some(0),
127 max: Some(99999),
128 warn: Some(7),
129 inact: None,
130 expire: None,
131 flag: "".to_string(),
132 }
133 }
134
135 #[test]
136 fn test_parse_lines() {
137 let salted = salted();
138 let content = format!(
139 r#"
140root:*:18912:0:99999:7:::
141daemon:*:18474:0:99999:7:::
142
143salty:{salted}:18912:0:99999:7:::
144
145# Dummy comment
146systemd-coredump:!!:::::::
147systemd-resolve:!!:::::::
148rngd:!!:::::::
149"#
150 );
151
152 let input = Cursor::new(content);
153 let entries = parse_shadow_content(input).unwrap();
154 assert_eq!(entries.len(), 6);
155 assert_eq!(entries[2], mock_shadow_entry());
156 }
157
158 #[test]
159 fn test_write_entry() {
160 let entry = mock_shadow_entry();
161 let expected = format!("salty:{}:18912:0:99999:7:::\n", salted());
162 let mut buf = Vec::new();
163 entry.to_writer(&mut buf).unwrap();
164 assert_eq!(&buf, expected.as_bytes());
165 }
166}