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