1use anyhow::{Context, Error, anyhow};
6use cm_types::Name;
7use fidl::endpoints::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 { epitaph, .. }))
134 if epitaph == Status::NOT_FOUND || epitaph == Status::PEER_CLOSED =>
135 {
136 ()
137 }
138 Err(err) => return Err(anyhow!("Failed to read {}: {}", BOOT_CONFIG_FILE, err)),
139 }
140 }
141
142 Ok(Arc::new(Self { vars: result }))
143 }
144
145 fn parse_arguments(parsed: &mut HashMap<String, String>, raw: String) {
147 let lines = raw.trim_end_matches(char::from(0)).split_whitespace().collect::<Vec<&str>>();
148 for line in lines {
149 let split = line.splitn(2, "=").collect::<Vec<&str>>();
150 if split.len() == 0 {
151 info!("[Arguments] Empty argument string after parsing, ignoring: {}", line);
152 continue;
153 }
154
155 if split[0].is_empty() {
156 info!("[Arguments] Argument name cannot be empty, ignoring: {}", line);
157 continue;
158 }
159
160 parsed.insert(
161 split[0].to_string(),
162 if split.len() == 1 { String::new() } else { split[1].to_string() },
163 );
164 }
165 }
166
167 fn parse_legacy_arguments(parsed: &mut HashMap<String, String>, raw: String) {
169 let lines = raw.trim_end_matches(char::from(0)).lines();
170 for line in lines {
171 let trimmed = line.trim_start().trim_end();
172
173 if trimmed.starts_with("#") {
174 continue;
176 }
177
178 if trimmed.contains(char::is_whitespace) {
179 info!("[Arguments] Argument contains unexpected spaces, ignoring: {}", trimmed);
182 continue;
183 }
184
185 let split = trimmed.splitn(2, "=").collect::<Vec<&str>>();
186 if split.len() == 0 {
187 info!("[Arguments] Empty argument string after parsing, ignoring: {}", trimmed);
188 continue;
189 }
190
191 if split[0].is_empty() {
192 info!("[Arguments] Argument name cannot be empty, ignoring: {}", trimmed);
193 continue;
194 }
195
196 parsed.insert(
197 split[0].to_string(),
198 if split.len() == 1 { String::new() } else { split[1].to_string() },
199 );
200 }
201 }
202
203 fn get_bool_arg(self: &Arc<Self>, name: String, default: bool) -> bool {
204 let mut ret = default;
205 if let Ok(val) = self.var(name) {
206 if val == "0" || val == "false" || val == "off" {
207 ret = false;
208 } else {
209 ret = true;
210 }
211 }
212 ret
213 }
214
215 fn var(&self, var: String) -> Result<&str, env::VarError> {
216 if let Some(v) = self.vars.get(&var) { Ok(&v) } else { Err(env::VarError::NotPresent) }
217 }
218
219 fn vars<'a>(&'a self) -> Iter<'_, String, String> {
220 self.vars.iter()
221 }
222
223 pub async fn serve(
224 self: Arc<Self>,
225 mut stream: fboot::ArgumentsRequestStream,
226 ) -> Result<(), Error> {
227 while let Some(req) = stream.try_next().await? {
228 match req {
229 fboot::ArgumentsRequest::GetString { key, responder } => match self.var(key) {
230 Ok(val) => responder.send(Some(val)),
231 _ => responder.send(None),
232 }?,
233 fboot::ArgumentsRequest::GetStrings { keys, responder } => {
234 let vec: Vec<_> =
235 keys.into_iter().map(|x| self.var(x).ok().map(String::from)).collect();
236 responder.send(&vec)?
237 }
238 fboot::ArgumentsRequest::GetBool { key, defaultval, responder } => {
239 responder.send(self.get_bool_arg(key, defaultval))?
240 }
241 fboot::ArgumentsRequest::GetBools { keys, responder } => {
242 let vec: Vec<_> = keys
243 .into_iter()
244 .map(|key| self.get_bool_arg(key.key, key.defaultval))
245 .collect();
246 responder.send(&vec)?
247 }
248 fboot::ArgumentsRequest::Collect { prefix, responder } => {
249 let vec: Vec<_> = self
250 .vars()
251 .filter(|(k, _)| k.starts_with(&prefix))
252 .map(|(k, v)| k.to_owned() + "=" + &v)
253 .collect();
254 if vec.len() > fboot::MAX_ARGS_VECTOR_LENGTH.into() {
255 log::warn!(
256 "[Arguments] Collect results count {} exceeded maximum of {}",
257 vec.len(),
258 fboot::MAX_ARGS_VECTOR_LENGTH
259 );
260 responder.control_handle().shutdown_with_epitaph(Status::INTERNAL);
261 } else {
262 responder.send(&vec)?
263 }
264 }
265 }
266 }
267 Ok(())
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use fuchsia_async as fasync;
275 use fuchsia_fs::directory;
276 use fuchsia_fs::file::{close, write};
277
278 fn serve_bootargs(args: Arc<Arguments>) -> Result<fboot::ArgumentsProxy, Error> {
279 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ArgumentsMarker>();
280 fasync::Task::local(
281 args.serve(stream)
282 .unwrap_or_else(|e| panic!("Error while serving arguments service: {}", e)),
283 )
284 .detach();
285 Ok(proxy)
286 }
287
288 #[fuchsia::test]
289 async fn malformed_argument_sources() {
290 let data = vec![0xfe];
292
293 let tempdir = tempfile::TempDir::new().unwrap();
294 let dir = directory::open_in_namespace(
295 tempdir.path().to_str().unwrap(),
296 fio::PERM_READABLE | fio::PERM_WRITABLE,
297 )
298 .unwrap();
299
300 let config =
301 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
302 .await
303 .unwrap();
304 write(&config, data.clone()).await.unwrap();
305
306 assert!(
308 Arguments::new_from_sources(Env::mock_new(HashMap::new()), None, None, Some(config))
309 .await
310 .is_err()
311 );
312
313 assert!(
315 Arguments::new_from_sources(
316 Env::mock_new(HashMap::new()),
317 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
318 None,
319 None
320 )
321 .await
322 .is_err()
323 );
324
325 assert!(
327 Arguments::new_from_sources(
328 Env::mock_new(HashMap::new()),
329 None,
330 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
331 None
332 )
333 .await
334 .is_err()
335 );
336 }
337
338 #[fuchsia::test]
339 async fn prioritized_argument_sources() {
340 let env = Env::mock_new(
342 [("arg1", "env1"), ("arg2", "env2"), ("arg3", "env3"), ("arg4", "env4")]
343 .iter()
344 .map(|(a, b)| (a.to_string(), b.to_string()))
345 .collect(),
346 );
347
348 let cmdline = vec![
351 ZbiResult { bytes: b"arg2=notthisone arg3=cmd3 arg4=cmd4".to_vec(), extra: 0 },
352 ZbiResult { bytes: b"arg2=cmd2".to_vec(), extra: 0 },
353 ];
354
355 let image_args = vec![ZbiResult { bytes: b"arg3=img3\narg4=img4".to_vec(), extra: 0 }];
357
358 let tempdir = tempfile::TempDir::new().unwrap();
359 let dir = directory::open_in_namespace(
360 tempdir.path().to_str().unwrap(),
361 fio::PERM_READABLE | fio::PERM_WRITABLE,
362 )
363 .unwrap();
364
365 let config =
368 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
369 .await
370 .unwrap();
371
372 write(&config, b"# Comment!\narg4=config4").await.unwrap();
374 close(config).await.unwrap();
375
376 let config = directory::open_file(&dir, "file", fio::PERM_READABLE).await.unwrap();
377
378 let args = Arguments::new_from_sources(env, Some(cmdline), Some(image_args), Some(config))
379 .await
380 .unwrap();
381 let proxy = serve_bootargs(args).unwrap();
382
383 let result = proxy.get_string("arg1").await.unwrap().unwrap();
384 assert_eq!(result, "env1");
385
386 let result = proxy.get_string("arg2").await.unwrap().unwrap();
387 assert_eq!(result, "cmd2");
388
389 let result = proxy.get_string("arg3").await.unwrap().unwrap();
390 assert_eq!(result, "img3");
391
392 let result = proxy.get_string("arg4").await.unwrap().unwrap();
393 assert_eq!(result, "config4");
394 }
395
396 #[fuchsia::test]
397 async fn parse_argument_string() {
398 let raw_arguments = "arg1=val1 arg3 arg4= =val2 arg5='abcd=defg'".to_string();
399 let expected = [("arg1", "val1"), ("arg3", ""), ("arg4", ""), ("arg5", "'abcd=defg'")]
400 .iter()
401 .map(|(a, b)| (a.to_string(), b.to_string()))
402 .collect();
403
404 let mut actual = HashMap::new();
405 Arguments::parse_arguments(&mut actual, raw_arguments);
406
407 assert_eq!(actual, expected);
408 }
409
410 #[fuchsia::test]
411 async fn parse_legacy_argument_string() {
412 let raw_arguments = concat!(
413 "arg1=val1\n",
414 "arg2=val2,val3\n",
415 "=AnInvalidEmptyArgumentName!\n",
416 "perfectlyValidEmptyValue=\n",
417 "justThisIsFineToo\n",
418 "arg3=these=are=all=the=val\n",
419 " spacesAtStart=areFineButRemoved\n",
420 "# This is a comment\n",
421 "arg4=begrudinglyAllowButTrimTrailingSpaces \n"
422 )
423 .to_string();
424 let expected = [
425 ("arg1", "val1"),
426 ("arg2", "val2,val3"),
427 ("perfectlyValidEmptyValue", ""),
428 ("justThisIsFineToo", ""),
429 ("arg3", "these=are=all=the=val"),
430 ("spacesAtStart", "areFineButRemoved"),
431 ("arg4", "begrudinglyAllowButTrimTrailingSpaces"),
432 ]
433 .iter()
434 .map(|(a, b)| (a.to_string(), b.to_string()))
435 .collect();
436
437 let mut actual = HashMap::new();
438 Arguments::parse_legacy_arguments(&mut actual, raw_arguments);
439
440 assert_eq!(actual, expected);
441 }
442
443 #[fuchsia::test]
444 async fn can_get_string() -> Result<(), Error> {
445 let vars: HashMap<String, String> =
447 [("test_arg_1", "hello"), ("test_arg_2", "another var"), ("empty.arg", "")]
448 .iter()
449 .map(|(a, b)| (a.to_string(), b.to_string()))
450 .collect();
451 let proxy = serve_bootargs(
452 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
453 )?;
454
455 let res = proxy.get_string("test_arg_1").await?;
456 assert_ne!(res, None);
457 assert_eq!(res.unwrap(), "hello");
458
459 let res = proxy.get_string("test_arg_2").await?;
460 assert_ne!(res, None);
461 assert_eq!(res.unwrap(), "another var");
462
463 let res = proxy.get_string("empty.arg").await?;
464 assert_ne!(res, None);
465 assert_eq!(res.unwrap(), "");
466
467 let res = proxy.get_string("does.not.exist").await?;
468 assert_eq!(res, None);
469 Ok(())
470 }
471
472 #[fuchsia::test]
473 async fn can_get_strings() -> Result<(), Error> {
474 let vars: HashMap<String, String> =
476 [("test_arg_1", "hello"), ("test_arg_2", "another var")]
477 .iter()
478 .map(|(a, b)| (a.to_string(), b.to_string()))
479 .collect();
480 let proxy = serve_bootargs(
481 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
482 )?;
483
484 let req = &["test_arg_1".to_owned(), "test_arg_2".to_owned(), "test_arg_3".to_owned()];
485 let res = proxy.get_strings(req).await?;
486 let panicker = || panic!("got None, expected Some(str)");
487 assert_eq!(res[0].as_ref().unwrap_or_else(panicker), "hello");
488 assert_eq!(res[1].as_ref().unwrap_or_else(panicker), "another var");
489 assert_eq!(res[2], None);
490 assert_eq!(res.len(), 3);
491
492 let res = proxy.get_strings(&[]).await?;
493 assert_eq!(res.len(), 0);
494 Ok(())
495 }
496
497 #[fuchsia::test]
498 async fn can_get_bool() -> Result<(), Error> {
499 let vars: HashMap<String, String> = [
500 ("zero", "0"),
501 ("not_true", "false"),
502 ("not_on", "off"),
503 ("empty_but_true", ""),
504 ("should_be_true", "hello there"),
505 ("still_true", "no"),
506 ]
507 .iter()
508 .map(|(a, b)| (a.to_string(), b.to_string()))
509 .collect();
510 let expected: Vec<(&str, bool, bool)> = vec![
512 ("zero", true, false),
514 ("zero", false, false),
515 ("not_true", false, false),
516 ("not_on", true, false),
517 ("empty_but_true", false, true),
519 ("should_be_true", false, true),
521 ("still_true", true, true),
522 ("not_specified", false, false),
524 ("not_specified", true, true),
525 ];
526 let proxy = serve_bootargs(
527 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
528 )?;
529
530 for (var, default, correct) in expected.iter() {
531 let res = proxy.get_bool(var, *default).await?;
532 assert_eq!(
533 res, *correct,
534 "expect get_bool({}, {}) = {} but got {}",
535 var, default, correct, res
536 );
537 }
538
539 Ok(())
540 }
541
542 #[fuchsia::test]
543 async fn can_get_bools() -> Result<(), Error> {
544 let vars: HashMap<String, String> = [
545 ("zero", "0"),
546 ("not_true", "false"),
547 ("not_on", "off"),
548 ("empty_but_true", ""),
549 ("should_be_true", "hello there"),
550 ("still_true", "no"),
551 ]
552 .iter()
553 .map(|(a, b)| (a.to_string(), b.to_string()))
554 .collect();
555 let expected: Vec<(&str, bool, bool)> = vec![
557 ("zero", true, false),
559 ("zero", false, false),
560 ("not_true", false, false),
561 ("not_on", true, false),
562 ("empty_but_true", false, true),
564 ("should_be_true", false, true),
566 ("still_true", true, true),
567 ("not_specified", false, false),
569 ("not_specified", true, true),
570 ];
571 let proxy = serve_bootargs(
572 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
573 )?;
574
575 let req: Vec<fboot::BoolPair> = expected
576 .iter()
577 .map(|(key, default, _expected)| fboot::BoolPair {
578 key: String::from(*key),
579 defaultval: *default,
580 })
581 .collect();
582 let mut cur = 0;
583 for val in proxy.get_bools(&req).await?.iter() {
584 assert_eq!(
585 *val, expected[cur].2,
586 "get_bools() index {} returned {} but want {}",
587 cur, val, expected[cur].2
588 );
589 cur += 1;
590 }
591 Ok(())
592 }
593
594 #[fuchsia::test]
595 async fn can_collect() -> Result<(), Error> {
596 let vars: HashMap<String, String> = [
597 ("test.value1", "3"),
598 ("test.value2", ""),
599 ("testing.value1", "hello"),
600 ("test.bool", "false"),
601 ("another_test.value1", ""),
602 ("armadillos", "off"),
603 ]
604 .iter()
605 .map(|(a, b)| (a.to_string(), b.to_string()))
606 .collect();
607 let proxy = serve_bootargs(
608 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
609 )?;
610
611 let res = proxy.collect("test.").await?;
612 let expected = vec!["test.value1=3", "test.value2=", "test.bool=false"];
613 for val in expected.iter() {
614 assert_eq!(
615 res.contains(&String::from(*val)),
616 true,
617 "collect() is missing expected value {}",
618 val
619 );
620 }
621 assert_eq!(res.len(), expected.len());
622
623 let res = proxy.collect("nothing").await?;
624 assert_eq!(res.len(), 0);
625
626 Ok(())
627 }
628}