1use anyhow::Error;
6use std::borrow::Cow;
7use std::fmt::Write;
8
9pub fn parse_cmdline<T, F, W>(
12 cmdline: &str,
13 log: &mut W,
14 opts: &mut T,
15 mut parse_option: F,
16) -> Result<(), Error>
17where
18 W: Write + ?Sized,
19 F: FnMut(&str, &str, &mut T) -> bool,
20{
21 for raw_opt in cmdline.split_whitespace() {
22 let opt = raw_opt.trim_matches('\0');
23 if !opt.starts_with("userboot") {
24 continue;
25 }
26
27 let (key, value) = match opt.split_once('=') {
28 Some((k, v)) => (k, v),
29 None => (opt, ""),
30 };
31
32 if !parse_option(key, value, opts) {
33 writeln!(log, "WARNING: unknown option {key} ignored")?;
34 } else if value.is_empty() {
35 writeln!(log, "OPTION {key}")?;
36 } else {
37 writeln!(log, "OPTION {key}={value}")?;
38 }
39 }
40 Ok(())
41}
42
43#[derive(Default)]
45pub struct ProgramInfo {
46 pub root: String,
48 pub next: String,
50}
51
52impl ProgramInfo {
53 pub fn filename(&self) -> (&str, Cow<'_, str>) {
55 let name = if let Some(pos) = self.next.find('+') { &self.next[..pos] } else { &self.next };
56 let path = if !self.root.is_empty() {
57 format!("{}/{}", self.root, name).into()
58 } else {
59 name.into()
60 };
61 (name, path)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use std::collections::HashMap;
69
70 #[test]
71 fn test_parse_cmdline_valid_options() {
72 let cmdline = "userboot.root=bootfs userboot.debugger non_userboot_option=123";
73 let mut log = String::new();
74 let mut parsed = HashMap::new();
75
76 let result = parse_cmdline(cmdline, &mut log, &mut parsed, |key, val, map| {
77 map.insert(key.to_string(), val.to_string());
78 true
79 });
80
81 assert!(result.is_ok());
82 assert_eq!(parsed.get("userboot.root"), Some(&"bootfs".to_string()));
83 assert_eq!(parsed.get("userboot.debugger"), Some(&"".to_string()));
84 assert!(!parsed.contains_key("non_userboot_option"));
85
86 let expected_log = "OPTION userboot.root=bootfs\nOPTION userboot.debugger\n";
87 assert_eq!(log, expected_log);
88 }
89
90 #[test]
91 fn test_parse_cmdline_unknown_option() {
92 let cmdline = "userboot.known=yes userboot.unknown=no";
93 let mut log = String::new();
94 let mut parsed = Vec::new();
95
96 let result = parse_cmdline(cmdline, &mut log, &mut parsed, |key, val, vec| {
97 if key == "userboot.known" {
98 vec.push((key.to_string(), val.to_string()));
99 true
100 } else {
101 false
102 }
103 });
104
105 assert!(result.is_ok());
106 assert_eq!(parsed, vec![("userboot.known".to_string(), "yes".to_string())]);
107
108 let expected_log =
109 "OPTION userboot.known=yes\nWARNING: unknown option userboot.unknown ignored\n";
110 assert_eq!(log, expected_log);
111 }
112
113 #[test]
114 fn test_parse_cmdline_null_bytes_and_whitespace() {
115 let cmdline = "\0\0userboot.foo=bar\0\0 \0userboot.flag\0 ";
116 let mut log = String::new();
117 let mut parsed = HashMap::new();
118
119 let result = parse_cmdline(cmdline, &mut log, &mut parsed, |key, val, map| {
120 map.insert(key.to_string(), val.to_string());
121 true
122 });
123
124 assert!(result.is_ok());
125 assert_eq!(parsed.get("userboot.foo"), Some(&"bar".to_string()));
126 assert_eq!(parsed.get("userboot.flag"), Some(&"".to_string()));
127
128 let expected_log = "OPTION userboot.foo=bar\nOPTION userboot.flag\n";
129 assert_eq!(log, expected_log);
130 }
131
132 #[test]
133 fn test_parse_cmdline_empty() {
134 let cmdline = " ";
135 let mut log = String::new();
136 let mut count = 0;
137
138 let result = parse_cmdline(cmdline, &mut log, &mut count, |_, _, cnt| {
139 *cnt += 1;
140 true
141 });
142
143 assert!(result.is_ok());
144 assert_eq!(count, 0);
145 assert!(log.is_empty());
146 }
147
148 #[test]
149 fn test_parse_cmdline_multiple_equals() {
150 let cmdline = "userboot.path=a=b=c";
151 let mut log = String::new();
152 let mut parsed = HashMap::new();
153
154 let result = parse_cmdline(cmdline, &mut log, &mut parsed, |key, val, map| {
155 map.insert(key.to_string(), val.to_string());
156 true
157 });
158
159 assert!(result.is_ok());
160 assert_eq!(parsed.get("userboot.path"), Some(&"a=b=c".to_string()));
161 assert_eq!(log, "OPTION userboot.path=a=b=c\n");
162 }
163
164 #[test]
165 fn test_program_info_filename() {
166 let info_with_plus =
167 ProgramInfo { root: "boot".to_string(), next: "bin/init+arg1+arg2".to_string() };
168 assert_eq!(info_with_plus.filename(), ("bin/init", Cow::Borrowed("boot/bin/init")));
169
170 let info_without_plus =
171 ProgramInfo { root: "boot".to_string(), next: "bin/init".to_string() };
172 assert_eq!(info_without_plus.filename(), ("bin/init", Cow::Borrowed("boot/bin/init")));
173
174 let info_empty = ProgramInfo::default();
175 assert_eq!(info_empty.filename(), ("", Cow::Borrowed("")));
176 }
177}