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::{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        /*
34         * env::var() returns the first element in the environment.
35         * We want to return the last one, so that booting with a commandline like
36         * a=1 a=2 a=3 yields a=3.
37         */
38        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        // This config file may not be present depending on the device, but errors besides file
81        // not found should be surfaced.
82        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        // There is an arbitrary (but consistent) ordering between these four sources, where
98        // duplicate arguments in lower priority sources will be overwritten by arguments in
99        // higher priority sources. Within one source derived from the ZBI such as cmdline_args,
100        // the last time an argument occurs is canonically the chosen one.
101        //
102        // The chosen order is:
103        // 1) Environment
104        // 2) ZbiType::Cmdline
105        // 3) ZbiType::ImageArgs
106        // 4) Config file (hosted in bootfs)
107        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            // While this file has been "opened", FIDL I/O works on Fuchsia channels, so existence
128            // isn't confirmed until an I/O operation is performed. As before, any errors besides
129            // file not found should be surfaced.
130            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    /// Arguments are whitespace separated.
148    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    /// Legacy arguments are newline separated, and allow comments.
170    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                // This is a comment.
177                continue;
178            }
179
180            if trimmed.contains(char::is_whitespace) {
181                // Leading and trailing whitespace have already been trimmed, so any other
182                // internal whitespace makes this argument malformed.
183                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        // 0xfe is an invalid UTF-8 byte, and all sources must be parsable as UTF-8.
293        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        // Invalid config file.
309        assert!(
310            Arguments::new_from_sources(Env::mock_new(HashMap::new()), None, None, Some(config))
311                .await
312                .is_err()
313        );
314
315        // Invalid cmdline args.
316        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        // Invalid image args.
328        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        // Four arguments, all with the lowest priority.
343        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        // Overrides three of the four arguments originally passed via environment variable. Note
351        // that the second cmdline ZBI item overrides an argument in the first.
352        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        // Overrides two of the three arguments passed via cmdline.
358        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        // Finally, overrides one of the two arguments passed via image args. Note the comment
368        // which is ignored.
369        let config =
370            directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
371                .await
372                .unwrap();
373
374        // Write and flush to disk.
375        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        // check get_string works
448        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        // check get_strings() works
477        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        // map of key => (defaultval, expectedval)
513        let expected: Vec<(&str, bool, bool)> = vec![
514            // check 0, false, off all return false:
515            ("zero", true, false),
516            ("zero", false, false),
517            ("not_true", false, false),
518            ("not_on", true, false),
519            // check empty arguments return true
520            ("empty_but_true", false, true),
521            // check other values return true
522            ("should_be_true", false, true),
523            ("still_true", true, true),
524            // check unspecified values return defaultval.
525            ("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        // map of key => (defaultval, expectedval)
558        let expected: Vec<(&str, bool, bool)> = vec![
559            // check 0, false, off all return false:
560            ("zero", true, false),
561            ("zero", false, false),
562            ("not_true", false, false),
563            ("not_on", true, false),
564            // check empty arguments return true
565            ("empty_but_true", false, true),
566            // check other values return true
567            ("should_be_true", false, true),
568            ("still_true", true, true),
569            // check unspecified values return defaultval.
570            ("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}