1pub 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#[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
52pub fn get_resolved_url(start_info: &fcrunner::ComponentStartInfo) -> Option<String> {
54 start_info.resolved_url.clone()
55}
56
57pub 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
72pub 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
95pub 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
103pub 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
115pub 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
127pub 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
142pub 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
154pub 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
173pub 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
184pub 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 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#[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: _, }) => 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#[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 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
276pub struct StartInfo {
279 pub resolved_url: String,
284
285 pub program: fdata::Dictionary,
288
289 pub namespace: Vec<fcrunner::ComponentNamespaceEntry>,
310
311 pub outgoing_dir: Option<ServerEnd<fio::DirectoryMarker>>,
313
314 pub runtime_dir: Option<ServerEnd<fio::DirectoryMarker>>,
318
319 pub numbered_handles: Vec<fprocess::HandleInfo>,
324
325 pub encoded_config: Option<fmem::Data>,
336
337 pub break_on_start: Option<zx::EventPair>,
346
347 #[cfg(fuchsia_api_level_at_least = "HEAD")]
357 pub component_instance: Option<zx::Event>,
358
359 #[cfg(fuchsia_api_level_at_least = "HEAD")]
363 pub escrowed_dictionary: Option<fsandbox::DictionaryRef>,
364
365 #[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}