builtins/
arguments.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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        /*
35         * env::var() returns the first element in the environment.
36         * We want to return the last one, so that booting with a commandline like
37         * a=1 a=2 a=3 yields a=3.
38         */
39        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        // This config file may not be present depending on the device, but errors besides file
82        // not found should be surfaced.
83        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        // There is an arbitrary (but consistent) ordering between these four sources, where
99        // duplicate arguments in lower priority sources will be overwritten by arguments in
100        // higher priority sources. Within one source derived from the ZBI such as cmdline_args,
101        // the last time an argument occurs is canonically the chosen one.
102        //
103        // The chosen order is:
104        // 1) Environment
105        // 2) ZbiType::Cmdline
106        // 3) ZbiType::ImageArgs
107        // 4) Config file (hosted in bootfs)
108        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            // While this file has been "opened", FIDL I/O works on Fuchsia channels, so existence
129            // isn't confirmed until an I/O operation is performed. As before, any errors besides
130            // file not found should be surfaced.
131            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    /// Arguments are whitespace separated.
149    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    /// Legacy arguments are newline separated, and allow comments.
171    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                // This is a comment.
178                continue;
179            }
180
181            if trimmed.contains(char::is_whitespace) {
182                // Leading and trailing whitespace have already been trimmed, so any other
183                // internal whitespace makes this argument malformed.
184                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        // 0xfe is an invalid UTF-8 byte, and all sources must be parsable as UTF-8.
298        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        // Invalid config file.
314        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        // Invalid cmdline args.
324        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        // Invalid image args.
334        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        // Four arguments, all with the lowest priority.
347        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        // Overrides three of the four arguments originally passed via environment variable. Note
355        // that the second cmdline ZBI item overrides an argument in the first.
356        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        // Overrides two of the three arguments passed via cmdline.
362        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        // Finally, overrides one of the two arguments passed via image args. Note the comment
372        // which is ignored.
373        let config =
374            directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
375                .await
376                .unwrap();
377
378        // Write and flush to disk.
379        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        // check get_string works
452        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        // check get_strings() works
481        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        // map of key => (defaultval, expectedval)
517        let expected: Vec<(&str, bool, bool)> = vec![
518            // check 0, false, off all return false:
519            ("zero", true, false),
520            ("zero", false, false),
521            ("not_true", false, false),
522            ("not_on", true, false),
523            // check empty arguments return true
524            ("empty_but_true", false, true),
525            // check other values return true
526            ("should_be_true", false, true),
527            ("still_true", true, true),
528            // check unspecified values return defaultval.
529            ("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        // map of key => (defaultval, expectedval)
562        let expected: Vec<(&str, bool, bool)> = vec![
563            // check 0, false, off all return false:
564            ("zero", true, false),
565            ("zero", false, false),
566            ("not_true", false, false),
567            ("not_on", true, false),
568            // check empty arguments return true
569            ("empty_but_true", false, true),
570            // check other values return true
571            ("should_be_true", false, true),
572            ("still_true", true, true),
573            // check unspecified values return defaultval.
574            ("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}