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