| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| anyhow::{anyhow, Context, Error}, |
| cm_types::Name, |
| fidl::endpoints::{ControlHandle as _, Responder as _}, |
| fidl_fuchsia_boot as fboot, fidl_fuchsia_io as fio, |
| fuchsia_fs::{file, file::ReadError, node::OpenError, OpenFlags}, |
| fuchsia_zbi::{ZbiParser, ZbiResult, ZbiType}, |
| fuchsia_zircon_status::Status, |
| futures::prelude::*, |
| lazy_static::lazy_static, |
| std::{ |
| collections::{hash_map::Iter, HashMap}, |
| env, |
| sync::Arc, |
| }, |
| tracing::info, |
| }; |
| |
| lazy_static! { |
| static ref BOOT_ARGS_CAPABILITY_NAME: Name = "fuchsia.boot.Arguments".parse().unwrap(); |
| } |
| |
| const BOOT_CONFIG_FILE: &str = "/boot/config/additional_boot_args"; |
| |
| struct Env { |
| vars: HashMap<String, String>, |
| } |
| |
| impl Env { |
| pub fn new() -> Self { |
| /* |
| * env::var() returns the first element in the environment. |
| * We want to return the last one, so that booting with a commandline like |
| * a=1 a=2 a=3 yields a=3. |
| */ |
| let mut map = HashMap::new(); |
| for (k, v) in env::vars() { |
| map.insert(k, v); |
| } |
| Env { vars: map } |
| } |
| |
| #[cfg(test)] |
| pub fn mock_new(map: HashMap<String, String>) -> Self { |
| Env { vars: map } |
| } |
| } |
| |
| pub struct Arguments { |
| vars: HashMap<String, String>, |
| } |
| |
| impl Arguments { |
| pub async fn new(parser: &mut Option<ZbiParser>) -> Result<Arc<Self>, Error> { |
| let (cmdline_args, image_args) = match parser { |
| Some(parser) => { |
| let cmdline_args = match parser.try_get_item(ZbiType::Cmdline.into_raw(), None) { |
| Ok(result) => { |
| let _ = parser.release_item(ZbiType::Cmdline); |
| Some(result) |
| } |
| Err(_) => None, |
| }; |
| |
| let image_args = match parser.try_get_item(ZbiType::ImageArgs.into_raw(), None) { |
| Ok(result) => { |
| let _ = parser.release_item(ZbiType::ImageArgs); |
| Some(result) |
| } |
| Err(_) => None, |
| }; |
| |
| (cmdline_args, image_args) |
| } |
| None => (None, None), |
| }; |
| |
| // This config file may not be present depending on the device, but errors besides file |
| // not found should be surfaced. |
| let config = match file::open_in_namespace(BOOT_CONFIG_FILE, OpenFlags::RIGHT_READABLE) { |
| Ok(config) => Some(config), |
| Err(OpenError::Namespace(Status::NOT_FOUND)) => None, |
| Err(err) => return Err(anyhow!("Failed to open {}: {}", BOOT_CONFIG_FILE, err)), |
| }; |
| |
| Arguments::new_from_sources(Env::new(), cmdline_args, image_args, config).await |
| } |
| |
| async fn new_from_sources( |
| env: Env, |
| cmdline_args: Option<Vec<ZbiResult>>, |
| image_args: Option<Vec<ZbiResult>>, |
| config_file: Option<fio::FileProxy>, |
| ) -> Result<Arc<Self>, Error> { |
| // There is an arbitrary (but consistent) ordering between these four sources, where |
| // duplicate arguments in lower priority sources will be overwritten by arguments in |
| // higher priority sources. Within one source derived from the ZBI such as cmdline_args, |
| // the last time an argument occurs is canonically the chosen one. |
| // |
| // The chosen order is: |
| // 1) Environment |
| // 2) ZbiType::Cmdline |
| // 3) ZbiType::ImageArgs |
| // 4) Config file (hosted in bootfs) |
| let mut result = HashMap::new(); |
| result.extend(env.vars); |
| |
| if cmdline_args.is_some() { |
| for cmdline_arg_item in cmdline_args.unwrap() { |
| let cmdline_arg_str = std::str::from_utf8(&cmdline_arg_item.bytes) |
| .context("failed to parse ZbiType::Cmdline as utf8")?; |
| Arguments::parse_arguments(&mut result, cmdline_arg_str.to_string()); |
| } |
| } |
| |
| if image_args.is_some() { |
| for image_arg_item in image_args.unwrap() { |
| let image_arg_str = std::str::from_utf8(&image_arg_item.bytes) |
| .context("failed to parse ZbiType::ImageArgs as utf8")?; |
| Arguments::parse_legacy_arguments(&mut result, image_arg_str.to_string()); |
| } |
| } |
| |
| if config_file.is_some() { |
| // While this file has been "opened", FIDL I/O works on Fuchsia channels, so existence |
| // isn't confirmed until an I/O operation is performed. As before, any errors besides |
| // file not found should be surfaced. |
| match file::read_to_string(&config_file.unwrap()).await { |
| Ok(config) => Arguments::parse_legacy_arguments(&mut result, config), |
| Err(ReadError::Fidl(fidl::Error::ClientChannelClosed { |
| status: Status::NOT_FOUND, |
| .. |
| })) => (), |
| Err(ReadError::Fidl(fidl::Error::ClientChannelClosed { |
| status: Status::PEER_CLOSED, |
| .. |
| })) => (), |
| Err(err) => return Err(anyhow!("Failed to read {}: {}", BOOT_CONFIG_FILE, err)), |
| } |
| } |
| |
| Ok(Arc::new(Self { vars: result })) |
| } |
| |
| /// Arguments are whitespace separated. |
| fn parse_arguments(parsed: &mut HashMap<String, String>, raw: String) { |
| let lines = raw.trim_end_matches(char::from(0)).split_whitespace().collect::<Vec<&str>>(); |
| for line in lines { |
| let split = line.splitn(2, "=").collect::<Vec<&str>>(); |
| if split.len() == 0 { |
| info!("[Arguments] Empty argument string after parsing, ignoring: {}", line); |
| continue; |
| } |
| |
| if split[0].is_empty() { |
| info!("[Arguments] Argument name cannot be empty, ignoring: {}", line); |
| continue; |
| } |
| |
| parsed.insert( |
| split[0].to_string(), |
| if split.len() == 1 { String::new() } else { split[1].to_string() }, |
| ); |
| } |
| } |
| |
| /// Legacy arguments are newline separated, and allow comments. |
| fn parse_legacy_arguments(parsed: &mut HashMap<String, String>, raw: String) { |
| let lines = raw.trim_end_matches(char::from(0)).lines(); |
| for line in lines { |
| let trimmed = line.trim_start().trim_end(); |
| |
| if trimmed.starts_with("#") { |
| // This is a comment. |
| continue; |
| } |
| |
| if trimmed.contains(char::is_whitespace) { |
| // Leading and trailing whitespace have already been trimmed, so any other |
| // internal whitespace makes this argument malformed. |
| info!("[Arguments] Argument contains unexpected spaces, ignoring: {}", trimmed); |
| continue; |
| } |
| |
| let split = trimmed.splitn(2, "=").collect::<Vec<&str>>(); |
| if split.len() == 0 { |
| info!("[Arguments] Empty argument string after parsing, ignoring: {}", trimmed); |
| continue; |
| } |
| |
| if split[0].is_empty() { |
| info!("[Arguments] Argument name cannot be empty, ignoring: {}", trimmed); |
| continue; |
| } |
| |
| parsed.insert( |
| split[0].to_string(), |
| if split.len() == 1 { String::new() } else { split[1].to_string() }, |
| ); |
| } |
| } |
| |
| fn get_bool_arg(self: &Arc<Self>, name: String, default: bool) -> bool { |
| let mut ret = default; |
| if let Ok(val) = self.var(name) { |
| if val == "0" || val == "false" || val == "off" { |
| ret = false; |
| } else { |
| ret = true; |
| } |
| } |
| ret |
| } |
| |
| fn var(&self, var: String) -> Result<&str, env::VarError> { |
| if let Some(v) = self.vars.get(&var) { |
| Ok(&v) |
| } else { |
| Err(env::VarError::NotPresent) |
| } |
| } |
| |
| fn vars<'a>(&'a self) -> Iter<'_, String, String> { |
| self.vars.iter() |
| } |
| |
| pub async fn serve( |
| self: Arc<Self>, |
| mut stream: fboot::ArgumentsRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(req) = stream.try_next().await? { |
| match req { |
| fboot::ArgumentsRequest::GetString { key, responder } => match self.var(key) { |
| Ok(val) => responder.send(Some(val)), |
| _ => responder.send(None), |
| }?, |
| fboot::ArgumentsRequest::GetStrings { keys, responder } => { |
| let vec: Vec<_> = |
| keys.into_iter().map(|x| self.var(x).ok().map(String::from)).collect(); |
| responder.send(&vec)? |
| } |
| fboot::ArgumentsRequest::GetBool { key, defaultval, responder } => { |
| responder.send(self.get_bool_arg(key, defaultval))? |
| } |
| fboot::ArgumentsRequest::GetBools { keys, responder } => { |
| let vec: Vec<_> = keys |
| .into_iter() |
| .map(|key| self.get_bool_arg(key.key, key.defaultval)) |
| .collect(); |
| responder.send(&vec)? |
| } |
| fboot::ArgumentsRequest::Collect { prefix, responder } => { |
| let vec: Vec<_> = self |
| .vars() |
| .filter(|(k, _)| k.starts_with(&prefix)) |
| .map(|(k, v)| k.to_owned() + "=" + &v) |
| .collect(); |
| if vec.len() > fboot::MAX_ARGS_VECTOR_LENGTH.into() { |
| tracing::warn!( |
| "[Arguments] Collect results count {} exceeded maximum of {}", |
| vec.len(), |
| fboot::MAX_ARGS_VECTOR_LENGTH |
| ); |
| responder.control_handle().shutdown_with_epitaph(Status::INTERNAL); |
| } else { |
| responder.send(&vec)? |
| } |
| } |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| fuchsia_async as fasync, |
| fuchsia_fs::{directory, file::close, file::write}, |
| }; |
| |
| fn serve_bootargs(args: Arc<Arguments>) -> Result<fboot::ArgumentsProxy, Error> { |
| let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ArgumentsMarker>()?; |
| fasync::Task::local( |
| args.serve(stream) |
| .unwrap_or_else(|e| panic!("Error while serving arguments service: {}", e)), |
| ) |
| .detach(); |
| Ok(proxy) |
| } |
| |
| #[fuchsia::test] |
| async fn malformed_argument_sources() { |
| // 0xfe is an invalid UTF-8 byte, and all sources must be parsable as UTF-8. |
| let data = vec![0xfe]; |
| |
| let tempdir = tempfile::TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| let config = directory::open_file( |
| &dir, |
| "file", |
| fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE, |
| ) |
| .await |
| .unwrap(); |
| write(&config, data.clone()).await.unwrap(); |
| |
| // Invalid config file. |
| assert!(Arguments::new_from_sources( |
| Env::mock_new(HashMap::new()), |
| None, |
| None, |
| Some(config) |
| ) |
| .await |
| .is_err()); |
| |
| // Invalid cmdline args. |
| assert!(Arguments::new_from_sources( |
| Env::mock_new(HashMap::new()), |
| Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]), |
| None, |
| None |
| ) |
| .await |
| .is_err()); |
| |
| // Invalid image args. |
| assert!(Arguments::new_from_sources( |
| Env::mock_new(HashMap::new()), |
| None, |
| Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]), |
| None |
| ) |
| .await |
| .is_err()); |
| } |
| |
| #[fuchsia::test] |
| async fn prioritized_argument_sources() { |
| // Four arguments, all with the lowest priority. |
| let env = Env::mock_new( |
| [("arg1", "env1"), ("arg2", "env2"), ("arg3", "env3"), ("arg4", "env4")] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(), |
| ); |
| |
| // Overrides three of the four arguments originally passed via environment variable. Note |
| // that the second cmdline ZBI item overrides an argument in the first. |
| let cmdline = vec![ |
| ZbiResult { bytes: b"arg2=notthisone arg3=cmd3 arg4=cmd4".to_vec(), extra: 0 }, |
| ZbiResult { bytes: b"arg2=cmd2".to_vec(), extra: 0 }, |
| ]; |
| |
| // Overrides two of the three arguments passed via cmdline. |
| let image_args = vec![ZbiResult { bytes: b"arg3=img3\narg4=img4".to_vec(), extra: 0 }]; |
| |
| let tempdir = tempfile::TempDir::new().unwrap(); |
| let dir = directory::open_in_namespace( |
| tempdir.path().to_str().unwrap(), |
| OpenFlags::RIGHT_READABLE | OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| // Finally, overrides one of the two arguments passed via image args. Note the comment |
| // which is ignored. |
| let config = directory::open_file( |
| &dir, |
| "file", |
| fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE, |
| ) |
| .await |
| .unwrap(); |
| |
| // Write and flush to disk. |
| write(&config, b"# Comment!\narg4=config4").await.unwrap(); |
| close(config).await.unwrap(); |
| |
| let config = |
| directory::open_file(&dir, "file", fio::OpenFlags::RIGHT_READABLE).await.unwrap(); |
| |
| let args = Arguments::new_from_sources(env, Some(cmdline), Some(image_args), Some(config)) |
| .await |
| .unwrap(); |
| let proxy = serve_bootargs(args).unwrap(); |
| |
| let result = proxy.get_string("arg1").await.unwrap().unwrap(); |
| assert_eq!(result, "env1"); |
| |
| let result = proxy.get_string("arg2").await.unwrap().unwrap(); |
| assert_eq!(result, "cmd2"); |
| |
| let result = proxy.get_string("arg3").await.unwrap().unwrap(); |
| assert_eq!(result, "img3"); |
| |
| let result = proxy.get_string("arg4").await.unwrap().unwrap(); |
| assert_eq!(result, "config4"); |
| } |
| |
| #[fuchsia::test] |
| async fn parse_argument_string() { |
| let raw_arguments = "arg1=val1 arg3 arg4= =val2 arg5='abcd=defg'".to_string(); |
| let expected = [("arg1", "val1"), ("arg3", ""), ("arg4", ""), ("arg5", "'abcd=defg'")] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| |
| let mut actual = HashMap::new(); |
| Arguments::parse_arguments(&mut actual, raw_arguments); |
| |
| assert_eq!(actual, expected); |
| } |
| |
| #[fuchsia::test] |
| async fn parse_legacy_argument_string() { |
| let raw_arguments = concat!( |
| "arg1=val1\n", |
| "arg2=val2,val3\n", |
| "=AnInvalidEmptyArgumentName!\n", |
| "perfectlyValidEmptyValue=\n", |
| "justThisIsFineToo\n", |
| "arg3=these=are=all=the=val\n", |
| " spacesAtStart=areFineButRemoved\n", |
| "# This is a comment\n", |
| "arg4=begrudinglyAllowButTrimTrailingSpaces \n" |
| ) |
| .to_string(); |
| let expected = [ |
| ("arg1", "val1"), |
| ("arg2", "val2,val3"), |
| ("perfectlyValidEmptyValue", ""), |
| ("justThisIsFineToo", ""), |
| ("arg3", "these=are=all=the=val"), |
| ("spacesAtStart", "areFineButRemoved"), |
| ("arg4", "begrudinglyAllowButTrimTrailingSpaces"), |
| ] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| |
| let mut actual = HashMap::new(); |
| Arguments::parse_legacy_arguments(&mut actual, raw_arguments); |
| |
| assert_eq!(actual, expected); |
| } |
| |
| #[fuchsia::test] |
| async fn can_get_string() -> Result<(), Error> { |
| // check get_string works |
| let vars: HashMap<String, String> = |
| [("test_arg_1", "hello"), ("test_arg_2", "another var"), ("empty.arg", "")] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| let proxy = serve_bootargs( |
| Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?, |
| )?; |
| |
| let res = proxy.get_string("test_arg_1").await?; |
| assert_ne!(res, None); |
| assert_eq!(res.unwrap(), "hello"); |
| |
| let res = proxy.get_string("test_arg_2").await?; |
| assert_ne!(res, None); |
| assert_eq!(res.unwrap(), "another var"); |
| |
| let res = proxy.get_string("empty.arg").await?; |
| assert_ne!(res, None); |
| assert_eq!(res.unwrap(), ""); |
| |
| let res = proxy.get_string("does.not.exist").await?; |
| assert_eq!(res, None); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn can_get_strings() -> Result<(), Error> { |
| // check get_strings() works |
| let vars: HashMap<String, String> = |
| [("test_arg_1", "hello"), ("test_arg_2", "another var")] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| let proxy = serve_bootargs( |
| Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?, |
| )?; |
| |
| let req = &["test_arg_1".to_owned(), "test_arg_2".to_owned(), "test_arg_3".to_owned()]; |
| let res = proxy.get_strings(req).await?; |
| let panicker = || panic!("got None, expected Some(str)"); |
| assert_eq!(res[0].as_ref().unwrap_or_else(panicker), "hello"); |
| assert_eq!(res[1].as_ref().unwrap_or_else(panicker), "another var"); |
| assert_eq!(res[2], None); |
| assert_eq!(res.len(), 3); |
| |
| let res = proxy.get_strings(&[]).await?; |
| assert_eq!(res.len(), 0); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn can_get_bool() -> Result<(), Error> { |
| let vars: HashMap<String, String> = [ |
| ("zero", "0"), |
| ("not_true", "false"), |
| ("not_on", "off"), |
| ("empty_but_true", ""), |
| ("should_be_true", "hello there"), |
| ("still_true", "no"), |
| ] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| // map of key => (defaultval, expectedval) |
| let expected: Vec<(&str, bool, bool)> = vec![ |
| // check 0, false, off all return false: |
| ("zero", true, false), |
| ("zero", false, false), |
| ("not_true", false, false), |
| ("not_on", true, false), |
| // check empty arguments return true |
| ("empty_but_true", false, true), |
| // check other values return true |
| ("should_be_true", false, true), |
| ("still_true", true, true), |
| // check unspecified values return defaultval. |
| ("not_specified", false, false), |
| ("not_specified", true, true), |
| ]; |
| let proxy = serve_bootargs( |
| Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?, |
| )?; |
| |
| for (var, default, correct) in expected.iter() { |
| let res = proxy.get_bool(var, *default).await?; |
| assert_eq!( |
| res, *correct, |
| "expect get_bool({}, {}) = {} but got {}", |
| var, default, correct, res |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn can_get_bools() -> Result<(), Error> { |
| let vars: HashMap<String, String> = [ |
| ("zero", "0"), |
| ("not_true", "false"), |
| ("not_on", "off"), |
| ("empty_but_true", ""), |
| ("should_be_true", "hello there"), |
| ("still_true", "no"), |
| ] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| // map of key => (defaultval, expectedval) |
| let expected: Vec<(&str, bool, bool)> = vec![ |
| // check 0, false, off all return false: |
| ("zero", true, false), |
| ("zero", false, false), |
| ("not_true", false, false), |
| ("not_on", true, false), |
| // check empty arguments return true |
| ("empty_but_true", false, true), |
| // check other values return true |
| ("should_be_true", false, true), |
| ("still_true", true, true), |
| // check unspecified values return defaultval. |
| ("not_specified", false, false), |
| ("not_specified", true, true), |
| ]; |
| let proxy = serve_bootargs( |
| Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?, |
| )?; |
| |
| let req: Vec<fboot::BoolPair> = expected |
| .iter() |
| .map(|(key, default, _expected)| fboot::BoolPair { |
| key: String::from(*key), |
| defaultval: *default, |
| }) |
| .collect(); |
| let mut cur = 0; |
| for val in proxy.get_bools(&req).await?.iter() { |
| assert_eq!( |
| *val, expected[cur].2, |
| "get_bools() index {} returned {} but want {}", |
| cur, val, expected[cur].2 |
| ); |
| cur += 1; |
| } |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn can_collect() -> Result<(), Error> { |
| let vars: HashMap<String, String> = [ |
| ("test.value1", "3"), |
| ("test.value2", ""), |
| ("testing.value1", "hello"), |
| ("test.bool", "false"), |
| ("another_test.value1", ""), |
| ("armadillos", "off"), |
| ] |
| .iter() |
| .map(|(a, b)| (a.to_string(), b.to_string())) |
| .collect(); |
| let proxy = serve_bootargs( |
| Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?, |
| )?; |
| |
| let res = proxy.collect("test.").await?; |
| let expected = vec!["test.value1=3", "test.value2=", "test.bool=false"]; |
| for val in expected.iter() { |
| assert_eq!( |
| res.contains(&String::from(*val)), |
| true, |
| "collect() is missing expected value {}", |
| val |
| ); |
| } |
| assert_eq!(res.len(), expected.len()); |
| |
| let res = proxy.collect("nothing").await?; |
| assert_eq!(res.len(), 0); |
| |
| Ok(()) |
| } |
| } |