composefs_boot/
os_release.rs1use std::collections::HashMap;
9
10fn dequote(value: &str) -> Option<String> {
15 let mut result = String::new();
17 let mut iter = value.trim().chars();
18
19 while let Some(c) = iter.next() {
22 match c {
23 '"' => loop {
24 result.push(match iter.next()? {
25 '\\' => iter.next()?,
27 '"' => break,
28 other => other,
29 });
30 },
31
32 '\'' => loop {
33 result.push(match iter.next()? {
34 '\'' => break,
35 other => other,
36 });
37 },
38
39 other => result.push(other),
42 }
43 }
44
45 Some(result)
46}
47
48#[derive(Debug)]
53pub struct OsReleaseInfo<'a> {
54 map: HashMap<&'a str, &'a str>,
55}
56
57impl<'a> OsReleaseInfo<'a> {
58 pub fn parse(content: &'a str) -> Self {
60 let map = HashMap::from_iter(
61 content
62 .lines()
63 .filter(|line| !line.trim().starts_with('#'))
64 .filter_map(|line| line.split_once('=')),
65 );
66 Self { map }
67 }
68
69 pub fn get_value(&self, keys: &[&str]) -> Option<String> {
72 keys.iter()
73 .find_map(|key| self.map.get(key).and_then(|v| dequote(v)))
74 }
75
76 pub fn get_pretty_name(&self) -> Option<String> {
78 self.get_value(&["PRETTY_NAME", "NAME", "ID"])
79 }
80
81 pub fn get_version(&self) -> Option<String> {
83 self.get_value(&["VERSION_ID", "VERSION"])
84 }
85
86 pub fn get_boot_label(&self) -> Option<String> {
90 let mut result = self.get_pretty_name()?;
91 if let Some(version) = self.get_version() {
92 result.push_str(&format!(" {version}"));
93 }
94 Some(result)
95 }
96}
97
98#[cfg(test)]
99mod test {
100 use similar_asserts::assert_eq;
101
102 use super::*;
103
104 #[test]
105 fn test_dequote() {
106 let cases = r##"
107
108 We encode the testcases inside of a custom string format to give
109 us more flexibility and less visual noise. Lines with 4 pipes
110 are successful testcases (left is quoted, right is unquoted):
111
112 |"example"| |example|
113
114 and lines with 2 pipes are failing testcases:
115
116 |"broken example|
117
118 Lines with no pipes are ignored as comments. Now, the cases:
119
120 || || Empty is empty...
121 |""| ||
122 |''| ||
123 |""''""| ||
124
125 Unquoted stuff
126
127 |hello| |hello|
128 |1234| |1234|
129 |\\\\| |\\\\| ...this is non-POSIX...
130 |\$\`\\| |\$\`\\| ...this too...
131
132 Double quotes
133
134 |"closed"| |closed|
135 |"closed\\"| |closed\|
136 |"a"| |a|
137 |" "| | |
138 |"\""| |"|
139 |"\\"| |\|
140 |"\$5"| |$5|
141 |"$5"| |$5| non-POSIX
142 |"\`tick\`"| |`tick`|
143 |"`tick`"| |`tick`| non-POSIX
144
145 |"\'"| |'| non-POSIX
146 |"\'"| |'| non-POSIX
147
148 ...failures...
149 |"not closed|
150 |"not closed\"|
151 |"|
152 |"\\|
153 |"\"|
154
155 Single quotes
156
157 |'a'| |a|
158 |' '| | |
159 |'\'| |\|
160 |'\$'| |\$|
161 |'closed\'| |closed\|
162
163 ...failures...
164 |'| not closed
165 |'not closed|
166 |'\''| this is '\' + a second unclosed quote '
167
168 "##;
169
170 for case in cases.lines() {
171 match case.split('|').collect::<Vec<&str>>()[..] {
172 [_comment] => {}
173 [_, quoted, _, result, _] => assert_eq!(dequote(quoted).as_deref(), Some(result)),
174 [_, quoted, _] => assert_eq!(dequote(quoted), None),
175 _ => unreachable!("Invalid test line {case:?}"),
176 }
177 }
178 }
179
180 #[test]
181 fn test_fallbacks() {
182 let cases = [
183 (
184 r#"
185PRETTY_NAME='prettyOS'
186VERSION_ID="Rocky Racoon"
187VERSION=42
188ID=pretty-os
189"#,
190 "prettyOS Rocky Racoon",
191 ),
192 (
193 r#"
194PRETTY_NAME='prettyOS
195VERSION_ID="Rocky Racoon"
196VERSION=42
197ID=pretty-os
198"#,
199 "pretty-os Rocky Racoon",
200 ),
201 (
202 r#"
203PRETTY_NAME='prettyOS
204VERSION=42
205ID=pretty-os
206"#,
207 "pretty-os 42",
208 ),
209 (
210 r#"
211PRETTY_NAME='prettyOS
212VERSION=42
213ID=pretty-os
214"#,
215 "pretty-os 42",
216 ),
217 (
218 r#"
219PRETTY_NAME='prettyOS'
220ID=pretty-os
221"#,
222 "prettyOS",
223 ),
224 (
225 r#"
226ID=pretty-os
227"#,
228 "pretty-os",
229 ),
230 ];
231
232 for (osrel, label) in cases {
233 let info = OsReleaseInfo::parse(osrel);
234 assert_eq!(info.get_boot_label().unwrap(), label);
235 }
236 }
237}