1use anyhow::{Context, Error, anyhow};
6use cm_types::Name;
7use fidl::endpoints::{ControlHandle as _, Responder as _};
8use fidl_fuchsia_boot as fboot;
9use fidl_fuchsia_io as fio;
10use fuchsia_fs::file;
11use fuchsia_fs::file::ReadError;
12use fuchsia_fs::node::OpenError;
13use fuchsia_zbi::{ZbiParser, ZbiResult, ZbiType};
14use futures::prelude::*;
15use log::info;
16use std::collections::HashMap;
17use std::collections::hash_map::Iter;
18use std::env;
19use std::sync::{Arc, LazyLock};
20use zx_status::Status;
21
22#[allow(dead_code)]
23static BOOT_ARGS_CAPABILITY_NAME: LazyLock<Name> =
24 LazyLock::new(|| "fuchsia.boot.Arguments".parse().unwrap());
25
26const BOOT_CONFIG_FILE: &str = "/boot/config/additional_boot_args";
27
28struct Env {
29 vars: HashMap<String, String>,
30}
31
32impl Env {
33 pub fn new() -> Self {
34 let mut map = HashMap::new();
40 for (k, v) in env::vars() {
41 map.insert(k, v);
42 }
43 Env { vars: map }
44 }
45
46 #[cfg(test)]
47 pub fn mock_new(map: HashMap<String, String>) -> Self {
48 Env { vars: map }
49 }
50}
51
52pub struct Arguments {
53 vars: HashMap<String, String>,
54}
55
56impl Arguments {
57 pub async fn new(parser: &mut Option<ZbiParser>) -> Result<Arc<Self>, Error> {
58 let (cmdline_args, image_args) = match parser {
59 Some(parser) => {
60 let cmdline_args = match parser.try_get_item(ZbiType::Cmdline.into_raw(), None) {
61 Ok(result) => {
62 let _ = parser.release_item(ZbiType::Cmdline);
63 Some(result)
64 }
65 Err(_) => None,
66 };
67
68 let image_args = match parser.try_get_item(ZbiType::ImageArgs.into_raw(), None) {
69 Ok(result) => {
70 let _ = parser.release_item(ZbiType::ImageArgs);
71 Some(result)
72 }
73 Err(_) => None,
74 };
75
76 (cmdline_args, image_args)
77 }
78 None => (None, None),
79 };
80
81 let config = match file::open_in_namespace(BOOT_CONFIG_FILE, fio::PERM_READABLE) {
84 Ok(config) => Some(config),
85 Err(OpenError::Namespace(Status::NOT_FOUND)) => None,
86 Err(err) => return Err(anyhow!("Failed to open {}: {}", BOOT_CONFIG_FILE, err)),
87 };
88
89 Arguments::new_from_sources(Env::new(), cmdline_args, image_args, config).await
90 }
91
92 async fn new_from_sources(
93 env: Env,
94 cmdline_args: Option<Vec<ZbiResult>>,
95 image_args: Option<Vec<ZbiResult>>,
96 config_file: Option<fio::FileProxy>,
97 ) -> Result<Arc<Self>, Error> {
98 let mut result = HashMap::new();
109 result.extend(env.vars);
110
111 if cmdline_args.is_some() {
112 for cmdline_arg_item in cmdline_args.unwrap() {
113 let cmdline_arg_str = std::str::from_utf8(&cmdline_arg_item.bytes)
114 .context("failed to parse ZbiType::Cmdline as utf8")?;
115 Arguments::parse_arguments(&mut result, cmdline_arg_str.to_string());
116 }
117 }
118
119 if image_args.is_some() {
120 for image_arg_item in image_args.unwrap() {
121 let image_arg_str = std::str::from_utf8(&image_arg_item.bytes)
122 .context("failed to parse ZbiType::ImageArgs as utf8")?;
123 Arguments::parse_legacy_arguments(&mut result, image_arg_str.to_string());
124 }
125 }
126
127 if config_file.is_some() {
128 match file::read_to_string(&config_file.unwrap()).await {
132 Ok(config) => Arguments::parse_legacy_arguments(&mut result, config),
133 Err(ReadError::Fidl(fidl::Error::ClientChannelClosed {
134 status: Status::NOT_FOUND,
135 ..
136 })) => (),
137 Err(ReadError::Fidl(fidl::Error::ClientChannelClosed {
138 status: Status::PEER_CLOSED,
139 ..
140 })) => (),
141 Err(err) => return Err(anyhow!("Failed to read {}: {}", BOOT_CONFIG_FILE, err)),
142 }
143 }
144
145 Ok(Arc::new(Self { vars: result }))
146 }
147
148 fn parse_arguments(parsed: &mut HashMap<String, String>, raw: String) {
150 let lines = raw.trim_end_matches(char::from(0)).split_whitespace().collect::<Vec<&str>>();
151 for line in lines {
152 let split = line.splitn(2, "=").collect::<Vec<&str>>();
153 if split.len() == 0 {
154 info!("[Arguments] Empty argument string after parsing, ignoring: {}", line);
155 continue;
156 }
157
158 if split[0].is_empty() {
159 info!("[Arguments] Argument name cannot be empty, ignoring: {}", line);
160 continue;
161 }
162
163 parsed.insert(
164 split[0].to_string(),
165 if split.len() == 1 { String::new() } else { split[1].to_string() },
166 );
167 }
168 }
169
170 fn parse_legacy_arguments(parsed: &mut HashMap<String, String>, raw: String) {
172 let lines = raw.trim_end_matches(char::from(0)).lines();
173 for line in lines {
174 let trimmed = line.trim_start().trim_end();
175
176 if trimmed.starts_with("#") {
177 continue;
179 }
180
181 if trimmed.contains(char::is_whitespace) {
182 info!("[Arguments] Argument contains unexpected spaces, ignoring: {}", trimmed);
185 continue;
186 }
187
188 let split = trimmed.splitn(2, "=").collect::<Vec<&str>>();
189 if split.len() == 0 {
190 info!("[Arguments] Empty argument string after parsing, ignoring: {}", trimmed);
191 continue;
192 }
193
194 if split[0].is_empty() {
195 info!("[Arguments] Argument name cannot be empty, ignoring: {}", trimmed);
196 continue;
197 }
198
199 parsed.insert(
200 split[0].to_string(),
201 if split.len() == 1 { String::new() } else { split[1].to_string() },
202 );
203 }
204 }
205
206 fn get_bool_arg(self: &Arc<Self>, name: String, default: bool) -> bool {
207 let mut ret = default;
208 if let Ok(val) = self.var(name) {
209 if val == "0" || val == "false" || val == "off" {
210 ret = false;
211 } else {
212 ret = true;
213 }
214 }
215 ret
216 }
217
218 fn var(&self, var: String) -> Result<&str, env::VarError> {
219 if let Some(v) = self.vars.get(&var) { Ok(&v) } else { Err(env::VarError::NotPresent) }
220 }
221
222 fn vars<'a>(&'a self) -> Iter<'_, String, String> {
223 self.vars.iter()
224 }
225
226 pub async fn serve(
227 self: Arc<Self>,
228 mut stream: fboot::ArgumentsRequestStream,
229 ) -> Result<(), Error> {
230 while let Some(req) = stream.try_next().await? {
231 match req {
232 fboot::ArgumentsRequest::GetString { key, responder } => match self.var(key) {
233 Ok(val) => responder.send(Some(val)),
234 _ => responder.send(None),
235 }?,
236 fboot::ArgumentsRequest::GetStrings { keys, responder } => {
237 let vec: Vec<_> =
238 keys.into_iter().map(|x| self.var(x).ok().map(String::from)).collect();
239 responder.send(&vec)?
240 }
241 fboot::ArgumentsRequest::GetBool { key, defaultval, responder } => {
242 responder.send(self.get_bool_arg(key, defaultval))?
243 }
244 fboot::ArgumentsRequest::GetBools { keys, responder } => {
245 let vec: Vec<_> = keys
246 .into_iter()
247 .map(|key| self.get_bool_arg(key.key, key.defaultval))
248 .collect();
249 responder.send(&vec)?
250 }
251 fboot::ArgumentsRequest::Collect { prefix, responder } => {
252 let vec: Vec<_> = self
253 .vars()
254 .filter(|(k, _)| k.starts_with(&prefix))
255 .map(|(k, v)| k.to_owned() + "=" + &v)
256 .collect();
257 if vec.len() > fboot::MAX_ARGS_VECTOR_LENGTH.into() {
258 log::warn!(
259 "[Arguments] Collect results count {} exceeded maximum of {}",
260 vec.len(),
261 fboot::MAX_ARGS_VECTOR_LENGTH
262 );
263 responder.control_handle().shutdown_with_epitaph(Status::INTERNAL);
264 } else {
265 responder.send(&vec)?
266 }
267 }
268 }
269 }
270 Ok(())
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use fuchsia_async as fasync;
278 use fuchsia_fs::directory;
279 use fuchsia_fs::file::{close, write};
280
281 fn serve_bootargs(args: Arc<Arguments>) -> Result<fboot::ArgumentsProxy, Error> {
282 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ArgumentsMarker>();
283 fasync::Task::local(
284 args.serve(stream)
285 .unwrap_or_else(|e| panic!("Error while serving arguments service: {}", e)),
286 )
287 .detach();
288 Ok(proxy)
289 }
290
291 #[fuchsia::test]
292 async fn malformed_argument_sources() {
293 let data = vec![0xfe];
295
296 let tempdir = tempfile::TempDir::new().unwrap();
297 let dir = directory::open_in_namespace(
298 tempdir.path().to_str().unwrap(),
299 fio::PERM_READABLE | fio::PERM_WRITABLE,
300 )
301 .unwrap();
302
303 let config =
304 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
305 .await
306 .unwrap();
307 write(&config, data.clone()).await.unwrap();
308
309 assert!(
311 Arguments::new_from_sources(Env::mock_new(HashMap::new()), None, None, Some(config))
312 .await
313 .is_err()
314 );
315
316 assert!(
318 Arguments::new_from_sources(
319 Env::mock_new(HashMap::new()),
320 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
321 None,
322 None
323 )
324 .await
325 .is_err()
326 );
327
328 assert!(
330 Arguments::new_from_sources(
331 Env::mock_new(HashMap::new()),
332 None,
333 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
334 None
335 )
336 .await
337 .is_err()
338 );
339 }
340
341 #[fuchsia::test]
342 async fn prioritized_argument_sources() {
343 let env = Env::mock_new(
345 [("arg1", "env1"), ("arg2", "env2"), ("arg3", "env3"), ("arg4", "env4")]
346 .iter()
347 .map(|(a, b)| (a.to_string(), b.to_string()))
348 .collect(),
349 );
350
351 let cmdline = vec![
354 ZbiResult { bytes: b"arg2=notthisone arg3=cmd3 arg4=cmd4".to_vec(), extra: 0 },
355 ZbiResult { bytes: b"arg2=cmd2".to_vec(), extra: 0 },
356 ];
357
358 let image_args = vec![ZbiResult { bytes: b"arg3=img3\narg4=img4".to_vec(), extra: 0 }];
360
361 let tempdir = tempfile::TempDir::new().unwrap();
362 let dir = directory::open_in_namespace(
363 tempdir.path().to_str().unwrap(),
364 fio::PERM_READABLE | fio::PERM_WRITABLE,
365 )
366 .unwrap();
367
368 let config =
371 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
372 .await
373 .unwrap();
374
375 write(&config, b"# Comment!\narg4=config4").await.unwrap();
377 close(config).await.unwrap();
378
379 let config = directory::open_file(&dir, "file", fio::PERM_READABLE).await.unwrap();
380
381 let args = Arguments::new_from_sources(env, Some(cmdline), Some(image_args), Some(config))
382 .await
383 .unwrap();
384 let proxy = serve_bootargs(args).unwrap();
385
386 let result = proxy.get_string("arg1").await.unwrap().unwrap();
387 assert_eq!(result, "env1");
388
389 let result = proxy.get_string("arg2").await.unwrap().unwrap();
390 assert_eq!(result, "cmd2");
391
392 let result = proxy.get_string("arg3").await.unwrap().unwrap();
393 assert_eq!(result, "img3");
394
395 let result = proxy.get_string("arg4").await.unwrap().unwrap();
396 assert_eq!(result, "config4");
397 }
398
399 #[fuchsia::test]
400 async fn parse_argument_string() {
401 let raw_arguments = "arg1=val1 arg3 arg4= =val2 arg5='abcd=defg'".to_string();
402 let expected = [("arg1", "val1"), ("arg3", ""), ("arg4", ""), ("arg5", "'abcd=defg'")]
403 .iter()
404 .map(|(a, b)| (a.to_string(), b.to_string()))
405 .collect();
406
407 let mut actual = HashMap::new();
408 Arguments::parse_arguments(&mut actual, raw_arguments);
409
410 assert_eq!(actual, expected);
411 }
412
413 #[fuchsia::test]
414 async fn parse_legacy_argument_string() {
415 let raw_arguments = concat!(
416 "arg1=val1\n",
417 "arg2=val2,val3\n",
418 "=AnInvalidEmptyArgumentName!\n",
419 "perfectlyValidEmptyValue=\n",
420 "justThisIsFineToo\n",
421 "arg3=these=are=all=the=val\n",
422 " spacesAtStart=areFineButRemoved\n",
423 "# This is a comment\n",
424 "arg4=begrudinglyAllowButTrimTrailingSpaces \n"
425 )
426 .to_string();
427 let expected = [
428 ("arg1", "val1"),
429 ("arg2", "val2,val3"),
430 ("perfectlyValidEmptyValue", ""),
431 ("justThisIsFineToo", ""),
432 ("arg3", "these=are=all=the=val"),
433 ("spacesAtStart", "areFineButRemoved"),
434 ("arg4", "begrudinglyAllowButTrimTrailingSpaces"),
435 ]
436 .iter()
437 .map(|(a, b)| (a.to_string(), b.to_string()))
438 .collect();
439
440 let mut actual = HashMap::new();
441 Arguments::parse_legacy_arguments(&mut actual, raw_arguments);
442
443 assert_eq!(actual, expected);
444 }
445
446 #[fuchsia::test]
447 async fn can_get_string() -> Result<(), Error> {
448 let vars: HashMap<String, String> =
450 [("test_arg_1", "hello"), ("test_arg_2", "another var"), ("empty.arg", "")]
451 .iter()
452 .map(|(a, b)| (a.to_string(), b.to_string()))
453 .collect();
454 let proxy = serve_bootargs(
455 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
456 )?;
457
458 let res = proxy.get_string("test_arg_1").await?;
459 assert_ne!(res, None);
460 assert_eq!(res.unwrap(), "hello");
461
462 let res = proxy.get_string("test_arg_2").await?;
463 assert_ne!(res, None);
464 assert_eq!(res.unwrap(), "another var");
465
466 let res = proxy.get_string("empty.arg").await?;
467 assert_ne!(res, None);
468 assert_eq!(res.unwrap(), "");
469
470 let res = proxy.get_string("does.not.exist").await?;
471 assert_eq!(res, None);
472 Ok(())
473 }
474
475 #[fuchsia::test]
476 async fn can_get_strings() -> Result<(), Error> {
477 let vars: HashMap<String, String> =
479 [("test_arg_1", "hello"), ("test_arg_2", "another var")]
480 .iter()
481 .map(|(a, b)| (a.to_string(), b.to_string()))
482 .collect();
483 let proxy = serve_bootargs(
484 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
485 )?;
486
487 let req = &["test_arg_1".to_owned(), "test_arg_2".to_owned(), "test_arg_3".to_owned()];
488 let res = proxy.get_strings(req).await?;
489 let panicker = || panic!("got None, expected Some(str)");
490 assert_eq!(res[0].as_ref().unwrap_or_else(panicker), "hello");
491 assert_eq!(res[1].as_ref().unwrap_or_else(panicker), "another var");
492 assert_eq!(res[2], None);
493 assert_eq!(res.len(), 3);
494
495 let res = proxy.get_strings(&[]).await?;
496 assert_eq!(res.len(), 0);
497 Ok(())
498 }
499
500 #[fuchsia::test]
501 async fn can_get_bool() -> Result<(), Error> {
502 let vars: HashMap<String, String> = [
503 ("zero", "0"),
504 ("not_true", "false"),
505 ("not_on", "off"),
506 ("empty_but_true", ""),
507 ("should_be_true", "hello there"),
508 ("still_true", "no"),
509 ]
510 .iter()
511 .map(|(a, b)| (a.to_string(), b.to_string()))
512 .collect();
513 let expected: Vec<(&str, bool, bool)> = vec![
515 ("zero", true, false),
517 ("zero", false, false),
518 ("not_true", false, false),
519 ("not_on", true, false),
520 ("empty_but_true", false, true),
522 ("should_be_true", false, true),
524 ("still_true", true, true),
525 ("not_specified", false, false),
527 ("not_specified", true, true),
528 ];
529 let proxy = serve_bootargs(
530 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
531 )?;
532
533 for (var, default, correct) in expected.iter() {
534 let res = proxy.get_bool(var, *default).await?;
535 assert_eq!(
536 res, *correct,
537 "expect get_bool({}, {}) = {} but got {}",
538 var, default, correct, res
539 );
540 }
541
542 Ok(())
543 }
544
545 #[fuchsia::test]
546 async fn can_get_bools() -> Result<(), Error> {
547 let vars: HashMap<String, String> = [
548 ("zero", "0"),
549 ("not_true", "false"),
550 ("not_on", "off"),
551 ("empty_but_true", ""),
552 ("should_be_true", "hello there"),
553 ("still_true", "no"),
554 ]
555 .iter()
556 .map(|(a, b)| (a.to_string(), b.to_string()))
557 .collect();
558 let expected: Vec<(&str, bool, bool)> = vec![
560 ("zero", true, false),
562 ("zero", false, false),
563 ("not_true", false, false),
564 ("not_on", true, false),
565 ("empty_but_true", false, true),
567 ("should_be_true", false, true),
569 ("still_true", true, true),
570 ("not_specified", false, false),
572 ("not_specified", true, true),
573 ];
574 let proxy = serve_bootargs(
575 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
576 )?;
577
578 let req: Vec<fboot::BoolPair> = expected
579 .iter()
580 .map(|(key, default, _expected)| fboot::BoolPair {
581 key: String::from(*key),
582 defaultval: *default,
583 })
584 .collect();
585 let mut cur = 0;
586 for val in proxy.get_bools(&req).await?.iter() {
587 assert_eq!(
588 *val, expected[cur].2,
589 "get_bools() index {} returned {} but want {}",
590 cur, val, expected[cur].2
591 );
592 cur += 1;
593 }
594 Ok(())
595 }
596
597 #[fuchsia::test]
598 async fn can_collect() -> Result<(), Error> {
599 let vars: HashMap<String, String> = [
600 ("test.value1", "3"),
601 ("test.value2", ""),
602 ("testing.value1", "hello"),
603 ("test.bool", "false"),
604 ("another_test.value1", ""),
605 ("armadillos", "off"),
606 ]
607 .iter()
608 .map(|(a, b)| (a.to_string(), b.to_string()))
609 .collect();
610 let proxy = serve_bootargs(
611 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
612 )?;
613
614 let res = proxy.collect("test.").await?;
615 let expected = vec!["test.value1=3", "test.value2=", "test.bool=false"];
616 for val in expected.iter() {
617 assert_eq!(
618 res.contains(&String::from(*val)),
619 true,
620 "collect() is missing expected value {}",
621 val
622 );
623 }
624 assert_eq!(res.len(), expected.len());
625
626 let res = proxy.collect("nothing").await?;
627 assert_eq!(res.len(), 0);
628
629 Ok(())
630 }
631}