Skip to main content

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