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