Skip to main content

cmdline/
cmdline.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Error;
6use std::borrow::Cow;
7use std::fmt::Write;
8
9/// Parses command-line arguments starting with "userboot", passing each key-value pair
10/// to `parse_option` and logging recognized options or warnings.
11pub 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/// Information about the program to boot next.
44#[derive(Default)]
45pub struct ProgramInfo {
46    /// Prefix directory path under bootfs.
47    pub root: String,
48    /// Next command line arguments (split by +).
49    pub next: String,
50}
51
52impl ProgramInfo {
53    /// Returns the program name and target path of the next command.
54    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}