runner/
lib.rs

1// Copyright 2019 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
5pub mod component;
6pub mod serde;
7
8use fidl::endpoints::ServerEnd;
9#[cfg(fuchsia_api_level_at_least = "HEAD")]
10use fidl_fuchsia_component_sandbox as fsandbox;
11use std::path::Path;
12use thiserror::Error;
13use {
14    fidl_fuchsia_component_runner as fcrunner, fidl_fuchsia_component_sandbox as _,
15    fidl_fuchsia_data as fdata, fidl_fuchsia_io as fio, fidl_fuchsia_mem as fmem,
16    fidl_fuchsia_process as fprocess,
17};
18
19const ARGS_KEY: &str = "args";
20const BINARY_KEY: &str = "binary";
21const ENVIRON_KEY: &str = "environ";
22
23/// An error encountered trying to get entry out of `ComponentStartInfo->program`.
24#[derive(Clone, Debug, PartialEq, Eq, Error)]
25pub enum StartInfoProgramError {
26    #[error("\"program.binary\" must be specified")]
27    MissingBinary,
28
29    #[error("the value of \"program.binary\" must be a string")]
30    InValidBinaryType,
31
32    #[error("the value of \"program.binary\" must be a relative path")]
33    BinaryPathNotRelative,
34
35    #[error("the value of \"program.{0}\" must be an array of strings")]
36    InvalidStrVec(String),
37
38    #[error("\"program\" must be specified")]
39    NotFound,
40
41    #[error("invalid type for key \"{0}\", expected string")]
42    InvalidType(String),
43
44    #[error("invalid value for key \"{0}\", expected one of \"{1}\", found \"{2}\"")]
45    InvalidValue(String, String, String),
46
47    #[error("environ value at index \"{0}\" is invalid. Value must be format of 'VARIABLE=VALUE'")]
48    InvalidEnvironValue(usize),
49}
50
51/// Retrieves component URL from start_info or errors out if not found.
52pub fn get_resolved_url(start_info: &fcrunner::ComponentStartInfo) -> Option<String> {
53    start_info.resolved_url.clone()
54}
55
56/// Returns a reference to the value corresponding to the key.
57pub fn get_value<'a>(dict: &'a fdata::Dictionary, key: &str) -> Option<&'a fdata::DictionaryValue> {
58    match &dict.entries {
59        Some(entries) => {
60            for entry in entries {
61                if entry.key == key {
62                    return entry.value.as_ref().map(|val| &**val);
63                }
64            }
65            None
66        }
67        _ => None,
68    }
69}
70
71/// Retrieve a reference to the enum value corresponding to the key.
72pub fn get_enum<'a>(
73    dict: &'a fdata::Dictionary,
74    key: &str,
75    variants: &[&str],
76) -> Result<Option<&'a str>, StartInfoProgramError> {
77    match get_value(dict, key) {
78        Some(fdata::DictionaryValue::Str(value)) => {
79            if variants.contains(&value.as_str()) {
80                Ok(Some(value.as_ref()))
81            } else {
82                Err(StartInfoProgramError::InvalidValue(
83                    key.to_owned(),
84                    format!("{:?}", variants),
85                    value.to_owned(),
86                ))
87            }
88        }
89        Some(_) => Err(StartInfoProgramError::InvalidType(key.to_owned())),
90        None => Ok(None),
91    }
92}
93
94/// Retrieve value of type bool. Defaults to 'false' if key is not found.
95pub fn get_bool<'a>(dict: &'a fdata::Dictionary, key: &str) -> Result<bool, StartInfoProgramError> {
96    match get_enum(dict, key, &["true", "false"])? {
97        Some("true") => Ok(true),
98        _ => Ok(false),
99    }
100}
101
102fn get_program_value<'a>(
103    start_info: &'a fcrunner::ComponentStartInfo,
104    key: &str,
105) -> Option<&'a fdata::DictionaryValue> {
106    get_value(start_info.program.as_ref()?, key)
107}
108
109/// Retrieve a string from the program dictionary in ComponentStartInfo.
110pub fn get_program_string<'a>(
111    start_info: &'a fcrunner::ComponentStartInfo,
112    key: &str,
113) -> Option<&'a str> {
114    if let fdata::DictionaryValue::Str(value) = get_program_value(start_info, key)? {
115        Some(value)
116    } else {
117        None
118    }
119}
120
121/// Retrieve a StrVec from the program dictionary in ComponentStartInfo. Returns StartInfoProgramError::InvalidStrVec if
122/// the value is not a StrVec.
123pub fn get_program_strvec<'a>(
124    start_info: &'a fcrunner::ComponentStartInfo,
125    key: &str,
126) -> Result<Option<&'a Vec<String>>, StartInfoProgramError> {
127    match get_program_value(start_info, key) {
128        Some(args_value) => match args_value {
129            fdata::DictionaryValue::StrVec(vec) => Ok(Some(vec)),
130            _ => Err(StartInfoProgramError::InvalidStrVec(key.to_string())),
131        },
132        None => Ok(None),
133    }
134}
135
136/// Retrieves program.binary from ComponentStartInfo and makes sure that path is relative.
137// TODO(https://fxbug.dev/42079981): This method should accept a program dict instead of start_info
138pub fn get_program_binary(
139    start_info: &fcrunner::ComponentStartInfo,
140) -> Result<String, StartInfoProgramError> {
141    if let Some(program) = &start_info.program {
142        get_program_binary_from_dict(&program)
143    } else {
144        Err(StartInfoProgramError::NotFound)
145    }
146}
147
148/// Retrieves `binary` from a ComponentStartInfo dict and makes sure that path is relative.
149pub fn get_program_binary_from_dict(
150    dict: &fdata::Dictionary,
151) -> Result<String, StartInfoProgramError> {
152    if let Some(val) = get_value(&dict, BINARY_KEY) {
153        if let fdata::DictionaryValue::Str(bin) = val {
154            if !Path::new(bin).is_absolute() {
155                Ok(bin.to_string())
156            } else {
157                Err(StartInfoProgramError::BinaryPathNotRelative)
158            }
159        } else {
160            Err(StartInfoProgramError::InValidBinaryType)
161        }
162    } else {
163        Err(StartInfoProgramError::MissingBinary)
164    }
165}
166
167/// Retrieves program.args from ComponentStartInfo and validates them.
168// TODO(https://fxbug.dev/42079981): This method should accept a program dict instead of start_info
169pub fn get_program_args(
170    start_info: &fcrunner::ComponentStartInfo,
171) -> Result<Vec<String>, StartInfoProgramError> {
172    match get_program_strvec(start_info, ARGS_KEY)? {
173        Some(vec) => Ok(vec.iter().map(|v| v.clone()).collect()),
174        None => Ok(vec![]),
175    }
176}
177
178/// Retrieves `args` from a ComponentStartInfo program dict and validates them.
179pub fn get_program_args_from_dict(
180    dict: &fdata::Dictionary,
181) -> Result<Vec<String>, StartInfoProgramError> {
182    match get_value(&dict, ARGS_KEY) {
183        Some(args_value) => match args_value {
184            fdata::DictionaryValue::StrVec(vec) => Ok(vec.iter().map(|v| v.clone()).collect()),
185            _ => Err(StartInfoProgramError::InvalidStrVec(ARGS_KEY.to_string())),
186        },
187        None => Ok(vec![]),
188    }
189}
190
191pub fn get_environ(dict: &fdata::Dictionary) -> Result<Option<Vec<String>>, StartInfoProgramError> {
192    match get_value(dict, ENVIRON_KEY) {
193        Some(fdata::DictionaryValue::StrVec(values)) => {
194            if values.is_empty() {
195                return Ok(None);
196            }
197            for (i, value) in values.iter().enumerate() {
198                let parts = value.split_once("=");
199                if parts.is_none() {
200                    return Err(StartInfoProgramError::InvalidEnvironValue(i));
201                }
202                let parts = parts.unwrap();
203                // The value of an environment variable can in fact be empty.
204                if parts.0.is_empty() {
205                    return Err(StartInfoProgramError::InvalidEnvironValue(i));
206                }
207            }
208            Ok(Some(values.clone()))
209        }
210        Some(fdata::DictionaryValue::Str(_)) => Err(StartInfoProgramError::InvalidValue(
211            ENVIRON_KEY.to_owned(),
212            "vector of string".to_owned(),
213            "string".to_owned(),
214        )),
215        Some(other) => Err(StartInfoProgramError::InvalidValue(
216            ENVIRON_KEY.to_owned(),
217            "vector of string".to_owned(),
218            format!("{:?}", other),
219        )),
220        None => Ok(None),
221    }
222}
223
224/// Errors from parsing a component's configuration data.
225#[derive(Debug, Clone, Error)]
226pub enum ConfigDataError {
227    #[error("failed to create a vmo: {_0}")]
228    VmoCreate(#[source] zx::Status),
229    #[error("failed to write to vmo: {_0}")]
230    VmoWrite(#[source] zx::Status),
231    #[error("encountered an unrecognized variant of fuchsia.mem.Data")]
232    UnrecognizedDataVariant,
233}
234
235pub fn get_config_vmo(encoded_config: fmem::Data) -> Result<zx::Vmo, ConfigDataError> {
236    match encoded_config {
237        fmem::Data::Buffer(fmem::Buffer {
238            vmo,
239            size: _, // we get this vmo from component manager which sets the content size
240        }) => Ok(vmo),
241        fmem::Data::Bytes(bytes) => {
242            let size = bytes.len() as u64;
243            let vmo = zx::Vmo::create(size).map_err(ConfigDataError::VmoCreate)?;
244            vmo.write(&bytes, 0).map_err(ConfigDataError::VmoWrite)?;
245            Ok(vmo)
246        }
247        _ => Err(ConfigDataError::UnrecognizedDataVariant.into()),
248    }
249}
250
251/// Errors from parsing ComponentStartInfo.
252#[derive(Debug, Clone, Error)]
253pub enum StartInfoError {
254    #[error("missing program")]
255    MissingProgram,
256    #[error("missing resolved URL")]
257    MissingResolvedUrl,
258}
259
260impl StartInfoError {
261    /// Convert this error into its approximate `zx::Status` equivalent.
262    pub fn as_zx_status(&self) -> zx::Status {
263        match self {
264            StartInfoError::MissingProgram => zx::Status::INVALID_ARGS,
265            StartInfoError::MissingResolvedUrl => zx::Status::INVALID_ARGS,
266        }
267    }
268}
269
270/// [StartInfo] is convertible from the FIDL [fcrunner::ComponentStartInfo]
271/// type and performs validation that makes sense for all runners in the process.
272pub struct StartInfo {
273    /// The resolved URL of the component.
274    ///
275    /// This is the canonical URL obtained by the component resolver after
276    /// following redirects and resolving relative paths.
277    pub resolved_url: String,
278
279    /// The component's program declaration.
280    /// This information originates from `ComponentDecl.program`.
281    pub program: fdata::Dictionary,
282
283    /// The namespace to provide to the component instance.
284    ///
285    /// A namespace specifies the set of directories that a component instance
286    /// receives at start-up. Through the namespace directories, a component
287    /// may access capabilities available to it. The contents of the namespace
288    /// are mainly determined by the component's `use` declarations but may
289    /// also contain additional capabilities automatically provided by the
290    /// framework.
291    ///
292    /// By convention, a component's namespace typically contains some or all
293    /// of the following directories:
294    ///
295    /// - "/svc": A directory containing services that the component requested
296    ///           to use via its "import" declarations.
297    /// - "/pkg": A directory containing the component's package, including its
298    ///           binaries, libraries, and other assets.
299    ///
300    /// The mount points specified in each entry must be unique and
301    /// non-overlapping. For example, [{"/foo", ..}, {"/foo/bar", ..}] is
302    /// invalid.
303    pub namespace: Vec<fcrunner::ComponentNamespaceEntry>,
304
305    /// The directory this component serves.
306    pub outgoing_dir: Option<ServerEnd<fio::DirectoryMarker>>,
307
308    /// The directory served by the runner to present runtime information about
309    /// the component. The runner must either serve it, or drop it to avoid
310    /// blocking any consumers indefinitely.
311    pub runtime_dir: Option<ServerEnd<fio::DirectoryMarker>>,
312
313    /// The numbered handles that were passed to the component.
314    ///
315    /// If the component does not support numbered handles, the runner is expected
316    /// to close the handles.
317    pub numbered_handles: Vec<fprocess::HandleInfo>,
318
319    /// Binary representation of the component's configuration.
320    ///
321    /// # Layout
322    ///
323    /// The first 2 bytes of the data should be interpreted as an unsigned 16-bit
324    /// little-endian integer which denotes the number of bytes following it that
325    /// contain the configuration checksum. After the checksum, all the remaining
326    /// bytes are a persistent FIDL message of a top-level struct. The struct's
327    /// fields match the configuration fields of the component's compiled manifest
328    /// in the same order.
329    pub encoded_config: Option<fmem::Data>,
330
331    /// An eventpair that debuggers can use to defer the launch of the component.
332    ///
333    /// For example, ELF runners hold off from creating processes in the component
334    /// until ZX_EVENTPAIR_PEER_CLOSED is signaled on this eventpair. They also
335    /// ensure that runtime_dir is served before waiting on this eventpair.
336    /// ELF debuggers can query the runtime_dir to decide whether to attach before
337    /// they drop the other side of the eventpair, which is sent in the payload of
338    /// the DebugStarted event in fuchsia.component.events.
339    pub break_on_start: Option<zx::EventPair>,
340
341    /// An opaque token that represents the component instance.
342    ///
343    /// The `fuchsia.component/Introspector` protocol may be used to get the
344    /// string moniker of the instance from this token.
345    ///
346    /// Runners may publish this token as part of diagnostics information, to
347    /// identify the running component without knowing its moniker.
348    ///
349    /// The token is invalidated when the component instance is destroyed.
350    #[cfg(fuchsia_api_level_at_least = "HEAD")]
351    pub component_instance: Option<zx::Event>,
352
353    /// A dictionary containing data and handles that the component has escrowed
354    /// during its previous execution via
355    /// `fuchsia.component.runner/ComponentController.OnEscrow`.
356    #[cfg(fuchsia_api_level_at_least = "HEAD")]
357    pub escrowed_dictionary: Option<fsandbox::DictionaryRef>,
358}
359
360impl TryFrom<fcrunner::ComponentStartInfo> for StartInfo {
361    type Error = StartInfoError;
362    fn try_from(start_info: fcrunner::ComponentStartInfo) -> Result<Self, Self::Error> {
363        let resolved_url = start_info.resolved_url.ok_or(StartInfoError::MissingResolvedUrl)?;
364        let program = start_info.program.ok_or(StartInfoError::MissingProgram)?;
365        Ok(Self {
366            resolved_url,
367            program,
368            namespace: start_info.ns.unwrap_or_else(|| Vec::new()),
369            outgoing_dir: start_info.outgoing_dir,
370            runtime_dir: start_info.runtime_dir,
371            numbered_handles: start_info.numbered_handles.unwrap_or_else(|| Vec::new()),
372            encoded_config: start_info.encoded_config,
373            break_on_start: start_info.break_on_start,
374            #[cfg(fuchsia_api_level_at_least = "HEAD")]
375            component_instance: start_info.component_instance,
376            #[cfg(fuchsia_api_level_at_least = "HEAD")]
377            escrowed_dictionary: start_info.escrowed_dictionary,
378        })
379    }
380}
381
382impl From<StartInfo> for fcrunner::ComponentStartInfo {
383    fn from(start_info: StartInfo) -> Self {
384        Self {
385            resolved_url: Some(start_info.resolved_url),
386            program: Some(start_info.program),
387            ns: Some(start_info.namespace),
388            outgoing_dir: start_info.outgoing_dir,
389            runtime_dir: start_info.runtime_dir,
390            numbered_handles: Some(start_info.numbered_handles),
391            encoded_config: start_info.encoded_config,
392            break_on_start: start_info.break_on_start,
393            ..Default::default()
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401    use test_case::test_case;
402
403    #[test_case(Some("some_url"), Some("some_url".to_owned()) ; "when url is valid")]
404    #[test_case(None, None ; "when url is missing")]
405    fn get_resolved_url_test(maybe_url: Option<&str>, expected: Option<String>) {
406        let start_info = fcrunner::ComponentStartInfo {
407            resolved_url: maybe_url.map(str::to_owned),
408            program: None,
409            ns: None,
410            outgoing_dir: None,
411            runtime_dir: None,
412            ..Default::default()
413        };
414        assert_eq!(get_resolved_url(&start_info), expected,);
415    }
416
417    #[test_case(Some("bin/myexecutable"), Ok("bin/myexecutable".to_owned()) ; "when binary value is valid")]
418    #[test_case(Some("/bin/myexecutable"), Err(StartInfoProgramError::BinaryPathNotRelative) ; "when binary path is not relative")]
419    #[test_case(None, Err(StartInfoProgramError::NotFound) ; "when program stanza is not set")]
420    fn get_program_binary_test(
421        maybe_value: Option<&str>,
422        expected: Result<String, StartInfoProgramError>,
423    ) {
424        let start_info = match maybe_value {
425            Some(value) => new_start_info(Some(new_program_stanza("binary", value))),
426            None => new_start_info(None),
427        };
428        assert_eq!(get_program_binary(&start_info), expected);
429    }
430
431    #[test]
432    fn get_program_binary_test_when_binary_key_is_missing() {
433        let start_info = new_start_info(Some(new_program_stanza("some_other_key", "bin/foo")));
434        assert_eq!(get_program_binary(&start_info), Err(StartInfoProgramError::MissingBinary));
435    }
436
437    #[test_case("bin/myexecutable", Ok("bin/myexecutable".to_owned()) ; "when binary value is valid")]
438    #[test_case("/bin/myexecutable", Err(StartInfoProgramError::BinaryPathNotRelative) ; "when binary path is not relative")]
439    fn get_program_binary_from_dict_test(
440        value: &str,
441        expected: Result<String, StartInfoProgramError>,
442    ) {
443        let program = new_program_stanza("binary", value);
444        assert_eq!(get_program_binary_from_dict(&program), expected);
445    }
446
447    #[test]
448    fn get_program_binary_from_dict_test_when_binary_key_is_missing() {
449        let program = new_program_stanza("some_other_key", "bin/foo");
450        assert_eq!(
451            get_program_binary_from_dict(&program),
452            Err(StartInfoProgramError::MissingBinary)
453        );
454    }
455
456    #[test_case(&[], vec![] ; "when args is empty")]
457    #[test_case(&["a".to_owned()], vec!["a".to_owned()] ; "when args is a")]
458    #[test_case(&["a".to_owned(), "b".to_owned()], vec!["a".to_owned(), "b".to_owned()] ; "when args a and b")]
459    fn get_program_args_test(args: &[String], expected: Vec<String>) {
460        let start_info =
461            new_start_info(Some(new_program_stanza_with_vec(ARGS_KEY, Vec::from(args))));
462        assert_eq!(get_program_args(&start_info).unwrap(), expected);
463    }
464
465    #[test_case(&[], vec![] ; "when args is empty")]
466    #[test_case(&["a".to_owned()], vec!["a".to_owned()] ; "when args is a")]
467    #[test_case(&["a".to_owned(), "b".to_owned()], vec!["a".to_owned(), "b".to_owned()] ; "when args a and b")]
468    fn get_program_args_from_dict_test(args: &[String], expected: Vec<String>) {
469        let program = new_program_stanza_with_vec(ARGS_KEY, Vec::from(args));
470        assert_eq!(get_program_args_from_dict(&program).unwrap(), expected);
471    }
472
473    #[test]
474    fn get_program_args_invalid() {
475        let program = fdata::Dictionary {
476            entries: Some(vec![fdata::DictionaryEntry {
477                key: ARGS_KEY.to_string(),
478                value: Some(Box::new(fdata::DictionaryValue::Str("hello".to_string()))),
479            }]),
480            ..Default::default()
481        };
482        assert_eq!(
483            get_program_args_from_dict(&program),
484            Err(StartInfoProgramError::InvalidStrVec(ARGS_KEY.to_string()))
485        );
486    }
487
488    #[test_case(fdata::DictionaryValue::StrVec(vec!["foo=bar".to_owned(), "bar=baz".to_owned()]), Ok(Some(vec!["foo=bar".to_owned(), "bar=baz".to_owned()])); "when_values_are_valid")]
489    #[test_case(fdata::DictionaryValue::StrVec(vec![]), Ok(None); "when_value_is_empty")]
490    #[test_case(fdata::DictionaryValue::StrVec(vec!["=bad".to_owned()]), Err(StartInfoProgramError::InvalidEnvironValue(0)); "for_environ_with_empty_left_hand_side")]
491    #[test_case(fdata::DictionaryValue::StrVec(vec!["good=".to_owned()]), Ok(Some(vec!["good=".to_owned()])); "for_environ_with_empty_right_hand_side")]
492    #[test_case(fdata::DictionaryValue::StrVec(vec!["no_equal_sign".to_owned()]), Err(StartInfoProgramError::InvalidEnvironValue(0)); "for_environ_with_no_delimiter")]
493    #[test_case(fdata::DictionaryValue::StrVec(vec!["foo=bar=baz".to_owned()]), Ok(Some(vec!["foo=bar=baz".to_owned()])); "for_environ_with_multiple_delimiters")]
494    #[test_case(fdata::DictionaryValue::Str("foo=bar".to_owned()), Err(StartInfoProgramError::InvalidValue(ENVIRON_KEY.to_owned(), "vector of string".to_owned(), "string".to_owned())); "for_environ_as_invalid_type")]
495    fn get_environ_test(
496        value: fdata::DictionaryValue,
497        expected: Result<Option<Vec<String>>, StartInfoProgramError>,
498    ) {
499        let program = fdata::Dictionary {
500            entries: Some(vec![fdata::DictionaryEntry {
501                key: ENVIRON_KEY.to_owned(),
502                value: Some(Box::new(value)),
503            }]),
504            ..Default::default()
505        };
506
507        assert_eq!(get_environ(&program), expected);
508    }
509
510    fn new_start_info(program: Option<fdata::Dictionary>) -> fcrunner::ComponentStartInfo {
511        fcrunner::ComponentStartInfo {
512            program: program,
513            ns: None,
514            outgoing_dir: None,
515            runtime_dir: None,
516            resolved_url: None,
517            ..Default::default()
518        }
519    }
520
521    fn new_program_stanza(key: &str, value: &str) -> fdata::Dictionary {
522        fdata::Dictionary {
523            entries: Some(vec![fdata::DictionaryEntry {
524                key: key.to_owned(),
525                value: Some(Box::new(fdata::DictionaryValue::Str(value.to_owned()))),
526            }]),
527            ..Default::default()
528        }
529    }
530
531    fn new_program_stanza_with_vec(key: &str, values: Vec<String>) -> fdata::Dictionary {
532        fdata::Dictionary {
533            entries: Some(vec![fdata::DictionaryEntry {
534                key: key.to_owned(),
535                value: Some(Box::new(fdata::DictionaryValue::StrVec(values))),
536            }]),
537            ..Default::default()
538        }
539    }
540}