Skip to main content

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::Responder as _;
8use fidl_fuchsia_boot as fboot;
9use fidl_fuchsia_io as fio;
10use fuchsia_fs::file;
11use fuchsia_fs::file::ReadError;
12use fuchsia_fs::node::OpenError;
13use fuchsia_zbi::{ZbiParser, ZbiResult, ZbiType};
14use futures::prelude::*;
15use log::info;
16use std::collections::HashMap;
17use std::collections::hash_map::Iter;
18use std::env;
19use std::sync::{Arc, LazyLock};
20use zx_status::Status;
21
22#[allow(dead_code)]
23static BOOT_ARGS_CAPABILITY_NAME: LazyLock<Name> =
24    LazyLock::new(|| "fuchsia.boot.Arguments".parse().unwrap());
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 { epitaph, .. }))
134                    if epitaph == Status::NOT_FOUND || epitaph == Status::PEER_CLOSED =>
135                {
136                    ()
137                }
138                Err(err) => return Err(anyhow!("Failed to read {}: {}", BOOT_CONFIG_FILE, err)),
139            }
140        }
141
142        Ok(Arc::new(Self { vars: result }))
143    }
144
145    /// Arguments are whitespace separated.
146    fn parse_arguments(parsed: &mut HashMap<String, String>, raw: String) {
147        let lines = raw.trim_end_matches(char::from(0)).split_whitespace().collect::<Vec<&str>>();
148        for line in lines {
149            let split = line.splitn(2, "=").collect::<Vec<&str>>();
150            if split.len() == 0 {
151                info!("[Arguments] Empty argument string after parsing, ignoring: {}", line);
152                continue;
153            }
154
155            if split[0].is_empty() {
156                info!("[Arguments] Argument name cannot be empty, ignoring: {}", line);
157                continue;
158            }
159
160            parsed.insert(
161                split[0].to_string(),
162                if split.len() == 1 { String::new() } else { split[1].to_string() },
163            );
164        }
165    }
166
167    /// Legacy arguments are newline separated, and allow comments.
168    fn parse_legacy_arguments(parsed: &mut HashMap<String, String>, raw: String) {
169        let lines = raw.trim_end_matches(char::from(0)).lines();
170        for line in lines {
171            let trimmed = line.trim_start().trim_end();
172
173            if trimmed.starts_with("#") {
174                // This is a comment.
175                continue;
176            }
177
178            if trimmed.contains(char::is_whitespace) {
179                // Leading and trailing whitespace have already been trimmed, so any other
180                // internal whitespace makes this argument malformed.
181                info!("[Arguments] Argument contains unexpected spaces, ignoring: {}", trimmed);
182                continue;
183            }
184
185            let split = trimmed.splitn(2, "=").collect::<Vec<&str>>();
186            if split.len() == 0 {
187                info!("[Arguments] Empty argument string after parsing, ignoring: {}", trimmed);
188                continue;
189            }
190
191            if split[0].is_empty() {
192                info!("[Arguments] Argument name cannot be empty, ignoring: {}", trimmed);
193                continue;
194            }
195
196            parsed.insert(
197                split[0].to_string(),
198                if split.len() == 1 { String::new() } else { split[1].to_string() },
199            );
200        }
201    }
202
203    fn get_bool_arg(self: &Arc<Self>, name: String, default: bool) -> bool {
204        let mut ret = default;
205        if let Ok(val) = self.var(name) {
206            if val == "0" || val == "false" || val == "off" {
207                ret = false;
208            } else {
209                ret = true;
210            }
211        }
212        ret
213    }
214
215    fn var(&self, var: String) -> Result<&str, env::VarError> {
216        if let Some(v) = self.vars.get(&var) { Ok(&v) } else { Err(env::VarError::NotPresent) }
217    }
218
219    fn vars<'a>(&'a self) -> Iter<'_, String, String> {
220        self.vars.iter()
221    }
222
223    pub async fn serve(
224        self: Arc<Self>,
225        mut stream: fboot::ArgumentsRequestStream,
226    ) -> Result<(), Error> {
227        while let Some(req) = stream.try_next().await? {
228            match req {
229                fboot::ArgumentsRequest::GetString { key, responder } => match self.var(key) {
230                    Ok(val) => responder.send(Some(val)),
231                    _ => responder.send(None),
232                }?,
233                fboot::ArgumentsRequest::GetStrings { keys, responder } => {
234                    let vec: Vec<_> =
235                        keys.into_iter().map(|x| self.var(x).ok().map(String::from)).collect();
236                    responder.send(&vec)?
237                }
238                fboot::ArgumentsRequest::GetBool { key, defaultval, responder } => {
239                    responder.send(self.get_bool_arg(key, defaultval))?
240                }
241                fboot::ArgumentsRequest::GetBools { keys, responder } => {
242                    let vec: Vec<_> = keys
243                        .into_iter()
244                        .map(|key| self.get_bool_arg(key.key, key.defaultval))
245                        .collect();
246                    responder.send(&vec)?
247                }
248                fboot::ArgumentsRequest::Collect { prefix, responder } => {
249                    let vec: Vec<_> = self
250                        .vars()
251                        .filter(|(k, _)| k.starts_with(&prefix))
252                        .map(|(k, v)| k.to_owned() + "=" + &v)
253                        .collect();
254                    if vec.len() > fboot::MAX_ARGS_VECTOR_LENGTH.into() {
255                        log::warn!(
256                            "[Arguments] Collect results count {} exceeded maximum of {}",
257                            vec.len(),
258                            fboot::MAX_ARGS_VECTOR_LENGTH
259                        );
260                        responder.control_handle().shutdown_with_epitaph(Status::INTERNAL);
261                    } else {
262                        responder.send(&vec)?
263                    }
264                }
265            }
266        }
267        Ok(())
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use fuchsia_async as fasync;
275    use fuchsia_fs::directory;
276    use fuchsia_fs::file::{close, write};
277
278    fn serve_bootargs(args: Arc<Arguments>) -> Result<fboot::ArgumentsProxy, Error> {
279        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ArgumentsMarker>();
280        fasync::Task::local(
281            args.serve(stream)
282                .unwrap_or_else(|e| panic!("Error while serving arguments service: {}", e)),
283        )
284        .detach();
285        Ok(proxy)
286    }
287
288    #[fuchsia::test]
289    async fn malformed_argument_sources() {
290        // 0xfe is an invalid UTF-8 byte, and all sources must be parsable as UTF-8.
291        let data = vec![0xfe];
292
293        let tempdir = tempfile::TempDir::new().unwrap();
294        let dir = directory::open_in_namespace(
295            tempdir.path().to_str().unwrap(),
296            fio::PERM_READABLE | fio::PERM_WRITABLE,
297        )
298        .unwrap();
299
300        let config =
301            directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
302                .await
303                .unwrap();
304        write(&config, data.clone()).await.unwrap();
305
306        // Invalid config file.
307        assert!(
308            Arguments::new_from_sources(Env::mock_new(HashMap::new()), None, None, Some(config))
309                .await
310                .is_err()
311        );
312
313        // Invalid cmdline args.
314        assert!(
315            Arguments::new_from_sources(
316                Env::mock_new(HashMap::new()),
317                Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
318                None,
319                None
320            )
321            .await
322            .is_err()
323        );
324
325        // Invalid image args.
326        assert!(
327            Arguments::new_from_sources(
328                Env::mock_new(HashMap::new()),
329                None,
330                Some(vec![ZbiResult { bytes: data.clone(), extra: 0 }]),
331                None
332            )
333            .await
334            .is_err()
335        );
336    }
337
338    #[fuchsia::test]
339    async fn prioritized_argument_sources() {
340        // Four arguments, all with the lowest priority.
341        let env = Env::mock_new(
342            [("arg1", "env1"), ("arg2", "env2"), ("arg3", "env3"), ("arg4", "env4")]
343                .iter()
344                .map(|(a, b)| (a.to_string(), b.to_string()))
345                .collect(),
346        );
347
348        // Overrides three of the four arguments originally passed via environment variable. Note
349        // that the second cmdline ZBI item overrides an argument in the first.
350        let cmdline = vec![
351            ZbiResult { bytes: b"arg2=notthisone arg3=cmd3 arg4=cmd4".to_vec(), extra: 0 },
352            ZbiResult { bytes: b"arg2=cmd2".to_vec(), extra: 0 },
353        ];
354
355        // Overrides two of the three arguments passed via cmdline.
356        let image_args = vec![ZbiResult { bytes: b"arg3=img3\narg4=img4".to_vec(), extra: 0 }];
357
358        let tempdir = tempfile::TempDir::new().unwrap();
359        let dir = directory::open_in_namespace(
360            tempdir.path().to_str().unwrap(),
361            fio::PERM_READABLE | fio::PERM_WRITABLE,
362        )
363        .unwrap();
364
365        // Finally, overrides one of the two arguments passed via image args. Note the comment
366        // which is ignored.
367        let config =
368            directory::open_file(&dir, "file", fio::PERM_WRITABLE | fio::Flags::FLAG_MAYBE_CREATE)
369                .await
370                .unwrap();
371
372        // Write and flush to disk.
373        write(&config, b"# Comment!\narg4=config4").await.unwrap();
374        close(config).await.unwrap();
375
376        let config = directory::open_file(&dir, "file", fio::PERM_READABLE).await.unwrap();
377
378        let args = Arguments::new_from_sources(env, Some(cmdline), Some(image_args), Some(config))
379            .await
380            .unwrap();
381        let proxy = serve_bootargs(args).unwrap();
382
383        let result = proxy.get_string("arg1").await.unwrap().unwrap();
384        assert_eq!(result, "env1");
385
386        let result = proxy.get_string("arg2").await.unwrap().unwrap();
387        assert_eq!(result, "cmd2");
388
389        let result = proxy.get_string("arg3").await.unwrap().unwrap();
390        assert_eq!(result, "img3");
391
392        let result = proxy.get_string("arg4").await.unwrap().unwrap();
393        assert_eq!(result, "config4");
394    }
395
396    #[fuchsia::test]
397    async fn parse_argument_string() {
398        let raw_arguments = "arg1=val1 arg3   arg4= =val2 arg5='abcd=defg'".to_string();
399        let expected = [("arg1", "val1"), ("arg3", ""), ("arg4", ""), ("arg5", "'abcd=defg'")]
400            .iter()
401            .map(|(a, b)| (a.to_string(), b.to_string()))
402            .collect();
403
404        let mut actual = HashMap::new();
405        Arguments::parse_arguments(&mut actual, raw_arguments);
406
407        assert_eq!(actual, expected);
408    }
409
410    #[fuchsia::test]
411    async fn parse_legacy_argument_string() {
412        let raw_arguments = concat!(
413            "arg1=val1\n",
414            "arg2=val2,val3\n",
415            "=AnInvalidEmptyArgumentName!\n",
416            "perfectlyValidEmptyValue=\n",
417            "justThisIsFineToo\n",
418            "arg3=these=are=all=the=val\n",
419            "  spacesAtStart=areFineButRemoved\n",
420            "# This is a comment\n",
421            "arg4=begrudinglyAllowButTrimTrailingSpaces \n"
422        )
423        .to_string();
424        let expected = [
425            ("arg1", "val1"),
426            ("arg2", "val2,val3"),
427            ("perfectlyValidEmptyValue", ""),
428            ("justThisIsFineToo", ""),
429            ("arg3", "these=are=all=the=val"),
430            ("spacesAtStart", "areFineButRemoved"),
431            ("arg4", "begrudinglyAllowButTrimTrailingSpaces"),
432        ]
433        .iter()
434        .map(|(a, b)| (a.to_string(), b.to_string()))
435        .collect();
436
437        let mut actual = HashMap::new();
438        Arguments::parse_legacy_arguments(&mut actual, raw_arguments);
439
440        assert_eq!(actual, expected);
441    }
442
443    #[fuchsia::test]
444    async fn can_get_string() -> Result<(), Error> {
445        // check get_string works
446        let vars: HashMap<String, String> =
447            [("test_arg_1", "hello"), ("test_arg_2", "another var"), ("empty.arg", "")]
448                .iter()
449                .map(|(a, b)| (a.to_string(), b.to_string()))
450                .collect();
451        let proxy = serve_bootargs(
452            Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
453        )?;
454
455        let res = proxy.get_string("test_arg_1").await?;
456        assert_ne!(res, None);
457        assert_eq!(res.unwrap(), "hello");
458
459        let res = proxy.get_string("test_arg_2").await?;
460        assert_ne!(res, None);
461        assert_eq!(res.unwrap(), "another var");
462
463        let res = proxy.get_string("empty.arg").await?;
464        assert_ne!(res, None);
465        assert_eq!(res.unwrap(), "");
466
467        let res = proxy.get_string("does.not.exist").await?;
468        assert_eq!(res, None);
469        Ok(())
470    }
471
472    #[fuchsia::test]
473    async fn can_get_strings() -> Result<(), Error> {
474        // check get_strings() works
475        let vars: HashMap<String, String> =
476            [("test_arg_1", "hello"), ("test_arg_2", "another var")]
477                .iter()
478                .map(|(a, b)| (a.to_string(), b.to_string()))
479                .collect();
480        let proxy = serve_bootargs(
481            Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
482        )?;
483
484        let req = &["test_arg_1".to_owned(), "test_arg_2".to_owned(), "test_arg_3".to_owned()];
485        let res = proxy.get_strings(req).await?;
486        let panicker = || panic!("got None, expected Some(str)");
487        assert_eq!(res[0].as_ref().unwrap_or_else(panicker), "hello");
488        assert_eq!(res[1].as_ref().unwrap_or_else(panicker), "another var");
489        assert_eq!(res[2], None);
490        assert_eq!(res.len(), 3);
491
492        let res = proxy.get_strings(&[]).await?;
493        assert_eq!(res.len(), 0);
494        Ok(())
495    }
496
497    #[fuchsia::test]
498    async fn can_get_bool() -> Result<(), Error> {
499        let vars: HashMap<String, String> = [
500            ("zero", "0"),
501            ("not_true", "false"),
502            ("not_on", "off"),
503            ("empty_but_true", ""),
504            ("should_be_true", "hello there"),
505            ("still_true", "no"),
506        ]
507        .iter()
508        .map(|(a, b)| (a.to_string(), b.to_string()))
509        .collect();
510        // map of key => (defaultval, expectedval)
511        let expected: Vec<(&str, bool, bool)> = vec![
512            // check 0, false, off all return false:
513            ("zero", true, false),
514            ("zero", false, false),
515            ("not_true", false, false),
516            ("not_on", true, false),
517            // check empty arguments return true
518            ("empty_but_true", false, true),
519            // check other values return true
520            ("should_be_true", false, true),
521            ("still_true", true, true),
522            // check unspecified values return defaultval.
523            ("not_specified", false, false),
524            ("not_specified", true, true),
525        ];
526        let proxy = serve_bootargs(
527            Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
528        )?;
529
530        for (var, default, correct) in expected.iter() {
531            let res = proxy.get_bool(var, *default).await?;
532            assert_eq!(
533                res, *correct,
534                "expect get_bool({}, {}) = {} but got {}",
535                var, default, correct, res
536            );
537        }
538
539        Ok(())
540    }
541
542    #[fuchsia::test]
543    async fn can_get_bools() -> Result<(), Error> {
544        let vars: HashMap<String, String> = [
545            ("zero", "0"),
546            ("not_true", "false"),
547            ("not_on", "off"),
548            ("empty_but_true", ""),
549            ("should_be_true", "hello there"),
550            ("still_true", "no"),
551        ]
552        .iter()
553        .map(|(a, b)| (a.to_string(), b.to_string()))
554        .collect();
555        // map of key => (defaultval, expectedval)
556        let expected: Vec<(&str, bool, bool)> = vec![
557            // check 0, false, off all return false:
558            ("zero", true, false),
559            ("zero", false, false),
560            ("not_true", false, false),
561            ("not_on", true, false),
562            // check empty arguments return true
563            ("empty_but_true", false, true),
564            // check other values return true
565            ("should_be_true", false, true),
566            ("still_true", true, true),
567            // check unspecified values return defaultval.
568            ("not_specified", false, false),
569            ("not_specified", true, true),
570        ];
571        let proxy = serve_bootargs(
572            Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
573        )?;
574
575        let req: Vec<fboot::BoolPair> = expected
576            .iter()
577            .map(|(key, default, _expected)| fboot::BoolPair {
578                key: String::from(*key),
579                defaultval: *default,
580            })
581            .collect();
582        let mut cur = 0;
583        for val in proxy.get_bools(&req).await?.iter() {
584            assert_eq!(
585                *val, expected[cur].2,
586                "get_bools() index {} returned {} but want {}",
587                cur, val, expected[cur].2
588            );
589            cur += 1;
590        }
591        Ok(())
592    }
593
594    #[fuchsia::test]
595    async fn can_collect() -> Result<(), Error> {
596        let vars: HashMap<String, String> = [
597            ("test.value1", "3"),
598            ("test.value2", ""),
599            ("testing.value1", "hello"),
600            ("test.bool", "false"),
601            ("another_test.value1", ""),
602            ("armadillos", "off"),
603        ]
604        .iter()
605        .map(|(a, b)| (a.to_string(), b.to_string()))
606        .collect();
607        let proxy = serve_bootargs(
608            Arguments::new_from_sources(Env::mock_new(vars), None, None, None).await?,
609        )?;
610
611        let res = proxy.collect("test.").await?;
612        let expected = vec!["test.value1=3", "test.value2=", "test.bool=false"];
613        for val in expected.iter() {
614            assert_eq!(
615                res.contains(&String::from(*val)),
616                true,
617                "collect() is missing expected value {}",
618                val
619            );
620        }
621        assert_eq!(res.len(), expected.len());
622
623        let res = proxy.collect("nothing").await?;
624        assert_eq!(res.len(), 0);
625
626        Ok(())
627    }
628}