1use anyhow::{anyhow, Context, Error};
6use cm_types::Name;
7use fidl::endpoints::{ControlHandle as _, Responder as _};
8use fuchsia_fs::file;
9use fuchsia_fs::file::ReadError;
10use fuchsia_fs::node::OpenError;
11use fuchsia_zbi::{ZbiParser, ZbiResult, ZbiType};
12use futures::prelude::*;
13use lazy_static::lazy_static;
14use log::info;
15use std::collections::hash_map::Iter;
16use std::collections::HashMap;
17use std::env;
18use std::sync::Arc;
19use zx_status::Status;
20use {fidl_fuchsia_boot as fboot, fidl_fuchsia_io as fio};
21
22lazy_static! {
23 static ref BOOT_ARGS_CAPABILITY_NAME: Name = "fuchsia.boot.Arguments".parse().unwrap();
24}
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) {
220 Ok(&v)
221 } else {
222 Err(env::VarError::NotPresent)
223 }
224 }
225
226 fn vars<'a>(&'a self) -> Iter<'_, String, String> {
227 self.vars.iter()
228 }
229
230 pub async fn serve(
231 self: Arc<Self>,
232 mut stream: fboot::ArgumentsRequestStream,
233 ) -> Result<(), Error> {
234 while let Some(req) = stream.try_next().await? {
235 match req {
236 fboot::ArgumentsRequest::GetString { key, responder } => match self.var(key) {
237 Ok(val) => responder.send(Some(val)),
238 _ => responder.send(None),
239 }?,
240 fboot::ArgumentsRequest::GetStrings { keys, responder } => {
241 let vec: Vec<_> =
242 keys.into_iter().map(|x| self.var(x).ok().map(String::from)).collect();
243 responder.send(&vec)?
244 }
245 fboot::ArgumentsRequest::GetBool { key, defaultval, responder } => {
246 responder.send(self.get_bool_arg(key, defaultval))?
247 }
248 fboot::ArgumentsRequest::GetBools { keys, responder } => {
249 let vec: Vec<_> = keys
250 .into_iter()
251 .map(|key| self.get_bool_arg(key.key, key.defaultval))
252 .collect();
253 responder.send(&vec)?
254 }
255 fboot::ArgumentsRequest::Collect { prefix, responder } => {
256 let vec: Vec<_> = self
257 .vars()
258 .filter(|(k, _)| k.starts_with(&prefix))
259 .map(|(k, v)| k.to_owned() + "=" + &v)
260 .collect();
261 if vec.len() > fboot::MAX_ARGS_VECTOR_LENGTH.into() {
262 log::warn!(
263 "[Arguments] Collect results count {} exceeded maximum of {}",
264 vec.len(),
265 fboot::MAX_ARGS_VECTOR_LENGTH
266 );
267 responder.control_handle().shutdown_with_epitaph(Status::INTERNAL);
268 } else {
269 responder.send(&vec)?
270 }
271 }
272 }
273 }
274 Ok(())
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use fuchsia_async as fasync;
282 use fuchsia_fs::directory;
283 use fuchsia_fs::file::{close, write};
284
285 fn serve_bootargs(args: Arc<Arguments>) -> Result<fboot::ArgumentsProxy, Error> {
286 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ArgumentsMarker>();
287 fasync::Task::local(
288 args.serve(stream)
289 .unwrap_or_else(|e| panic!("Error while serving arguments service: {}", e)),
290 )
291 .detach();
292 Ok(proxy)
293 }
294
295 #[fuchsia::test]
296 async fn malformed_argument_sources() {
297 let data = vec![0xfe];
299
300 let tempdir = tempfile::TempDir::new().unwrap();
301 let dir = directory::open_in_namespace(
302 tempdir.path().to_str().unwrap(),
303 fio::PERM_READABLE | fio::PERM_WRITABLE,
304 )
305 .unwrap();
306
307 let config =
308 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
309 .await
310 .unwrap();
311 write(&config, data.clone()).await.unwrap();
312
313 assert!(Arguments::new_from_sources(
315 Env::mock_new(HashMap::new()),
316 None,
317 None,
318 Some(config)
319 )
320 .await
321 .is_err());
322
323 assert!(Arguments::new_from_sources(
325 Env::mock_new(HashMap::new()),
326 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
327 None,
328 None
329 )
330 .await
331 .is_err());
332
333 assert!(Arguments::new_from_sources(
335 Env::mock_new(HashMap::new()),
336 None,
337 Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
338 None
339 )
340 .await
341 .is_err());
342 }
343
344 #[fuchsia::test]
345 async fn prioritized_argument_sources() {
346 let env = Env::mock_new(
348 [("arg1", "env1"), ("arg2", "env2"), ("arg3", "env3"), ("arg4", "env4")]
349 .iter()
350 .map(|(a, b)| (a.to_string(), b.to_string()))
351 .collect(),
352 );
353
354 let cmdline = vec![
357 ZbiResult { bytes: b"arg2=notthisone arg3=cmd3 arg4=cmd4".to_vec(), extra: 0 },
358 ZbiResult { bytes: b"arg2=cmd2".to_vec(), extra: 0 },
359 ];
360
361 let image_args = vec![ZbiResult { bytes: b"arg3=img3\narg4=img4".to_vec(), extra: 0 }];
363
364 let tempdir = tempfile::TempDir::new().unwrap();
365 let dir = directory::open_in_namespace(
366 tempdir.path().to_str().unwrap(),
367 fio::PERM_READABLE | fio::PERM_WRITABLE,
368 )
369 .unwrap();
370
371 let config =
374 directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
375 .await
376 .unwrap();
377
378 write(&config, b"# Comment!\narg4=config4").await.unwrap();
380 close(config).await.unwrap();
381
382 let config = directory::open_file(&dir, "file", fio::PERM_READABLE).await.unwrap();
383
384 let args = Arguments::new_from_sources(env, Some(cmdline), Some(image_args), Some(config))
385 .await
386 .unwrap();
387 let proxy = serve_bootargs(args).unwrap();
388
389 let result = proxy.get_string("arg1").await.unwrap().unwrap();
390 assert_eq!(result, "env1");
391
392 let result = proxy.get_string("arg2").await.unwrap().unwrap();
393 assert_eq!(result, "cmd2");
394
395 let result = proxy.get_string("arg3").await.unwrap().unwrap();
396 assert_eq!(result, "img3");
397
398 let result = proxy.get_string("arg4").await.unwrap().unwrap();
399 assert_eq!(result, "config4");
400 }
401
402 #[fuchsia::test]
403 async fn parse_argument_string() {
404 let raw_arguments = "arg1=val1 arg3 arg4= =val2 arg5='abcd=defg'".to_string();
405 let expected = [("arg1", "val1"), ("arg3", ""), ("arg4", ""), ("arg5", "'abcd=defg'")]
406 .iter()
407 .map(|(a, b)| (a.to_string(), b.to_string()))
408 .collect();
409
410 let mut actual = HashMap::new();
411 Arguments::parse_arguments(&mut actual, raw_arguments);
412
413 assert_eq!(actual, expected);
414 }
415
416 #[fuchsia::test]
417 async fn parse_legacy_argument_string() {
418 let raw_arguments = concat!(
419 "arg1=val1\n",
420 "arg2=val2,val3\n",
421 "=AnInvalidEmptyArgumentName!\n",
422 "perfectlyValidEmptyValue=\n",
423 "justThisIsFineToo\n",
424 "arg3=these=are=all=the=val\n",
425 " spacesAtStart=areFineButRemoved\n",
426 "# This is a comment\n",
427 "arg4=begrudinglyAllowButTrimTrailingSpaces \n"
428 )
429 .to_string();
430 let expected = [
431 ("arg1", "val1"),
432 ("arg2", "val2,val3"),
433 ("perfectlyValidEmptyValue", ""),
434 ("justThisIsFineToo", ""),
435 ("arg3", "these=are=all=the=val"),
436 ("spacesAtStart", "areFineButRemoved"),
437 ("arg4", "begrudinglyAllowButTrimTrailingSpaces"),
438 ]
439 .iter()
440 .map(|(a, b)| (a.to_string(), b.to_string()))
441 .collect();
442
443 let mut actual = HashMap::new();
444 Arguments::parse_legacy_arguments(&mut actual, raw_arguments);
445
446 assert_eq!(actual, expected);
447 }
448
449 #[fuchsia::test]
450 async fn can_get_string() -> Result<(), Error> {
451 let vars: HashMap<String, String> =
453 [("test_arg_1", "hello"), ("test_arg_2", "another var"), ("empty.arg", "")]
454 .iter()
455 .map(|(a, b)| (a.to_string(), b.to_string()))
456 .collect();
457 let proxy = serve_bootargs(
458 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
459 )?;
460
461 let res = proxy.get_string("test_arg_1").await?;
462 assert_ne!(res, None);
463 assert_eq!(res.unwrap(), "hello");
464
465 let res = proxy.get_string("test_arg_2").await?;
466 assert_ne!(res, None);
467 assert_eq!(res.unwrap(), "another var");
468
469 let res = proxy.get_string("empty.arg").await?;
470 assert_ne!(res, None);
471 assert_eq!(res.unwrap(), "");
472
473 let res = proxy.get_string("does.not.exist").await?;
474 assert_eq!(res, None);
475 Ok(())
476 }
477
478 #[fuchsia::test]
479 async fn can_get_strings() -> Result<(), Error> {
480 let vars: HashMap<String, String> =
482 [("test_arg_1", "hello"), ("test_arg_2", "another var")]
483 .iter()
484 .map(|(a, b)| (a.to_string(), b.to_string()))
485 .collect();
486 let proxy = serve_bootargs(
487 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
488 )?;
489
490 let req = &["test_arg_1".to_owned(), "test_arg_2".to_owned(), "test_arg_3".to_owned()];
491 let res = proxy.get_strings(req).await?;
492 let panicker = || panic!("got None, expected Some(str)");
493 assert_eq!(res[0].as_ref().unwrap_or_else(panicker), "hello");
494 assert_eq!(res[1].as_ref().unwrap_or_else(panicker), "another var");
495 assert_eq!(res[2], None);
496 assert_eq!(res.len(), 3);
497
498 let res = proxy.get_strings(&[]).await?;
499 assert_eq!(res.len(), 0);
500 Ok(())
501 }
502
503 #[fuchsia::test]
504 async fn can_get_bool() -> Result<(), Error> {
505 let vars: HashMap<String, String> = [
506 ("zero", "0"),
507 ("not_true", "false"),
508 ("not_on", "off"),
509 ("empty_but_true", ""),
510 ("should_be_true", "hello there"),
511 ("still_true", "no"),
512 ]
513 .iter()
514 .map(|(a, b)| (a.to_string(), b.to_string()))
515 .collect();
516 let expected: Vec<(&str, bool, bool)> = vec![
518 ("zero", true, false),
520 ("zero", false, false),
521 ("not_true", false, false),
522 ("not_on", true, false),
523 ("empty_but_true", false, true),
525 ("should_be_true", false, true),
527 ("still_true", true, true),
528 ("not_specified", false, false),
530 ("not_specified", true, true),
531 ];
532 let proxy = serve_bootargs(
533 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
534 )?;
535
536 for (var, default, correct) in expected.iter() {
537 let res = proxy.get_bool(var, *default).await?;
538 assert_eq!(
539 res, *correct,
540 "expect get_bool({}, {}) = {} but got {}",
541 var, default, correct, res
542 );
543 }
544
545 Ok(())
546 }
547
548 #[fuchsia::test]
549 async fn can_get_bools() -> Result<(), Error> {
550 let vars: HashMap<String, String> = [
551 ("zero", "0"),
552 ("not_true", "false"),
553 ("not_on", "off"),
554 ("empty_but_true", ""),
555 ("should_be_true", "hello there"),
556 ("still_true", "no"),
557 ]
558 .iter()
559 .map(|(a, b)| (a.to_string(), b.to_string()))
560 .collect();
561 let expected: Vec<(&str, bool, bool)> = vec![
563 ("zero", true, false),
565 ("zero", false, false),
566 ("not_true", false, false),
567 ("not_on", true, false),
568 ("empty_but_true", false, true),
570 ("should_be_true", false, true),
572 ("still_true", true, true),
573 ("not_specified", false, false),
575 ("not_specified", true, true),
576 ];
577 let proxy = serve_bootargs(
578 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
579 )?;
580
581 let req: Vec<fboot::BoolPair> = expected
582 .iter()
583 .map(|(key, default, _expected)| fboot::BoolPair {
584 key: String::from(*key),
585 defaultval: *default,
586 })
587 .collect();
588 let mut cur = 0;
589 for val in proxy.get_bools(&req).await?.iter() {
590 assert_eq!(
591 *val, expected[cur].2,
592 "get_bools() index {} returned {} but want {}",
593 cur, val, expected[cur].2
594 );
595 cur += 1;
596 }
597 Ok(())
598 }
599
600 #[fuchsia::test]
601 async fn can_collect() -> Result<(), Error> {
602 let vars: HashMap<String, String> = [
603 ("test.value1", "3"),
604 ("test.value2", ""),
605 ("testing.value1", "hello"),
606 ("test.bool", "false"),
607 ("another_test.value1", ""),
608 ("armadillos", "off"),
609 ]
610 .iter()
611 .map(|(a, b)| (a.to_string(), b.to_string()))
612 .collect();
613 let proxy = serve_bootargs(
614 Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
615 )?;
616
617 let res = proxy.collect("test.").await?;
618 let expected = vec!["test.value1=3", "test.value2=", "test.bool=false"];
619 for val in expected.iter() {
620 assert_eq!(
621 res.contains(&String::from(*val)),
622 true,
623 "collect() is missing expected value {}",
624 val
625 );
626 }
627 assert_eq!(res.len(), expected.len());
628
629 let res = proxy.collect("nothing").await?;
630 assert_eq!(res.len(), 0);
631
632 Ok(())
633 }
634}