1use crate::features::{Feature, FeatureSet};
6use crate::{
7 offer_to_all_would_duplicate, AnyRef, Availability, Capability, CapabilityClause,
8 CapabilityFromRef, CapabilityId, Child, Collection, ConfigKey, ConfigType, ConfigValueType,
9 DependencyType, DictionaryRef, Document, Environment, EnvironmentExtends, EnvironmentRef,
10 Error, EventScope, Expose, ExposeFromRef, ExposeToRef, FromClause, Name, Offer, OfferFromRef,
11 OfferToRef, OneOrMany, Program, RegistrationRef, Rights, RootDictionaryRef, SourceAvailability,
12 Use, UseFromRef,
13};
14use cm_types::IterablePath;
15use directed_graph::DirectedGraph;
16use itertools::Either;
17use std::collections::{BTreeMap, HashMap, HashSet};
18use std::hash::Hash;
19use std::path::Path;
20use std::{fmt, iter};
21
22#[derive(Default, Clone)]
23pub struct CapabilityRequirements<'a> {
24 pub must_offer: &'a [OfferToAllCapability<'a>],
25 pub must_use: &'a [MustUseRequirement<'a>],
26}
27
28#[derive(PartialEq)]
29pub enum MustUseRequirement<'a> {
30 Protocol(&'a str),
31}
32
33impl<'a> MustUseRequirement<'a> {
34 fn name(&self) -> &str {
35 match self {
36 MustUseRequirement::Protocol(name) => name,
37 }
38 }
39}
40
41#[derive(PartialEq, Clone)]
42pub enum OfferToAllCapability<'a> {
43 Dictionary(&'a str),
44 Protocol(&'a str),
45}
46
47impl<'a> OfferToAllCapability<'a> {
48 pub fn name(&self) -> &'a str {
49 match self {
50 OfferToAllCapability::Dictionary(name) => name,
51 OfferToAllCapability::Protocol(name) => name,
52 }
53 }
54
55 pub fn offer_type(&self) -> &'static str {
56 match self {
57 OfferToAllCapability::Dictionary(_) => "Dictionary",
58 OfferToAllCapability::Protocol(_) => "Protocol",
59 }
60 }
61
62 pub fn offer_type_plural(&self) -> &'static str {
63 match self {
64 OfferToAllCapability::Dictionary(_) => "dictionaries",
65 OfferToAllCapability::Protocol(_) => "protocols",
66 }
67 }
68}
69
70pub fn offer_to_all_from_offer(value: &Offer) -> impl Iterator<Item = OfferToAllCapability<'_>> {
71 if let Some(protocol) = &value.protocol {
72 Either::Left(
73 protocol.iter().map(|protocol| OfferToAllCapability::Protocol(protocol.as_str())),
74 )
75 } else if let Some(dictionary) = &value.dictionary {
76 Either::Right(
77 dictionary
78 .iter()
79 .map(|dictionary| OfferToAllCapability::Dictionary(dictionary.as_str())),
80 )
81 } else {
82 panic!("Expected a dictionary or a protocol");
83 }
84}
85
86pub(crate) fn validate_cml(
88 document: &Document,
89 file: Option<&Path>,
90 features: &FeatureSet,
91 capability_requirements: &CapabilityRequirements<'_>,
92) -> Result<(), Error> {
93 let mut ctx = ValidationContext::new(&document, features, capability_requirements);
94 let mut res = ctx.validate();
95 if let Err(Error::Validate { filename, .. }) = &mut res {
96 if let Some(file) = file {
97 *filename = Some(file.to_string_lossy().into_owned());
98 }
99 }
100 res
101}
102
103fn duplicate_capability_check<'a>(
104 duplicate_check: &mut HashSet<CapabilityId<'a>>,
105 capability_string: &str,
106 offered_to_all: &Vec<&'a Offer>,
107 filter: impl Fn(&&&Offer) -> bool,
108) -> Result<(), Error> {
109 let problem_capabilities = offered_to_all
110 .iter()
111 .filter(filter)
112 .map(|o| CapabilityId::from_offer_expose(*o))
113 .collect::<Result<Vec<Vec<CapabilityId<'a>>>, _>>()?
114 .into_iter()
115 .flatten()
116 .filter(|cap_id| !duplicate_check.insert((*cap_id).clone()))
117 .collect::<Vec<_>>();
118 if !problem_capabilities.is_empty() {
119 return Err(Error::validate(format!(
120 r#"{} {:?} offered to "all" multiple times"#,
121 capability_string,
122 problem_capabilities.iter().map(|p| format!("{p}")).collect::<Vec<_>>()
123 )));
124 }
125 Ok(())
126}
127
128fn offer_can_have_dependency(offer: &Offer) -> bool {
129 offer.directory.is_some() || offer.protocol.is_some() || offer.service.is_some()
130}
131
132fn offer_dependency(offer: &Offer) -> DependencyType {
133 offer.dependency.clone().unwrap_or(DependencyType::Strong)
134}
135struct ValidationContext<'a> {
136 document: &'a Document,
137 features: &'a FeatureSet,
138 capability_requirements: &'a CapabilityRequirements<'a>,
139 all_children: HashMap<&'a Name, &'a Child>,
140 all_collections: HashSet<&'a Name>,
141 all_storages: HashMap<&'a Name, &'a CapabilityFromRef>,
142 all_services: HashSet<&'a Name>,
143 all_protocols: HashSet<&'a Name>,
144 all_directories: HashSet<&'a Name>,
145 all_runners: HashSet<&'a Name>,
146 all_resolvers: HashSet<&'a Name>,
147 all_dictionaries: HashMap<&'a Name, &'a Capability>,
148 all_configs: HashSet<&'a Name>,
149 all_environment_names: HashSet<&'a Name>,
150 all_capability_names: HashSet<&'a Name>,
151 strong_dependencies: DirectedGraph<DependencyNode<'a>>,
152}
153
154const TEST_FACET_KEY: &'static str = "fuchsia.test";
156
157const TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: &'static str = "deprecated-allowed-packages";
159
160const TEST_TYPE_FACET_KEY: &'static str = "type";
162
163impl<'a> ValidationContext<'a> {
164 fn new(
165 document: &'a Document,
166 features: &'a FeatureSet,
167 capability_requirements: &'a CapabilityRequirements<'a>,
168 ) -> Self {
169 ValidationContext {
170 document,
171 features,
172 capability_requirements,
173 all_children: HashMap::new(),
174 all_collections: HashSet::new(),
175 all_storages: HashMap::new(),
176 all_services: HashSet::new(),
177 all_protocols: HashSet::new(),
178 all_directories: HashSet::new(),
179 all_runners: HashSet::new(),
180 all_resolvers: HashSet::new(),
181 all_dictionaries: HashMap::new(),
182 all_configs: HashSet::new(),
183 all_environment_names: HashSet::new(),
184 all_capability_names: HashSet::new(),
185 strong_dependencies: DirectedGraph::new(),
186 }
187 }
188
189 fn validate(&mut self) -> Result<(), Error> {
190 let all_children_names =
197 self.document.all_children_names().into_iter().zip(iter::repeat("children"));
198 let all_collection_names =
199 self.document.all_collection_names().into_iter().zip(iter::repeat("collections"));
200 let all_storage_names =
201 self.document.all_storage_names().into_iter().zip(iter::repeat("storage"));
202 let all_runner_names =
203 self.document.all_runner_names().into_iter().zip(iter::repeat("runners"));
204 let all_resolver_names =
205 self.document.all_resolver_names().into_iter().zip(iter::repeat("resolvers"));
206 let all_environment_names =
207 self.document.all_environment_names().into_iter().zip(iter::repeat("environments"));
208 let all_dictionary_names =
209 self.document.all_dictionary_names().into_iter().zip(iter::repeat("dictionaries"));
210 ensure_no_duplicate_names(
211 all_children_names
212 .chain(all_collection_names)
213 .chain(all_storage_names)
214 .chain(all_runner_names)
215 .chain(all_resolver_names)
216 .chain(all_environment_names)
217 .chain(all_dictionary_names),
218 )?;
219
220 if let Some(children) = &self.document.children {
222 self.all_children = children.iter().map(|c| (&c.name, c)).collect();
223 }
224 self.all_collections = self.document.all_collection_names().into_iter().collect();
225 self.all_storages = self.document.all_storage_with_sources();
226 self.all_services = self.document.all_service_names().into_iter().collect();
227 self.all_protocols = self.document.all_protocol_names().into_iter().collect();
228 self.all_directories = self.document.all_directory_names().into_iter().collect();
229 self.all_runners = self.document.all_runner_names().into_iter().collect();
230 self.all_resolvers = self.document.all_resolver_names().into_iter().collect();
231 self.all_dictionaries = self.document.all_dictionaries().into_iter().collect();
232 self.all_configs = self.document.all_config_names().into_iter().collect();
233 self.all_environment_names = self.document.all_environment_names().into_iter().collect();
234 self.all_capability_names = self.document.all_capability_names();
235
236 if let Some(children) = &self.document.children {
238 for child in children {
239 self.validate_child(&child)?;
240 }
241 }
242
243 if let Some(collections) = &self.document.collections {
245 for collection in collections {
246 self.validate_collection(&collection)?;
247 }
248 }
249
250 if let Some(capabilities) = self.document.capabilities.as_ref() {
252 let mut used_ids = HashMap::new();
253 for capability in capabilities {
254 self.validate_capability(capability, &mut used_ids)?;
255 }
256 }
257
258 let mut uses_runner = false;
260 if let Some(uses) = self.document.r#use.as_ref() {
261 let mut used_ids = HashMap::new();
262 for use_ in uses.iter() {
263 self.validate_use(&use_, &mut used_ids)?;
264 if use_.runner.is_some() {
265 uses_runner = true;
266 }
267 }
268 }
269
270 if let Some(exposes) = self.document.expose.as_ref() {
272 let mut used_ids = HashMap::new();
273 let mut exposed_to_framework_ids = HashMap::new();
274 for expose in exposes.iter() {
275 self.validate_expose(&expose, &mut used_ids, &mut exposed_to_framework_ids)?;
276 }
277 }
278
279 if let Some(offers) = self.document.offer.as_ref() {
281 let mut used_ids = HashMap::new();
282 let offered_to_all = offers
283 .iter()
284 .filter(|o| matches!(o.to, OneOrMany::One(OfferToRef::All)))
285 .filter(|o| o.protocol.is_some() || o.dictionary.is_some())
286 .collect::<Vec<&Offer>>();
287
288 let mut duplicate_check: HashSet<CapabilityId<'a>> = HashSet::new();
289
290 duplicate_capability_check(
291 &mut duplicate_check,
292 "Protocol(s)",
293 &offered_to_all,
294 |o| o.protocol.is_some(),
295 )?;
296 duplicate_capability_check(
297 &mut duplicate_check,
298 "Dictionary(s)",
299 &offered_to_all,
300 |o| o.dictionary.is_some(),
301 )?;
302
303 for offer in offers.iter() {
304 self.validate_offer(&offer, &mut used_ids, &offered_to_all)?;
305 }
306 }
307
308 if uses_runner {
309 self.validate_runner_not_specified(self.document.program.as_ref())?;
312 } else {
313 self.validate_runner_specified(self.document.program.as_ref())?;
316 }
317
318 if let Some(environments) = &self.document.environments {
320 for env in environments {
321 self.validate_environment(&env)?;
322 }
323 }
324
325 self.validate_config(&self.document.config)?;
327
328 match self.strong_dependencies.topological_sort() {
330 Ok(_) => {}
331 Err(e) => {
332 return Err(Error::validate(format!(
333 "Strong dependency cycles were found. Break the cycle by removing a dependency or marking an offer as weak. Cycles: {}", e.format_cycle())));
334 }
335 }
336
337 self.validate_required_offer_decls()?;
339
340 self.validate_required_use_decls()?;
342
343 self.validate_facets()?;
344
345 Ok(())
346 }
347
348 fn get_test_facet(&self) -> Option<&serde_json::Value> {
349 match &self.document.facets {
350 Some(m) => m.get(TEST_FACET_KEY),
351 None => None,
352 }
353 }
354
355 fn validate_facets(&self) -> Result<(), Error> {
356 let test_facet_map = {
357 let test_facet = self.get_test_facet();
358 match &test_facet {
359 None => None,
360 Some(serde_json::Value::Object(m)) => Some(m),
361 Some(facet) => {
362 return Err(Error::validate(format!(
363 "'{TEST_FACET_KEY}' is not an object: {facet:?}"
364 )))
365 }
366 }
367 };
368
369 let restrict_test_type = self.features.has(&Feature::RestrictTestTypeInFacet);
370 let enable_allow_non_hermetic_packages =
371 self.features.has(&Feature::EnableAllowNonHermeticPackagesFeature);
372
373 if restrict_test_type {
374 let test_type = test_facet_map.map(|m| m.get(TEST_TYPE_FACET_KEY)).flatten();
375 if test_type.is_some() {
376 return Err(Error::validate(format!(
377 "'{}' is not a allowed in facets. Refer \
378https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#non-hermetic_tests \
379to run your test in the correct test realm.", TEST_TYPE_FACET_KEY)));
380 }
381 }
382
383 if enable_allow_non_hermetic_packages {
384 let allow_non_hermetic_packages = self.features.has(&Feature::AllowNonHermeticPackages);
385 let deprecated_allowed_packages = test_facet_map
386 .map_or(false, |m| m.contains_key(TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY));
387 if deprecated_allowed_packages && !allow_non_hermetic_packages {
388 return Err(Error::validate(format!(
389 "restricted_feature '{}' should be present with facet '{}'",
390 Feature::AllowNonHermeticPackages,
391 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
392 )));
393 }
394 if allow_non_hermetic_packages && !deprecated_allowed_packages {
395 return Err(Error::validate(format!(
396 "Remove restricted_feature '{}' as manifest does not contain facet '{}'",
397 Feature::AllowNonHermeticPackages,
398 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
399 )));
400 }
401 }
402 Ok(())
403 }
404
405 fn validate_child(&mut self, child: &'a Child) -> Result<(), Error> {
406 if let Some(resource) = child.url.resource() {
407 if resource.ends_with(".cml") {
408 return Err(Error::validate(format!(
409 "child URL ends in .cml instead of .cm, \
410which is almost certainly a mistake: {}",
411 child.url
412 )));
413 }
414 }
415
416 if let Some(environment_ref) = &child.environment {
417 match environment_ref {
418 EnvironmentRef::Named(environment_name) => {
419 if !self.all_environment_names.contains(&environment_name) {
420 return Err(Error::validate(format!(
421 "\"{}\" does not appear in \"environments\"",
422 &environment_name
423 )));
424 }
425 let source = DependencyNode::Named(&environment_name);
426 let target = DependencyNode::Named(&child.name);
427 self.add_strong_dep(source, target);
428 }
429 }
430 }
431 Ok(())
432 }
433
434 fn validate_collection(&mut self, collection: &'a Collection) -> Result<(), Error> {
435 if collection.allow_long_names.is_some() {
436 self.features.check(Feature::AllowLongNames)?;
437 }
438 if let Some(environment_ref) = &collection.environment {
439 match environment_ref {
440 EnvironmentRef::Named(environment_name) => {
441 if !self.all_environment_names.contains(&environment_name) {
442 return Err(Error::validate(format!(
443 "\"{}\" does not appear in \"environments\"",
444 &environment_name
445 )));
446 }
447 let source = DependencyNode::Named(&environment_name);
448 let target = DependencyNode::Named(&collection.name);
449 self.add_strong_dep(source, target);
450 }
451 }
452 }
453 Ok(())
454 }
455
456 fn validate_capability(
457 &mut self,
458 capability: &'a Capability,
459 used_ids: &mut HashMap<String, CapabilityId<'a>>,
460 ) -> Result<(), Error> {
461 if capability.directory.is_some() && capability.path.is_none() {
462 return Err(Error::validate("\"path\" should be present with \"directory\""));
463 }
464 if capability.directory.is_some() && capability.rights.is_none() {
465 return Err(Error::validate("\"rights\" should be present with \"directory\""));
466 }
467 if let Some(name) = capability.storage.as_ref() {
468 if capability.from.is_none() {
469 return Err(Error::validate("\"from\" should be present with \"storage\""));
470 }
471 if capability.path.is_some() {
472 return Err(Error::validate(
473 "\"path\" can not be present with \"storage\", use \"backing_dir\"",
474 ));
475 }
476 if capability.backing_dir.is_none() {
477 return Err(Error::validate("\"backing_dir\" should be present with \"storage\""));
478 }
479 if capability.storage_id.is_none() {
480 return Err(Error::validate("\"storage_id\" should be present with \"storage\""));
481 }
482
483 let target = DependencyNode::Named(name);
485 let source = capability.from.as_ref().unwrap();
486 let names = vec![capability.backing_dir.as_ref().unwrap()];
487 for source in self.expand_source_dependencies(names, &source.into()) {
488 self.add_strong_dep(source, target);
489 }
490 }
491 if capability.runner.is_some() && capability.from.is_some() {
492 return Err(Error::validate("\"from\" should not be present with \"runner\""));
493 }
494 if capability.runner.is_some() && capability.path.is_none() {
495 return Err(Error::validate("\"path\" should be present with \"runner\""));
496 }
497 if capability.resolver.is_some() && capability.from.is_some() {
498 return Err(Error::validate("\"from\" should not be present with \"resolver\""));
499 }
500 if capability.resolver.is_some() && capability.path.is_none() {
501 return Err(Error::validate("\"path\" should be present with \"resolver\""));
502 }
503
504 if let Some(name) = capability.dictionary.as_ref() {
505 if capability.path.is_some() {
506 self.features.check(Feature::DynamicDictionaries)?;
507 let target = DependencyNode::Named(name);
510 self.add_strong_dep(DependencyNode::Self_, target);
511 }
512 }
513 if capability.delivery.is_some() {
514 self.features.check(Feature::DeliveryType)?;
515 }
516 if let Some(from) = capability.from.as_ref() {
517 self.validate_component_child_ref("\"capabilities\" source", &AnyRef::from(from))?;
518 }
519
520 let capability_ids = CapabilityId::from_capability(capability)?;
522 for capability_id in capability_ids {
523 if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
524 return Err(Error::validate(format!(
525 "\"{}\" is a duplicate \"capability\" name",
526 capability_id,
527 )));
528 }
529 }
530
531 Ok(())
532 }
533
534 fn validate_use(
535 &mut self,
536 use_: &'a Use,
537 used_ids: &mut HashMap<String, CapabilityId<'a>>,
538 ) -> Result<(), Error> {
539 use_.capability_type()?;
540 for checker in [
541 self.service_from_self_checker(use_),
542 self.protocol_from_self_checker(use_),
543 self.directory_from_self_checker(use_),
544 self.config_from_self_checker(use_),
545 ] {
546 checker.validate("used")?;
547 }
548
549 if use_.from == Some(UseFromRef::Debug) && use_.protocol.is_none() {
550 return Err(Error::validate("only \"protocol\" supports source from \"debug\""));
551 }
552 if use_.event_stream.is_some() && use_.availability.is_some() {
553 return Err(Error::validate("\"availability\" cannot be used with \"event_stream\""));
554 }
555 if use_.event_stream.is_none() && use_.filter.is_some() {
556 return Err(Error::validate("\"filter\" can only be used with \"event_stream\""));
557 }
558 if use_.storage.is_some() && use_.from.is_some() {
559 return Err(Error::validate("\"from\" cannot be used with \"storage\""));
560 }
561 if use_.runner.is_some() && use_.availability.is_some() {
562 return Err(Error::validate("\"availability\" cannot be used with \"runner\""));
563 }
564 if use_.from == Some(UseFromRef::Self_) && use_.event_stream.is_some() {
565 return Err(Error::validate("\"from: self\" cannot be used with \"event_stream\""));
566 }
567 if use_.from == Some(UseFromRef::Self_) && use_.runner.is_some() {
568 return Err(Error::validate("\"from: self\" cannot be used with \"runner\""));
569 }
570 if use_.availability == Some(Availability::SameAsTarget) {
571 return Err(Error::validate(
572 "\"availability: same_as_target\" cannot be used with use declarations",
573 ));
574 }
575 if let Some(UseFromRef::Dictionary(_)) = use_.from.as_ref() {
576 if use_.storage.is_some() {
577 return Err(Error::validate(
578 "Dictionaries do not support \"storage\" capabilities",
579 ));
580 }
581 if use_.event_stream.is_some() {
582 return Err(Error::validate(
583 "Dictionaries do not support \"event_stream\" capabilities",
584 ));
585 }
586 }
587 if let Some(config) = use_.config.as_ref() {
588 if use_.key == None {
589 return Err(Error::validate(format!("Config '{}' missing field 'key'", config)));
590 }
591 let _ = use_config_to_value_type(use_)?;
592 let availability = use_.availability.unwrap_or(Availability::Required);
593 if availability == Availability::Required && use_.config_default.is_some() {
594 return Err(Error::validate(format!(
595 "Config '{}' is required and has a default value",
596 config
597 )));
598 }
599 }
600
601 if let Some(source) = use_.from.as_ref() {
602 for source in self.expand_source_dependencies(use_.names(), &source.into()) {
603 let target = DependencyNode::Self_;
604 if use_.dependency.as_ref().unwrap_or(&DependencyType::Strong)
605 == &DependencyType::Strong
606 {
607 self.add_strong_dep(source, target);
608 }
609 }
610 }
611
612 let capability_ids = CapabilityId::from_use(use_)?;
614 for capability_id in capability_ids {
615 if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
616 return Err(Error::validate(format!(
617 "\"{}\" is a duplicate \"use\" target {}",
618 capability_id,
619 capability_id.type_str()
620 )));
621 }
622 let dir = capability_id.get_dir_path();
623
624 let pkg_path = cm_types::NamespacePath::new("/pkg").unwrap();
626 if let Some(ref dir) = dir {
627 if dir.has_prefix(&pkg_path) {
628 return Err(Error::validate(format!(
629 "{} \"{}\" conflicts with the protected path \"/pkg\", please use this capability with a different path",
630 capability_id.type_str(), capability_id,
631 )));
632 }
633 }
634
635 for (_, used_id) in used_ids.iter() {
638 if capability_id == *used_id {
639 continue;
640 }
641 let Some(ref dir) = dir else {
642 continue;
643 };
644 let Some(used_dir) = used_id.get_dir_path() else {
645 continue;
646 };
647
648 if match (used_id, &capability_id) {
649 (CapabilityId::UsedDirectory(_), CapabilityId::UsedStorage(_))
651 | (CapabilityId::UsedStorage(_), CapabilityId::UsedDirectory(_))
652 | (CapabilityId::UsedDirectory(_), CapabilityId::UsedDirectory(_))
653 | (CapabilityId::UsedStorage(_), CapabilityId::UsedStorage(_)) => {
654 dir.has_prefix(&used_dir) || used_dir.has_prefix(&dir)
655 }
656
657 (CapabilityId::UsedDirectory(_), _)
659 | (CapabilityId::UsedStorage(_), _)
660 | (_, CapabilityId::UsedDirectory(_))
661 | (_, CapabilityId::UsedStorage(_)) => {
662 dir.has_prefix(&used_dir) || used_dir.has_prefix(&dir)
663 }
664
665 (_, _) => {
668 *dir != used_dir && (dir.has_prefix(&used_dir) || used_dir.has_prefix(&dir))
669 }
670 } {
671 return Err(Error::validate(format!(
672 "{} \"{}\" is a prefix of \"use\" target {} \"{}\"",
673 used_id.type_str(),
674 used_id,
675 capability_id.type_str(),
676 capability_id,
677 )));
678 }
679 }
680 }
681
682 if let Some(_) = use_.directory.as_ref() {
683 match &use_.rights {
685 Some(rights) => self.validate_directory_rights(&rights)?,
686 None => {
687 return Err(Error::validate("This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer."))
688 }
689 };
690 }
691
692 match (&use_.from, &use_.dependency) {
693 (Some(UseFromRef::Named(name)), _) if use_.service.is_some() => {
694 self.validate_component_child_or_collection_ref(
695 "\"use\" source",
696 &AnyRef::Named(name),
697 )?;
698 }
699 (Some(UseFromRef::Named(name)), _) => {
700 self.validate_component_child_or_capability_ref(
701 "\"use\" source",
702 &AnyRef::Named(name),
703 )?;
704 }
705 (_, Some(DependencyType::Weak)) => {
706 return Err(Error::validate(format!(
707 "Only `use` from children can have dependency: \"weak\""
708 )));
709 }
710 _ => {}
711 }
712 Ok(())
713 }
714
715 fn validate_expose(
716 &self,
717 expose: &'a Expose,
718 used_ids: &mut HashMap<String, CapabilityId<'a>>,
719 exposed_to_framework_ids: &mut HashMap<String, CapabilityId<'a>>,
720 ) -> Result<(), Error> {
721 expose.capability_type()?;
722 for checker in [
723 self.service_from_self_checker(expose),
724 self.protocol_from_self_checker(expose),
725 self.directory_from_self_checker(expose),
726 self.runner_from_self_checker(expose),
727 self.resolver_from_self_checker(expose),
728 self.dictionary_from_self_checker(expose),
729 self.config_from_self_checker(expose),
730 ] {
731 checker.validate("exposed")?;
732 }
733
734 if let Some(_) = expose.directory.as_ref() {
736 if expose.from.iter().any(|r| *r == ExposeFromRef::Self_) || expose.rights.is_some() {
737 if let Some(rights) = expose.rights.as_ref() {
738 self.validate_directory_rights(&rights)?;
739 }
740 }
741
742 if expose.to == Some(ExposeToRef::Framework) {
745 if expose.subdir.is_some() {
746 return Err(Error::validate(
747 "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
748 ));
749 }
750 }
751 }
752
753 if let Some(event_stream) = &expose.event_stream {
754 if event_stream.iter().len() > 1 && expose.r#as.is_some() {
755 return Err(Error::validate(format!(
756 "as cannot be used with multiple event streams"
757 )));
758 }
759 if let Some(ExposeToRef::Framework) = &expose.to {
760 return Err(Error::validate(format!("cannot expose an event_stream to framework")));
761 }
762 for from in expose.from.iter() {
763 if from == &ExposeFromRef::Self_ {
764 return Err(Error::validate(format!("Cannot expose event_streams from self")));
765 }
766 }
767 if let Some(scopes) = &expose.scope {
768 for scope in scopes {
769 match scope {
770 EventScope::Named(name) => {
771 if !self.all_children.contains_key(name)
772 && !self.all_collections.contains(name)
773 {
774 return Err(Error::validate(format!("event_stream scope {} did not match a component or collection in this .cml file.", name.as_str())));
775 }
776 }
777 }
778 }
779 }
780 }
781
782 for ref_ in expose.from.iter() {
783 if let ExposeFromRef::Dictionary(d) = ref_ {
784 if expose.event_stream.is_some() {
785 return Err(Error::validate(
786 "Dictionaries do not support \"event_stream\" capabilities",
787 ));
788 }
789 match &d.root {
790 RootDictionaryRef::Self_ | RootDictionaryRef::Named(_) => {}
791 RootDictionaryRef::Parent => {
792 return Err(Error::validate(
793 "`expose` dictionary path must begin with `self` or `#<child-name>`",
794 ));
795 }
796 }
797 }
798 }
799
800 let capability_ids = CapabilityId::from_offer_expose(expose)?;
802 for capability_id in capability_ids {
803 let mut ids = &mut *used_ids;
804 if expose.to == Some(ExposeToRef::Framework) {
805 ids = &mut *exposed_to_framework_ids;
806 }
807 if ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
808 if let CapabilityId::Service(_) = capability_id {
809 } else {
811 return Err(Error::validate(format!(
812 "\"{}\" is a duplicate \"expose\" target capability for \"{}\"",
813 capability_id,
814 expose.to.as_ref().unwrap_or(&ExposeToRef::Parent)
815 )));
816 }
817 }
818 }
819
820 self.validate_from_clause(
823 "expose",
824 expose,
825 &expose.source_availability,
826 &expose.availability,
827 )?;
828
829 Ok(())
830 }
831
832 fn validate_offer(
833 &mut self,
834 offer: &'a Offer,
835 used_ids: &mut HashMap<Name, HashMap<String, CapabilityId<'a>>>,
836 protocols_offered_to_all: &[&'a Offer],
837 ) -> Result<(), Error> {
838 offer.capability_type()?;
839 for checker in [
840 self.service_from_self_checker(offer),
841 self.protocol_from_self_checker(offer),
842 self.directory_from_self_checker(offer),
843 self.storage_from_self_checker(offer),
844 self.runner_from_self_checker(offer),
845 self.resolver_from_self_checker(offer),
846 self.dictionary_from_self_checker(offer),
847 self.config_from_self_checker(offer),
848 ] {
849 checker.validate("offered")?;
850 }
851
852 if let Some(stream) = offer.event_stream.as_ref() {
853 if stream.iter().len() > 1 && offer.r#as.is_some() {
854 return Err(Error::validate(format!("as cannot be used with multiple events")));
855 }
856 for from in &offer.from {
857 match from {
858 OfferFromRef::Self_ => {
859 return Err(Error::validate(format!(
860 "cannot offer an event_stream from self"
861 )));
862 }
863 _ => {}
864 }
865 }
866 }
867
868 if let Some(_) = offer.directory.as_ref() {
870 if offer.from.iter().any(|r| *r == OfferFromRef::Self_) || offer.rights.is_some() {
871 if let Some(rights) = offer.rights.as_ref() {
872 self.validate_directory_rights(&rights)?;
873 }
874 }
875 }
876
877 if let Some(storage) = offer.storage.as_ref() {
878 for storage in storage {
879 if offer.from.iter().any(|r| r.is_named()) {
880 return Err(Error::validate(format!(
881 "Storage \"{}\" is offered from a child, but storage capabilities cannot be exposed", storage)));
882 }
883 }
884 }
885
886 for ref_ in offer.from.iter() {
887 if let OfferFromRef::Dictionary(d) = ref_ {
888 match &d.root {
889 RootDictionaryRef::Self_
890 | RootDictionaryRef::Named(_)
891 | RootDictionaryRef::Parent => {}
892 }
893
894 if offer.storage.is_some() {
895 return Err(Error::validate(
896 "Dictionaries do not support \"storage\" capabilities",
897 ));
898 }
899 if offer.event_stream.is_some() {
900 return Err(Error::validate(
901 "Dictionaries do not support \"event_stream\" capabilities",
902 ));
903 }
904 }
905 }
906
907 if !offer_can_have_dependency(offer) && offer.dependency.is_some() {
909 return Err(Error::validate(
910 "Dependency can only be provided for protocol, directory, and service capabilities",
911 ));
912 }
913
914 let target_cap_ids = CapabilityId::from_offer_expose(offer)?;
916 for to in &offer.to {
917 let to_target = match to {
919 OfferToRef::All => continue,
920 OfferToRef::Named(ref to_target) => {
921 for offer_to_all in protocols_offered_to_all {
924 offer_to_all_would_duplicate(offer_to_all, offer, to_target)?;
925 }
926
927 if self.all_children.contains_key(to_target)
929 || self.all_collections.contains(to_target)
930 {
931 } else {
933 if let OneOrMany::One(from) = &offer.from {
934 return Err(Error::validate(format!(
935 "\"{to}\" is an \"offer\" target from \"{from}\" but it does \
936 not appear in \"children\" or \"collections\"",
937 )));
938 } else {
939 return Err(Error::validate(format!(
940 "\"{to}\" is an \"offer\" target but it does not appear in \
941 \"children\" or \"collections\"",
942 )));
943 }
944 }
945
946 if let Some(storage) = offer.storage.as_ref() {
948 for storage in storage {
949 if let OneOrMany::One(OfferFromRef::Self_) = &offer.from {
952 if let Some(CapabilityFromRef::Named(source)) =
953 self.all_storages.get(storage)
954 {
955 if to_target == source {
956 return Err(Error::validate(format!(
957 "Storage offer target \"{}\" is same as source",
958 to
959 )));
960 }
961 }
962 }
963 }
964 } else {
965 for reference in offer.from.iter() {
966 if offer_dependency(offer) == DependencyType::Weak {
968 continue;
969 }
970 match reference {
971 OfferFromRef::Named(name) if name == to_target => {
972 return Err(Error::validate(format!(
973 "Offer target \"{}\" is same as source",
974 to
975 )));
976 }
977 _ => {}
978 }
979 }
980 }
981 to_target
982 }
983 OfferToRef::OwnDictionary(ref to_target) => {
984 if let Ok(capability_ids) = CapabilityId::from_offer_expose(offer) {
985 for id in capability_ids {
986 match &id {
987 CapabilityId::Protocol(_)
988 | CapabilityId::Dictionary(_)
989 | CapabilityId::Runner(_)
990 | CapabilityId::Resolver(_)
991 | CapabilityId::Configuration(_) => {}
992 CapabilityId::Directory(_)
993 | CapabilityId::Service(_)
994 | CapabilityId::Storage(_)
995 | CapabilityId::EventStream(_) => {
996 let type_name = id.type_str();
997 return Err(Error::validate(format!(
998 "\"offer\" to dictionary \"{to}\" for \"{type_name}\" but \
999 dictionaries do not support this type yet."
1000 )));
1001 }
1002 CapabilityId::UsedService(_)
1003 | CapabilityId::UsedProtocol(_)
1004 | CapabilityId::UsedDirectory(_)
1005 | CapabilityId::UsedStorage(_)
1006 | CapabilityId::UsedEventStream(_)
1007 | CapabilityId::UsedRunner(_)
1008 | CapabilityId::UsedConfiguration(_) => {
1009 unreachable!("this is not a use")
1010 }
1011 }
1012 }
1013 }
1014 let Some(d) = self.all_dictionaries.get(&to_target) else {
1016 return Err(Error::validate(format!(
1017 "\"offer\" has dictionary target \"{to}\" but \"{to_target}\" \
1018 is not a dictionary capability defined by this component"
1019 )));
1020 };
1021 if d.path.is_some() {
1022 return Err(Error::validate(format!(
1023 "\"offer\" has dictionary target \"{to}\" but \"{to_target}\" \
1024 sets \"path\". Therefore, it is a dynamic dictionary that \
1025 does not allow offers into it."
1026 )));
1027 }
1028 to_target
1029 }
1030 };
1031
1032 let ids_for_entity = used_ids.entry(to_target.clone()).or_insert(HashMap::new());
1034 for target_cap_id in &target_cap_ids {
1035 if ids_for_entity.insert(target_cap_id.to_string(), target_cap_id.clone()).is_some()
1036 {
1037 if let CapabilityId::Service(_) = target_cap_id {
1038 } else {
1040 return Err(Error::validate(format!(
1041 "\"{}\" is a duplicate \"offer\" target capability for \"{}\"",
1042 target_cap_id, to
1043 )));
1044 }
1045 }
1046 }
1047
1048 for from in offer.from.iter() {
1051 if offer_dependency(offer) == DependencyType::Strong {
1052 for source in self.expand_source_dependencies(offer.names(), &from.into()) {
1053 let target = DependencyNode::offer_to_ref(to);
1054 self.add_strong_dep(source, target);
1055 }
1056 }
1057 }
1058 }
1059
1060 self.validate_from_clause("offer", offer, &offer.source_availability, &offer.availability)?;
1063
1064 Ok(())
1065 }
1066
1067 fn validate_required_offer_decls(&self) -> Result<(), Error> {
1068 let children_stub = Vec::new();
1069 let children = self.document.children.as_ref().unwrap_or(&children_stub);
1070 let collections_stub = Vec::new();
1071 let collections = self.document.collections.as_ref().unwrap_or(&collections_stub);
1072 let offers_stub = Vec::new();
1073 let offers = self.document.offer.as_ref().unwrap_or(&offers_stub);
1074
1075 for required_offer in self.capability_requirements.must_offer {
1076 for child in children.iter() {
1081 if !offers
1082 .iter()
1083 .any(|offer| Self::has_required_offer(offer, &child.name, required_offer))
1084 {
1085 let capability_type = required_offer.offer_type();
1086 return Err(Error::validate(format!(
1087 r#"{capability_type} "{}" is not offered to child component "{}" but it is a required offer"#,
1088 required_offer.name(),
1089 child.name
1090 )));
1091 }
1092 }
1093
1094 for collection in collections.iter() {
1095 if !offers
1096 .iter()
1097 .any(|offer| Self::has_required_offer(offer, &collection.name, required_offer))
1098 {
1099 let capability_type = required_offer.offer_type();
1100 return Err(Error::validate(format!(
1101 r#"{capability_type} "{}" is not offered to collection "{}" but it is a required offer"#,
1102 required_offer.name(),
1103 collection.name
1104 )));
1105 }
1106 }
1107 }
1108
1109 Ok(())
1110 }
1111
1112 fn has_required_offer(
1113 offer: &Offer,
1114 target_name: &Name,
1115 required_offer: &OfferToAllCapability<'_>,
1116 ) -> bool {
1117 let names_this_collection = offer.to.iter().any(|target| match target {
1118 OfferToRef::Named(ref name) => name == target_name,
1119 OfferToRef::All => true,
1120 OfferToRef::OwnDictionary(_) => false,
1121 });
1122 let capability_names = match required_offer {
1123 OfferToAllCapability::Dictionary(_) => offer.dictionary.as_ref(),
1124 OfferToAllCapability::Protocol(_) => offer.protocol.as_ref(),
1125 };
1126 let names_this_capability = match capability_names.as_ref() {
1127 Some(OneOrMany::Many(names)) => {
1128 names.iter().any(|cap_name| cap_name.as_str() == required_offer.name())
1129 }
1130 Some(OneOrMany::One(name)) => {
1131 let cap_name = offer.r#as.as_ref().unwrap_or(name);
1132 cap_name.as_str() == required_offer.name()
1133 }
1134 None => false,
1135 };
1136 names_this_collection && names_this_capability
1137 }
1138
1139 fn validate_required_use_decls(&self) -> Result<(), Error> {
1140 let use_decls_stub = Vec::new();
1141 let use_decls = self.document.r#use.as_ref().unwrap_or(&use_decls_stub);
1142
1143 for required_usage in self.capability_requirements.must_use {
1144 if !use_decls.iter().any(|usage| match usage.protocol.as_ref() {
1145 None => false,
1146 Some(protocol) => protocol
1147 .iter()
1148 .any(|protocol_name| protocol_name.as_str() == required_usage.name()),
1149 }) {
1150 return Err(Error::validate(format!(
1151 r#"Protocol "{}" is not used by a component but is required by all"#,
1152 required_usage.name(),
1153 )));
1154 }
1155 }
1156
1157 Ok(())
1158 }
1159
1160 fn expand_source_dependencies(
1161 &self,
1162 names: Vec<&'a Name>,
1163 source: &AnyRef<'a>,
1164 ) -> Vec<DependencyNode<'a>> {
1165 let mut sources = vec![];
1166 match source {
1167 AnyRef::Self_ => {
1168 let mut has_named = false;
1169 for name in names {
1170 if self.all_dictionaries.contains_key(name)
1171 || self.all_storages.contains_key(name)
1172 {
1173 sources.push(DependencyNode::Named(name));
1174 has_named = true;
1175 }
1176 }
1177 if !has_named {
1178 sources.push(DependencyNode::Self_);
1179 }
1180 }
1181 AnyRef::Dictionary(d) => {
1182 let mut has_named = false;
1183 match d.root {
1184 RootDictionaryRef::Self_ => {
1185 let dictionary_name = d.path.iter_segments().next().unwrap();
1186 if self.all_dictionaries.contains_key(dictionary_name) {
1187 sources.push(DependencyNode::Named(dictionary_name));
1190 has_named = true;
1191 }
1192 }
1193 _ => {}
1194 }
1195 if !has_named {
1196 if let Some(s) = DependencyNode::from_dictionary_ref(d) {
1197 sources.push(s);
1198 }
1199 }
1200 }
1201 AnyRef::Named(n) => {
1202 sources.push(DependencyNode::Named(n));
1203 }
1204 AnyRef::Parent | AnyRef::Void | AnyRef::Framework | AnyRef::Debug => {}
1205 AnyRef::OwnDictionary(_) => unreachable!("can't be a source"),
1206 }
1207 sources
1208 }
1209
1210 fn add_strong_dep(&mut self, source: DependencyNode<'a>, target: DependencyNode<'a>) {
1215 match (source, target) {
1216 (DependencyNode::Self_, DependencyNode::Self_) => {
1217 }
1219 (source, target) => {
1220 self.strong_dependencies.add_edge(source, target);
1221 }
1222 }
1223 }
1224
1225 fn validate_from_clause<T>(
1234 &self,
1235 verb: &str,
1236 cap: &T,
1237 source_availability: &Option<SourceAvailability>,
1238 availability: &Option<Availability>,
1239 ) -> Result<(), Error>
1240 where
1241 T: CapabilityClause + FromClause,
1242 {
1243 let from = cap.from_();
1244 if cap.service().is_none() && from.is_many() {
1245 return Err(Error::validate(format!(
1246 "\"{}\" capabilities cannot have multiple \"from\" clauses",
1247 cap.capability_type().unwrap()
1248 )));
1249 }
1250
1251 if from.is_many() {
1252 ensure_no_duplicate_values(&cap.from_())?;
1253 }
1254
1255 let reference_description = format!("\"{}\" source", verb);
1256 for from_clause in from {
1257 let ref_validity_res = if cap.protocol().is_some() {
1260 self.validate_component_child_or_capability_ref(
1261 &reference_description,
1262 &from_clause,
1263 )
1264 } else if cap.service().is_some() {
1265 self.validate_component_child_or_collection_ref(
1267 &reference_description,
1268 &from_clause,
1269 )
1270 } else {
1271 self.validate_component_child_ref(&reference_description, &from_clause)
1272 };
1273
1274 match ref_validity_res {
1275 Ok(()) if from_clause == AnyRef::Void => {
1276 if availability != &Some(Availability::Optional) {
1278 return Err(Error::validate(format!(
1279 "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"{}\", from: \"{}\"",
1280 cap.names().iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", "),
1281 cap.from_(),
1282 )));
1283 }
1284 }
1285 Ok(()) => {
1286 }
1288 Err(_) if source_availability == &Some(SourceAvailability::Unknown) => {
1289 if availability != &Some(Availability::Optional) && availability != &None {
1291 return Err(Error::validate(format!(
1292 "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"{}\", from: \"{}\"",
1293 cap.names().iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", "),
1294 cap.from_(),
1295 )));
1296 }
1297 }
1298 Err(e) => {
1299 return Err(e);
1301 }
1302 }
1303 }
1304 Ok(())
1305 }
1306
1307 fn validate_component_child_ref(
1314 &self,
1315 reference_description: &str,
1316 component_ref: &AnyRef<'_>,
1317 ) -> Result<(), Error> {
1318 match component_ref {
1319 AnyRef::Named(name) => {
1320 if !self.all_children.contains_key(name) {
1322 return Err(Error::validate(format!(
1323 "{} \"{}\" does not appear in \"children\"",
1324 reference_description, component_ref
1325 )));
1326 }
1327 Ok(())
1328 }
1329 _ => Ok(()),
1331 }
1332 }
1333
1334 fn validate_component_child_or_collection_ref(
1341 &self,
1342 reference_description: &str,
1343 component_ref: &AnyRef<'_>,
1344 ) -> Result<(), Error> {
1345 match component_ref {
1346 AnyRef::Named(name) => {
1347 if !self.all_children.contains_key(name) && !self.all_collections.contains(name) {
1349 return Err(Error::validate(format!(
1350 "{} \"{}\" does not appear in \"children\" or \"collections\"",
1351 reference_description, component_ref
1352 )));
1353 }
1354 Ok(())
1355 }
1356 _ => Ok(()),
1358 }
1359 }
1360
1361 fn validate_component_capability_ref(
1368 &self,
1369 reference_description: &str,
1370 capability_ref: &AnyRef<'_>,
1371 ) -> Result<(), Error> {
1372 match capability_ref {
1373 AnyRef::Named(name) => {
1374 if !self.all_capability_names.contains(name) {
1375 return Err(Error::validate(format!(
1376 "{} \"{}\" does not appear in \"capabilities\"",
1377 reference_description, capability_ref
1378 )));
1379 }
1380 Ok(())
1381 }
1382 _ => Ok(()),
1383 }
1384 }
1385
1386 fn validate_component_child_or_capability_ref(
1393 &self,
1394 reference_description: &str,
1395 ref_: &AnyRef<'_>,
1396 ) -> Result<(), Error> {
1397 if self.validate_component_child_ref(reference_description, ref_).is_err()
1398 && self.validate_component_capability_ref(reference_description, ref_).is_err()
1399 {
1400 return Err(Error::validate(format!(
1401 "{} \"{}\" does not appear in \"children\" or \"capabilities\"",
1402 reference_description, ref_
1403 )));
1404 }
1405 Ok(())
1406 }
1407
1408 fn validate_directory_rights(&self, rights_clause: &Rights) -> Result<(), Error> {
1411 let mut rights = HashSet::new();
1412 for right_token in rights_clause.0.iter() {
1413 for right in right_token.expand() {
1414 if !rights.insert(right) {
1415 return Err(Error::validate(format!(
1416 "\"{}\" is duplicated in the rights clause.",
1417 right_token
1418 )));
1419 }
1420 }
1421 }
1422 Ok(())
1423 }
1424
1425 fn validate_runner_specified(&self, program: Option<&Program>) -> Result<(), Error> {
1428 match program {
1429 Some(program) => match program.runner {
1430 Some(_) => Ok(()),
1431 None => {
1432 return Err(Error::validate(
1433 "Component has a `program` block defined, but doesn't specify a `runner`. \
1434 Components need to use a runner to actually execute code.",
1435 ));
1436 }
1437 },
1438 None => Ok(()),
1439 }
1440 }
1441
1442 fn validate_runner_not_specified(&self, program: Option<&Program>) -> Result<(), Error> {
1445 match program {
1446 Some(program) => match program.runner {
1447 Some(_) => {
1448 return Err(Error::validate(
1451 "Component has conflicting runners in `program` block and `use` block.",
1452 ));
1453 }
1454 None => Ok(()),
1455 },
1456 None => Ok(()),
1457 }
1458 }
1459
1460 fn validate_config(
1461 &self,
1462 fields: &Option<BTreeMap<ConfigKey, ConfigValueType>>,
1463 ) -> Result<(), Error> {
1464 let optional_use_keys: BTreeMap<ConfigKey, ConfigValueType> = self
1467 .document
1468 .r#use
1469 .iter()
1470 .flatten()
1471 .map(|u| {
1472 if u.config == None {
1473 return None;
1474 }
1475 if u.availability == Some(Availability::Required) || u.availability == None {
1476 return None;
1477 }
1478 if let Some(_) = u.config_default.as_ref() {
1479 return None;
1480 }
1481 let key = ConfigKey(u.key.clone().expect("key should be set").into());
1482 let value = use_config_to_value_type(u).expect("config type should be valid");
1483 Some((key, value))
1484 })
1485 .flatten()
1486 .collect();
1487
1488 let Some(fields) = fields else {
1489 if !optional_use_keys.is_empty() {
1490 return Err(Error::validate(
1491 "Optionally using a config capability without a default requires a matching 'config' section.",
1492 ));
1493 }
1494 return Ok(());
1495 };
1496
1497 if fields.is_empty() {
1498 return Err(Error::validate("'config' section is empty"));
1499 }
1500
1501 for (key, value) in optional_use_keys {
1502 if !fields.contains_key(&key) {
1503 return Err(Error::validate(format!(
1504 "'config' section must contain key for optional use '{}'",
1505 key
1506 )));
1507 }
1508 if fields.get(&key) != Some(&value) {
1509 return Err(Error::validate(format!(
1510 "Use and config block differ on type for key '{}'",
1511 key
1512 )));
1513 }
1514 }
1515
1516 Ok(())
1517 }
1518
1519 fn validate_environment(&mut self, environment: &'a Environment) -> Result<(), Error> {
1520 match &environment.extends {
1521 Some(EnvironmentExtends::None) => {
1522 if environment.stop_timeout_ms.is_none() {
1523 return Err(Error::validate(
1524 "'__stop_timeout_ms' must be provided if the environment does not extend \
1525 another environment",
1526 ));
1527 }
1528 }
1529 Some(EnvironmentExtends::Realm) | None => {}
1530 }
1531
1532 if let Some(runners) = &environment.runners {
1533 let mut used_names = HashMap::new();
1534 for registration in runners {
1535 let name = registration.r#as.as_ref().unwrap_or(®istration.runner);
1537 if let Some(previous_runner) = used_names.insert(name, ®istration.runner) {
1538 return Err(Error::validate(format!(
1539 "Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".",
1540 name, ®istration.runner, previous_runner
1541 )));
1542 }
1543
1544 if registration.from == RegistrationRef::Self_
1546 && !self.all_runners.contains(®istration.runner)
1547 {
1548 return Err(Error::validate(format!(
1549 "Runner \"{}\" registered in environment is not in \"runners\"",
1550 ®istration.runner,
1551 )));
1552 }
1553
1554 self.validate_component_child_ref(
1555 &format!("\"{}\" runner source", ®istration.runner),
1556 &AnyRef::from(®istration.from),
1557 )?;
1558
1559 if let Some(source) = DependencyNode::registration_ref(®istration.from) {
1562 let target = DependencyNode::Named(&environment.name);
1563 self.add_strong_dep(source, target);
1564 }
1565 }
1566 }
1567
1568 if let Some(resolvers) = &environment.resolvers {
1569 let mut used_schemes = HashMap::new();
1570 for registration in resolvers {
1571 if let Some(previous_resolver) =
1573 used_schemes.insert(®istration.scheme, ®istration.resolver)
1574 {
1575 return Err(Error::validate(format!(
1576 "scheme \"{}\" for resolver \"{}\" is already registered; \
1577 previously registered to resolver \"{}\".",
1578 ®istration.scheme, ®istration.resolver, previous_resolver
1579 )));
1580 }
1581
1582 self.validate_component_child_ref(
1583 &format!("\"{}\" resolver source", ®istration.resolver),
1584 &AnyRef::from(®istration.from),
1585 )?;
1586 if let Some(source) = DependencyNode::registration_ref(®istration.from) {
1589 let target = DependencyNode::Named(&environment.name);
1590 self.add_strong_dep(source, target);
1591 }
1592 }
1593 }
1594
1595 if let Some(debug_capabilities) = &environment.debug {
1596 for debug in debug_capabilities {
1597 self.protocol_from_self_checker(debug).validate("registered as debug")?;
1598 self.validate_from_clause("debug", debug, &None, &None)?;
1599 for source in self.expand_source_dependencies(debug.names(), &(&debug.from).into())
1602 {
1603 let target = DependencyNode::Named(&environment.name);
1604 self.add_strong_dep(source, target);
1605 }
1606 }
1607 }
1608 Ok(())
1609 }
1610
1611 fn service_from_self_checker<'b>(
1612 &'b self,
1613 input: &'b (impl CapabilityClause + FromClause),
1614 ) -> RouteFromSelfChecker<'b> {
1615 RouteFromSelfChecker {
1616 capability_name: input.service(),
1617 from: input.from_(),
1618 container: &self.all_services,
1619 all_dictionaries: &self.all_dictionaries,
1620 typename: "service",
1621 }
1622 }
1623
1624 fn protocol_from_self_checker<'b>(
1625 &'b self,
1626 input: &'b (impl CapabilityClause + FromClause),
1627 ) -> RouteFromSelfChecker<'b> {
1628 RouteFromSelfChecker {
1629 capability_name: input.protocol(),
1630 from: input.from_(),
1631 container: &self.all_protocols,
1632 all_dictionaries: &self.all_dictionaries,
1633 typename: "protocol",
1634 }
1635 }
1636
1637 fn directory_from_self_checker<'b>(
1638 &'b self,
1639 input: &'b (impl CapabilityClause + FromClause),
1640 ) -> RouteFromSelfChecker<'b> {
1641 RouteFromSelfChecker {
1642 capability_name: input.directory(),
1643 from: input.from_(),
1644 container: &self.all_directories,
1645 all_dictionaries: &self.all_dictionaries,
1646 typename: "directory",
1647 }
1648 }
1649
1650 fn storage_from_self_checker<'b>(
1651 &'b self,
1652 input: &'b (impl CapabilityClause + FromClause),
1653 ) -> RouteFromSelfChecker<'b> {
1654 RouteFromSelfChecker {
1655 capability_name: input.storage(),
1656 from: input.from_(),
1657 container: &self.all_storages,
1658 all_dictionaries: &self.all_dictionaries,
1659 typename: "storage",
1660 }
1661 }
1662
1663 fn runner_from_self_checker<'b>(
1664 &'b self,
1665 input: &'b (impl CapabilityClause + FromClause),
1666 ) -> RouteFromSelfChecker<'b> {
1667 RouteFromSelfChecker {
1668 capability_name: input.runner(),
1669 from: input.from_(),
1670 container: &self.all_runners,
1671 all_dictionaries: &self.all_dictionaries,
1672 typename: "runner",
1673 }
1674 }
1675
1676 fn resolver_from_self_checker<'b>(
1677 &'b self,
1678 input: &'b (impl CapabilityClause + FromClause),
1679 ) -> RouteFromSelfChecker<'b> {
1680 RouteFromSelfChecker {
1681 capability_name: input.resolver(),
1682 from: input.from_(),
1683 container: &self.all_resolvers,
1684 all_dictionaries: &self.all_dictionaries,
1685 typename: "resolver",
1686 }
1687 }
1688
1689 fn dictionary_from_self_checker<'b>(
1690 &'b self,
1691 input: &'b (impl CapabilityClause + FromClause),
1692 ) -> RouteFromSelfChecker<'b> {
1693 RouteFromSelfChecker {
1694 capability_name: input.dictionary(),
1695 from: input.from_(),
1696 container: &self.all_dictionaries,
1697 all_dictionaries: &self.all_dictionaries,
1698 typename: "dictionary",
1699 }
1700 }
1701
1702 fn config_from_self_checker<'b>(
1703 &'b self,
1704 input: &'b (impl CapabilityClause + FromClause),
1705 ) -> RouteFromSelfChecker<'b> {
1706 RouteFromSelfChecker {
1707 capability_name: input.config(),
1708 from: input.from_(),
1709 container: &self.all_configs,
1710 all_dictionaries: &self.all_dictionaries,
1711 typename: "config",
1712 }
1713 }
1714}
1715
1716struct RouteFromSelfChecker<'a> {
1718 capability_name: Option<OneOrMany<&'a Name>>,
1720
1721 from: OneOrMany<AnyRef<'a>>,
1723
1724 container: &'a dyn Container,
1726
1727 all_dictionaries: &'a HashMap<&'a Name, &'a Capability>,
1729
1730 typename: &'static str,
1732}
1733
1734impl<'a> RouteFromSelfChecker<'a> {
1735 fn validate(self, operand: &'static str) -> Result<(), Error> {
1736 let Self { capability_name, from, container, all_dictionaries, typename } = self;
1737 let Some(capability) = capability_name else {
1738 return Ok(());
1739 };
1740 for capability in capability {
1741 for from in &from {
1742 match from {
1743 AnyRef::Self_ if !container.contains(capability) => {
1744 return Err(Error::validate(format!(
1745 "{typename} \"{capability}\" is {operand} from self, so it \
1746 must be declared as a \"{typename}\" in \"capabilities\"",
1747 )));
1748 }
1749 AnyRef::Dictionary(DictionaryRef { root: RootDictionaryRef::Self_, path }) => {
1750 let first_segment = path.iter_segments().next().unwrap();
1751 if !all_dictionaries.contains_key(first_segment) {
1752 return Err(Error::validate(format!(
1753 "{typename} \"{capability}\" is {operand} from \"self/{path}\", so \
1754 \"{first_segment}\" must be declared as a \"dictionary\" in \"capabilities\"",
1755 )));
1756 }
1757 }
1758 _ => {}
1759 }
1760 }
1761 }
1762 Ok(())
1763 }
1764}
1765
1766trait Container {
1770 fn contains(&self, key: &Name) -> bool;
1771}
1772
1773impl<'a> Container for HashSet<&'a Name> {
1774 fn contains(&self, key: &Name) -> bool {
1775 self.contains(key)
1776 }
1777}
1778
1779impl<'a, T> Container for HashMap<&'a Name, T> {
1780 fn contains(&self, key: &Name) -> bool {
1781 self.contains_key(key)
1782 }
1783}
1784
1785pub fn use_config_to_value_type(u: &Use) -> Result<ConfigValueType, Error> {
1788 let config = u.config.clone().expect("Only call use_config_to_value_type on a Config");
1789
1790 let Some(config_type) = u.config_type.as_ref() else {
1791 return Err(Error::validate(format!("Config '{}' is missing field 'type'", config)));
1792 };
1793
1794 let config_type = match config_type {
1795 ConfigType::Bool => ConfigValueType::Bool { mutability: None },
1796 ConfigType::Uint8 => ConfigValueType::Uint8 { mutability: None },
1797 ConfigType::Uint16 => ConfigValueType::Uint16 { mutability: None },
1798 ConfigType::Uint32 => ConfigValueType::Uint32 { mutability: None },
1799 ConfigType::Uint64 => ConfigValueType::Uint64 { mutability: None },
1800 ConfigType::Int8 => ConfigValueType::Int8 { mutability: None },
1801 ConfigType::Int16 => ConfigValueType::Int16 { mutability: None },
1802 ConfigType::Int32 => ConfigValueType::Int32 { mutability: None },
1803 ConfigType::Int64 => ConfigValueType::Int64 { mutability: None },
1804 ConfigType::String => {
1805 let Some(max_size) = u.config_max_size else {
1806 return Err(Error::validate(format!(
1807 "Config '{}' is type String but is missing field 'max_size'",
1808 config
1809 )));
1810 };
1811 ConfigValueType::String { max_size: max_size.into(), mutability: None }
1812 }
1813 ConfigType::Vector => {
1814 let Some(ref element) = u.config_element_type else {
1815 return Err(Error::validate(format!(
1816 "Config '{}' is type Vector but is missing field 'element'",
1817 config
1818 )));
1819 };
1820 let Some(max_count) = u.config_max_count else {
1821 return Err(Error::validate(format!(
1822 "Config '{}' is type Vector but is missing field 'max_count'",
1823 config
1824 )));
1825 };
1826 ConfigValueType::Vector {
1827 max_count: max_count.into(),
1828 element: element.clone(),
1829 mutability: None,
1830 }
1831 }
1832 };
1833 Ok(config_type)
1834}
1835
1836fn ensure_no_duplicate_names<'a, I>(values: I) -> Result<(), Error>
1839where
1840 I: Iterator<Item = (&'a Name, &'a str)>,
1841{
1842 let mut seen_keys = HashMap::new();
1843 for (key, name) in values {
1844 if let Some(preexisting_name) = seen_keys.insert(key, name) {
1845 return Err(Error::validate(format!(
1846 "identifier \"{}\" is defined twice, once in \"{}\" and once in \"{}\"",
1847 key, name, preexisting_name
1848 )));
1849 }
1850 }
1851 Ok(())
1852}
1853
1854fn ensure_no_duplicate_values<'a, I, V>(values: I) -> Result<(), Error>
1856where
1857 I: IntoIterator<Item = &'a V>,
1858 V: 'a + Hash + Eq + fmt::Display,
1859{
1860 let mut seen = HashSet::new();
1861 for value in values {
1862 if !seen.insert(value) {
1863 return Err(Error::validate(format!("Found duplicate value \"{}\" in array.", value)));
1864 }
1865 }
1866 Ok(())
1867}
1868
1869#[derive(Copy, Clone, Hash, Ord, Debug, PartialOrd, PartialEq, Eq)]
1871enum DependencyNode<'a> {
1872 Named(&'a Name),
1873 Self_,
1874}
1875
1876impl<'a> DependencyNode<'a> {
1877 fn from_dictionary_ref(d: &'a DictionaryRef) -> Option<DependencyNode<'a>> {
1878 match &d.root {
1879 RootDictionaryRef::Named(name) => Some(DependencyNode::Named(name)),
1880 RootDictionaryRef::Self_ => Some(DependencyNode::Self_),
1881 RootDictionaryRef::Parent => None,
1882 }
1883 }
1884
1885 fn offer_to_ref(ref_: &'a OfferToRef) -> DependencyNode<'a> {
1886 match ref_ {
1887 OfferToRef::Named(name) => DependencyNode::Named(name),
1888 OfferToRef::All => panic!(r#"offer to "all" may not be in Dependency Graph"#),
1889 OfferToRef::OwnDictionary(name) => DependencyNode::Named(name),
1890 }
1891 }
1892
1893 fn registration_ref(ref_: &'a RegistrationRef) -> Option<DependencyNode<'a>> {
1894 match ref_ {
1895 RegistrationRef::Named(name) => Some(DependencyNode::Named(name)),
1896 RegistrationRef::Self_ => Some(DependencyNode::Self_),
1897
1898 RegistrationRef::Parent => None,
1901 }
1902 }
1903}
1904
1905impl<'a> fmt::Display for DependencyNode<'a> {
1906 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1907 match self {
1908 DependencyNode::Self_ => write!(f, "self"),
1909 DependencyNode::Named(name) => write!(f, "#{}", name),
1910 }
1911 }
1912}
1913
1914#[cfg(test)]
1915mod tests {
1916 use super::*;
1917 use crate::error::Location;
1918 use crate::{
1919 offer_to_all_and_component_diff_capabilities_message,
1920 offer_to_all_and_component_diff_sources_message,
1921 };
1922 use assert_matches::assert_matches;
1923 use serde_json::json;
1924
1925 macro_rules! test_validate_cml {
1926 (
1927 $(
1928 $test_name:ident($input:expr, $($pattern:tt)+),
1929 )+
1930 ) => {
1931 $(
1932 #[test]
1933 fn $test_name() {
1934 let input = format!("{}", $input);
1935 let result = validate_for_test("test.cml", &input.as_bytes());
1936 assert_matches!(result, $($pattern)+);
1937 }
1938 )+
1939 }
1940 }
1941
1942 macro_rules! test_validate_cml_with_feature {
1943 (
1944 $features:expr,
1945 {
1946 $(
1947 $test_name:ident($input:expr, $($pattern:tt)+),
1948 )+
1949 }
1950 ) => {
1951 $(
1952 #[test]
1953 fn $test_name() {
1954 let input = format!("{}", $input);
1955 let features = $features;
1956 let result = validate_with_features_for_test("test.cml", &input.as_bytes(), &features, &vec![], &vec![], &vec![]);
1957 assert_matches!(result, $($pattern)+);
1958 }
1959 )+
1960 }
1961 }
1962
1963 fn validate_for_test(filename: &str, input: &[u8]) -> Result<(), Error> {
1964 validate_with_features_for_test(filename, input, &FeatureSet::empty(), &[], &[], &[])
1965 }
1966
1967 fn validate_with_features_for_test(
1968 filename: &str,
1969 input: &[u8],
1970 features: &FeatureSet,
1971 required_offers: &[String],
1972 required_uses: &[String],
1973 required_dictionary_offers: &[String],
1974 ) -> Result<(), Error> {
1975 let input = format!("{}", std::str::from_utf8(input).unwrap().to_string());
1976 let file = Path::new(filename);
1977 let document = crate::parse_one_document(&input, &file)?;
1978 validate_cml(
1979 &document,
1980 Some(&file),
1981 &features,
1982 &CapabilityRequirements {
1983 must_offer: &required_offers
1984 .iter()
1985 .map(|value| OfferToAllCapability::Protocol(value))
1986 .chain(
1987 required_dictionary_offers
1988 .iter()
1989 .map(|value| OfferToAllCapability::Dictionary(value)),
1990 )
1991 .collect::<Vec<_>>(),
1992 must_use: &required_uses
1993 .iter()
1994 .map(|value| MustUseRequirement::Protocol(value))
1995 .collect::<Vec<_>>(),
1996 },
1997 )
1998 }
1999
2000 fn unused_component_err_message(missing: &str) -> String {
2001 format!(r#"Protocol "{}" is not used by a component but is required by all"#, missing)
2002 }
2003
2004 #[test]
2005 fn must_use_protocol() {
2006 let input = r##"{
2007 children: [
2008 {
2009 name: "logger",
2010 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2011 },
2012 {
2013 name: "something",
2014 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2015 },
2016 ],
2017 }"##;
2018
2019 let result = validate_with_features_for_test(
2020 "test.cml",
2021 input.as_bytes(),
2022 &FeatureSet::empty(),
2023 &[],
2024 &vec!["fuchsia.logger.LogSink".into()],
2025 &[],
2026 );
2027
2028 assert_matches!(result,
2029 Err(Error::Validate { err, filename }) => {
2030 assert_eq!(err, unused_component_err_message("fuchsia.logger.LogSink"));
2031 assert!(filename.is_some(), "Expected there to be a filename in error message");
2032 }
2033 );
2034
2035 let input = r##"{
2036 children: [
2037 {
2038 name: "logger",
2039 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2040 },
2041 ],
2042
2043 use: [
2044 {
2045 protocol: [ "fuchsia.component.Binder" ],
2046 from: "framework",
2047 }
2048 ],
2049 }"##;
2050
2051 let result = validate_with_features_for_test(
2052 "test.cml",
2053 input.as_bytes(),
2054 &FeatureSet::empty(),
2055 &[],
2056 &vec!["fuchsia.component.Binder".into()],
2057 &[],
2058 );
2059 assert_matches!(result, Ok(_));
2060 }
2061
2062 #[test]
2063 fn required_offer_to_all() {
2064 let input = r##"{
2065 children: [
2066 {
2067 name: "logger",
2068 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2069 },
2070 {
2071 name: "something",
2072 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2073 },
2074 ],
2075 collections: [
2076 {
2077 name: "coll",
2078 durability: "transient",
2079 },
2080 ],
2081 offer: [
2082 {
2083 protocol: "fuchsia.logger.LogSink",
2084 from: "parent",
2085 to: "all"
2086 },
2087 {
2088 protocol: "fuchsia.inspect.InspectSink",
2089 from: "parent",
2090 to: "all"
2091 },
2092 {
2093 protocol: "fuchsia.process.Launcher",
2094 from: "parent",
2095 to: "#something",
2096 },
2097 ]
2098 }"##;
2099 let result = validate_with_features_for_test(
2100 "test.cml",
2101 input.as_bytes(),
2102 &FeatureSet::empty(),
2103 &vec!["fuchsia.logger.LogSink".into(), "fuchsia.inspect.InspectSink".into()],
2104 &Vec::new(),
2105 &[],
2106 );
2107 assert_matches!(result, Ok(_));
2108 }
2109
2110 #[test]
2111 fn required_offer_to_all_manually() {
2112 let input = r##"{
2113 children: [
2114 {
2115 name: "logger",
2116 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2117 },
2118 {
2119 name: "something",
2120 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2121 },
2122 ],
2123 collections: [
2124 {
2125 name: "coll",
2126 durability: "transient",
2127 },
2128 ],
2129 offer: [
2130 {
2131 protocol: "fuchsia.logger.LogSink",
2132 from: "#something",
2133 to: "#logger"
2134 },
2135 {
2136 protocol: "fuchsia.logger.LogSink",
2137 from: "parent",
2138 to: "#something"
2139 },
2140 {
2141 protocol: "fuchsia.logger.LogSink",
2142 from: "parent",
2143 to: "#coll",
2144 },
2145 ]
2146 }"##;
2147 let result = validate_with_features_for_test(
2148 "test.cml",
2149 input.as_bytes(),
2150 &FeatureSet::empty(),
2151 &vec!["fuchsia.logger.LogSink".into()],
2152 &[],
2153 &[],
2154 );
2155 assert_matches!(result, Ok(_));
2156
2157 let input = r##"{
2158 children: [
2159 {
2160 name: "logger",
2161 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2162 },
2163 {
2164 name: "something",
2165 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2166 },
2167 {
2168 name: "something_v2",
2169 url: "fuchsia-pkg://fuchsia.com/something_v2#meta/something_v2.cm",
2170 },
2171 ],
2172 collections: [
2173 {
2174 name: "coll",
2175 durability: "transient",
2176 },
2177 ],
2178 offer: [
2179 {
2180 protocol: "fuchsia.logger.LogSink",
2181 from: "parent",
2182 to: ["#logger", "#something", "#something_v2", "#coll"],
2183 },
2184 ]
2185 }"##;
2186 let result = validate_with_features_for_test(
2187 "test.cml",
2188 input.as_bytes(),
2189 &FeatureSet::empty(),
2190 &vec!["fuchsia.logger.LogSink".into()],
2191 &[],
2192 &[],
2193 );
2194 assert_matches!(result, Ok(_));
2195 }
2196
2197 #[test]
2198 fn offer_to_all_mixed_with_array_syntax() {
2199 let input = r##"{
2200 "children": [
2201 {
2202 "name": "something",
2203 "url": "fuchsia-pkg://fuchsia.com/something/stable#meta/something.cm",
2204 },
2205 ],
2206 "offer": [
2207 {
2208 "protocol": ["fuchsia.logger.LogSink", "fuchsia.inspect.InspectSink",],
2209 "from": "parent",
2210 "to": "#something",
2211 },
2212 {
2213 "protocol": "fuchsia.logger.LogSink",
2214 "from": "parent",
2215 "to": "all",
2216 },
2217 ],
2218 }"##;
2219
2220 let result = validate_with_features_for_test(
2221 "test.cml",
2222 input.as_bytes(),
2223 &FeatureSet::empty(),
2224 &vec!["fuchsia.logger.LogSink".into()],
2225 &Vec::new(),
2226 &[],
2227 );
2228
2229 assert_matches!(result, Ok(_));
2230
2231 let input = r##"{
2232
2233 "children": [
2234 {
2235 "name": "something",
2236 "url": "fuchsia-pkg://fuchsia.com/something/stable#meta/something.cm",
2237 },
2238 ],
2239 "offer": [
2240 {
2241 "protocol": ["fuchsia.logger.LogSink", "fuchsia.inspect.InspectSink",],
2242 "from": "parent",
2243 "to": "all",
2244 },
2245 {
2246 "protocol": "fuchsia.logger.LogSink",
2247 "from": "parent",
2248 "to": "#something",
2249 },
2250 ],
2251 }"##;
2252
2253 let result = validate_with_features_for_test(
2254 "test.cml",
2255 input.as_bytes(),
2256 &FeatureSet::empty(),
2257 &vec!["fuchsia.logger.LogSink".into()],
2258 &Vec::new(),
2259 &[],
2260 );
2261
2262 assert_matches!(result, Ok(_));
2263 }
2264
2265 #[test]
2266 fn offer_to_all_and_manual() {
2267 let input = r##"{
2268 children: [
2269 {
2270 name: "logger",
2271 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2272 },
2273 {
2274 name: "something",
2275 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2276 },
2277 ],
2278 offer: [
2279 {
2280 protocol: "fuchsia.logger.LogSink",
2281 from: "parent",
2282 to: "all"
2283 },
2284 {
2285 protocol: "fuchsia.logger.LogSink",
2286 from: "parent",
2287 to: "#something"
2288 },
2289 ]
2290 }"##;
2291
2292 let result = validate_with_features_for_test(
2293 "test.cml",
2294 input.as_bytes(),
2295 &FeatureSet::empty(),
2296 &vec!["fuchsia.logger.LogSink".into()],
2297 &Vec::new(),
2298 &[],
2299 );
2300
2301 assert_matches!(result, Ok(_));
2303
2304 let input = r##"{
2305 children: [
2306 {
2307 name: "logger",
2308 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2309 },
2310 {
2311 name: "something",
2312 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2313 },
2314 ],
2315 offer: [
2316 {
2317 protocol: "fuchsia.logger.LogSink",
2318 from: "parent",
2319 to: "all"
2320 },
2321 {
2322 protocol: "fuchsia.logger.FakLog",
2323 from: "parent",
2324 as: "fuchsia.logger.LogSink",
2325 to: "#something"
2326 },
2327 ]
2328 }"##;
2329
2330 let result = validate_with_features_for_test(
2331 "test.cml",
2332 input.as_bytes(),
2333 &FeatureSet::empty(),
2334 &vec!["fuchsia.logger.LogSink".into()],
2335 &Vec::new(),
2336 &[],
2337 );
2338
2339 assert_matches!(result,
2341 Err(Error::Validate { err, filename }) => {
2342 assert_eq!(
2343 err,
2344 offer_to_all_and_component_diff_capabilities_message([OfferToAllCapability::Protocol("fuchsia.logger.LogSink")].into_iter(), "something"),
2345 );
2346 assert!(filename.is_some(), "Expected there to be a filename in error message");
2347 }
2348 );
2349
2350 let input = r##"{
2351 children: [
2352 {
2353 name: "logger",
2354 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2355 },
2356 {
2357 name: "something",
2358 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2359 },
2360 ],
2361 offer: [
2362 {
2363 protocol: "fuchsia.logger.LogSink",
2364 from: "parent",
2365 to: "all"
2366 },
2367 {
2368 protocol: "fuchsia.logger.LogSink",
2369 from: "framework",
2370 to: "#something"
2371 },
2372 ]
2373 }"##;
2374
2375 let result = validate_with_features_for_test(
2376 "test.cml",
2377 input.as_bytes(),
2378 &FeatureSet::empty(),
2379 &vec!["fuchsia.logger.LogSink".into()],
2380 &Vec::new(),
2381 &[],
2382 );
2383
2384 assert_matches!(result,
2386 Err(Error::Validate { err, filename }) => {
2387 assert_eq!(
2388 err,
2389 offer_to_all_and_component_diff_sources_message([OfferToAllCapability::Protocol("fuchsia.logger.LogSink")].into_iter(), "something"),
2390 );
2391 assert!(filename.is_some(), "Expected there to be a filename in error message");
2392 }
2393 );
2394 }
2395
2396 #[test]
2397 fn offer_to_all_and_manual_for_dictionary() {
2398 let input = r##"{
2399 children: [
2400 {
2401 name: "logger",
2402 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2403 },
2404 {
2405 name: "something",
2406 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2407 },
2408 ],
2409 offer: [
2410 {
2411 dictionary: "diagnostics",
2412 from: "parent",
2413 to: "all"
2414 },
2415 {
2416 dictionary: "diagnostics",
2417 from: "parent",
2418 to: "#something"
2419 },
2420 ]
2421 }"##;
2422
2423 let result = validate_with_features_for_test(
2424 "test.cml",
2425 input.as_bytes(),
2426 &FeatureSet::empty(),
2427 &vec![],
2428 &Vec::new(),
2429 &["diagnostics".into()],
2430 );
2431
2432 assert_matches!(result, Ok(_));
2434
2435 let input = r##"{
2436 children: [
2437 {
2438 name: "logger",
2439 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2440 },
2441 {
2442 name: "something",
2443 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2444 },
2445 ],
2446 offer: [
2447 {
2448 dictionary: "diagnostics",
2449 from: "parent",
2450 to: "all"
2451 },
2452 {
2453 dictionary: "FakDictionary",
2454 from: "parent",
2455 as: "diagnostics",
2456 to: "#something"
2457 },
2458 ]
2459 }"##;
2460
2461 let result = validate_with_features_for_test(
2462 "test.cml",
2463 input.as_bytes(),
2464 &FeatureSet::empty(),
2465 &vec![],
2466 &Vec::new(),
2467 &["diagnostics".into()],
2468 );
2469
2470 assert_matches!(result,
2472 Err(Error::Validate { err, filename }) => {
2473 assert_eq!(
2474 err,
2475 offer_to_all_and_component_diff_capabilities_message([OfferToAllCapability::Dictionary("diagnostics")].into_iter(), "something"),
2476 );
2477 assert!(filename.is_some(), "Expected there to be a filename in error message");
2478 }
2479 );
2480
2481 let input = r##"{
2482 children: [
2483 {
2484 name: "logger",
2485 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2486 },
2487 {
2488 name: "something",
2489 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2490 },
2491 ],
2492 offer: [
2493 {
2494 dictionary: "diagnostics",
2495 from: "parent",
2496 to: "all"
2497 },
2498 {
2499 dictionary: "diagnostics",
2500 from: "framework",
2501 to: "#something"
2502 },
2503 ]
2504 }"##;
2505
2506 let result = validate_with_features_for_test(
2507 "test.cml",
2508 input.as_bytes(),
2509 &FeatureSet::empty(),
2510 &vec![],
2511 &Vec::new(),
2512 &["diagnostics".into()],
2513 );
2514
2515 assert_matches!(result,
2517 Err(Error::Validate { err, filename }) => {
2518 assert_eq!(
2519 err,
2520 offer_to_all_and_component_diff_sources_message([OfferToAllCapability::Dictionary("diagnostics")].into_iter(), "something"),
2521 );
2522 assert!(filename.is_some(), "Expected there to be a filename in error message");
2523 }
2524 );
2525 }
2526
2527 fn offer_to_all_diff_sources_message(protocols: &[&str]) -> String {
2528 format!(r#"Protocol(s) {:?} offered to "all" multiple times"#, protocols)
2529 }
2530
2531 #[test]
2532 fn offer_to_all_from_diff_sources() {
2533 let input = r##"{
2534 children: [
2535 {
2536 name: "logger",
2537 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2538 },
2539 {
2540 name: "something",
2541 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2542 },
2543 ],
2544 offer: [
2545 {
2546 protocol: "fuchsia.logger.LogSink",
2547 from: "parent",
2548 to: "all"
2549 },
2550 {
2551 protocol: "fuchsia.logger.LogSink",
2552 from: "framework",
2553 to: "all"
2554 },
2555 ]
2556 }"##;
2557
2558 let result = validate_with_features_for_test(
2559 "test.cml",
2560 input.as_bytes(),
2561 &FeatureSet::empty(),
2562 &vec!["fuchsia.logger.LogSink".into()],
2563 &Vec::new(),
2564 &[],
2565 );
2566
2567 assert_matches!(result,
2568 Err(Error::Validate { err, filename }) => {
2569 assert_eq!(
2570 err,
2571 offer_to_all_diff_sources_message(&["fuchsia.logger.LogSink"]),
2572 );
2573 assert!(filename.is_some(), "Expected there to be a filename in error message");
2574 }
2575 );
2576 }
2577
2578 #[test]
2579 fn offer_to_all_with_aliases() {
2580 let input = r##"{
2581 children: [
2582 {
2583 name: "logger",
2584 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2585 },
2586 {
2587 name: "something",
2588 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2589 },
2590 ],
2591 offer: [
2592 {
2593 protocol: "fuchsia.logger.LogSink",
2594 from: "parent",
2595 to: "all"
2596 },
2597 {
2598 protocol: "fuchsia.logger.LogSink",
2599 from: "framework",
2600 to: "all",
2601 as: "OtherLogSink",
2602 },
2603 {
2604 protocol: "fuchsia.logger.LogSink",
2605 from: "framework",
2606 to: "#something",
2607 as: "OtherOtherLogSink",
2608 },
2609 {
2610 protocol: "fuchsia.logger.LogSink",
2611 from: "parent",
2612 to: "#something",
2613 as: "fuchsia.logger.LogSink",
2614 },
2615 ]
2616 }"##;
2617
2618 let result = validate_with_features_for_test(
2619 "test.cml",
2620 input.as_bytes(),
2621 &FeatureSet::empty(),
2622 &["fuchsia.logger.LogSink".into()],
2623 &[],
2624 &[],
2625 );
2626
2627 assert_matches!(result, Ok(_));
2628 }
2629
2630 #[test]
2631 fn required_dict_offers_accept_aliases() {
2632 let input = r##"{
2633 capabilities: [
2634 {
2635 dictionary: "test-diagnostics",
2636 }
2637 ],
2638 children: [
2639 {
2640 name: "something",
2641 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2642 },
2643 ],
2644 offer: [
2645 {
2646 dictionary: "test-diagnostics",
2647 from: "self",
2648 to: "#something",
2649 as: "diagnostics",
2650 }
2651 ]
2652 }"##;
2653
2654 let result = validate_with_features_for_test(
2655 "test.cml",
2656 input.as_bytes(),
2657 &FeatureSet::empty(),
2658 &[],
2659 &[],
2660 &["diagnostics".into()],
2661 );
2662
2663 assert_matches!(result, Ok(_));
2664 }
2665
2666 fn fail_to_make_required_offer(
2667 protocol: &str,
2668 child_or_collection: &str,
2669 component: &str,
2670 ) -> String {
2671 format!(
2672 r#"Protocol "{}" is not offered to {} "{}" but it is a required offer"#,
2673 protocol, child_or_collection, component
2674 )
2675 }
2676
2677 fn fail_to_make_required_offer_dictionary(
2678 dictionary: &str,
2679 child_or_collection: &str,
2680 component: &str,
2681 ) -> String {
2682 format!(
2683 r#"Dictionary "{}" is not offered to {} "{}" but it is a required offer"#,
2684 dictionary, child_or_collection, component
2685 )
2686 }
2687
2688 #[test]
2689 fn fail_to_offer_to_all_when_required() {
2690 let input = r##"{
2691 children: [
2692 {
2693 name: "logger",
2694 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2695 },
2696 {
2697 name: "something",
2698 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2699 },
2700 ],
2701 offer: [
2702 {
2703 protocol: "fuchsia.logger.LogSink",
2704 from: "parent",
2705 to: "#logger"
2706 },
2707 {
2708 protocol: "fuchsia.logger.LegacyLog",
2709 from: "parent",
2710 to: "#something"
2711 },
2712 ]
2713 }"##;
2714 let result = validate_with_features_for_test(
2715 "test.cml",
2716 input.as_bytes(),
2717 &FeatureSet::empty(),
2718 &vec!["fuchsia.logger.LogSink".into()],
2719 &[],
2720 &[],
2721 );
2722
2723 assert_matches!(result,
2724 Err(Error::Validate { err, filename }) => {
2725 assert_eq!(
2726 err,
2727 fail_to_make_required_offer(
2728 "fuchsia.logger.LogSink",
2729 "child component",
2730 "something",
2731 ),
2732 );
2733 assert!(filename.is_some(), "Expected there to be a filename in error message");
2734 }
2735 );
2736
2737 let input = r##"{
2738 children: [
2739 {
2740 name: "logger",
2741 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2742 },
2743 ],
2744 collections: [
2745 {
2746 name: "coll",
2747 durability: "transient",
2748 },
2749 ],
2750 offer: [
2751 {
2752 protocol: "fuchsia.logger.LogSink",
2753 from: "parent",
2754 to: "#logger"
2755 },
2756 ]
2757 }"##;
2758 let result = validate_with_features_for_test(
2759 "test.cml",
2760 input.as_bytes(),
2761 &FeatureSet::empty(),
2762 &vec!["fuchsia.logger.LogSink".into()],
2763 &[],
2764 &[],
2765 );
2766
2767 assert_matches!(result,
2768 Err(Error::Validate { err, filename }) => {
2769 assert_eq!(
2770 err,
2771 fail_to_make_required_offer("fuchsia.logger.LogSink", "collection", "coll"),
2772 );
2773 assert!(filename.is_some(), "Expected there to be a filename in error message");
2774 }
2775 );
2776 }
2777
2778 #[test]
2779 fn fail_to_offer_dictionary_to_all_when_required() {
2780 let input = r##"{
2781 children: [
2782 {
2783 name: "logger",
2784 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2785 },
2786 {
2787 name: "something",
2788 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2789 },
2790 ],
2791 offer: [
2792 {
2793 protocol: "fuchsia.logger.LogSink",
2794 from: "parent",
2795 to: "all"
2796 },
2797 {
2798 dictionary: "diagnostics",
2799 from: "parent",
2800 to: "#logger"
2801 },
2802 {
2803 protocol: "fuchsia.logger.LegacyLog",
2804 from: "parent",
2805 to: "#something"
2806 },
2807 ]
2808 }"##;
2809 let result = validate_with_features_for_test(
2810 "test.cml",
2811 input.as_bytes(),
2812 &FeatureSet::empty(),
2813 &vec![],
2814 &[],
2815 &["diagnostics".to_string()],
2816 );
2817
2818 assert_matches!(result,
2819 Err(Error::Validate { err, filename }) => {
2820 assert_eq!(
2821 err,
2822 fail_to_make_required_offer_dictionary(
2823 "diagnostics",
2824 "child component",
2825 "something",
2826 ),
2827 );
2828 assert!(filename.is_some(), "Expected there to be a filename in error message");
2829 }
2830 );
2831
2832 let input = r##"{
2833 children: [
2834 {
2835 name: "logger",
2836 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2837 },
2838 ],
2839 collections: [
2840 {
2841 name: "coll",
2842 durability: "transient",
2843 },
2844 ],
2845 offer: [
2846 {
2847 protocol: "fuchsia.logger.LogSink",
2848 from: "parent",
2849 to: "all"
2850 },
2851 {
2852 protocol: "diagnostics",
2853 from: "parent",
2854 to: "all"
2855 },
2856 {
2857 dictionary: "diagnostics",
2858 from: "parent",
2859 to: "#logger"
2860 },
2861 ]
2862 }"##;
2863 let result = validate_with_features_for_test(
2864 "test.cml",
2865 input.as_bytes(),
2866 &FeatureSet::empty(),
2867 &vec!["fuchsia.logger.LogSink".into()],
2868 &[],
2869 &["diagnostics".to_string()],
2870 );
2871 assert_matches!(result,
2872 Err(Error::Validate { err, filename }) => {
2873 assert_eq!(
2874 err,
2875 fail_to_make_required_offer_dictionary("diagnostics", "collection", "coll"),
2876 );
2877 assert!(filename.is_some(), "Expected there to be a filename in error message");
2878 }
2879 );
2880 }
2881
2882 #[test]
2883 fn fail_to_offer_dictionary_to_all_when_required_even_if_protocol_called_diagnostics_offered() {
2884 let input = r##"{
2885 children: [
2886 {
2887 name: "logger",
2888 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
2889 },
2890 {
2891 name: "something",
2892 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
2893 },
2894 ],
2895 offer: [
2896 {
2897 protocol: "fuchsia.logger.LogSink",
2898 from: "parent",
2899 to: "all"
2900 },
2901 {
2902 protocol: "diagnostics",
2903 from: "parent",
2904 to: "all"
2905 },
2906 {
2907 protocol: "fuchsia.logger.LegacyLog",
2908 from: "parent",
2909 to: "#something"
2910 },
2911 ]
2912 }"##;
2913 let result = validate_with_features_for_test(
2914 "test.cml",
2915 input.as_bytes(),
2916 &FeatureSet::empty(),
2917 &[],
2918 &[],
2919 &["diagnostics".to_string()],
2920 );
2921
2922 assert_matches!(result,
2923 Err(Error::Validate { err, filename }) => {
2924 assert_eq!(
2925 err,
2926 fail_to_make_required_offer_dictionary(
2927 "diagnostics",
2928 "child component",
2929 "logger",
2930 ),
2931 );
2932 assert!(filename.is_some(), "Expected there to be a filename in error message");
2933 }
2934 );
2935 }
2936
2937 #[test]
2938 fn test_validate_invalid_json_fails() {
2939 let result = validate_for_test("test.cml", b"{");
2940 let expected_err = r#" --> 1:2
2941 |
29421 | {
2943 | ^---
2944 |
2945 = expected identifier or string"#;
2946 assert_matches!(result, Err(Error::Parse { err, .. }) if &err == expected_err);
2947 }
2948
2949 #[test]
2950 fn test_cml_json5() {
2951 let input = r##"{
2952 "expose": [
2953 // Here are some services to expose.
2954 { "protocol": "fuchsia.logger.Log", "from": "#logger", },
2955 { "directory": "blobfs", "from": "#logger", "rights": ["rw*"]},
2956 ],
2957 "children": [
2958 {
2959 name: 'logger',
2960 'url': 'fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm',
2961 },
2962 ],
2963 }"##;
2964 let result = validate_for_test("test.cml", input.as_bytes());
2965 assert_matches!(result, Ok(()));
2966 }
2967
2968 #[test]
2969 fn test_cml_error_location() {
2970 let input = r##"{
2971 "use": [
2972 {
2973 "protocol": "foo",
2974 "from": "bad",
2975 },
2976 ],
2977}"##;
2978 let result = validate_for_test("test.cml", input.as_bytes());
2979 assert_matches!(
2980 result,
2981 Err(Error::Parse { err, location: Some(l), filename: Some(f) })
2982 if &err == "invalid value: string \"bad\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none" &&
2983 l == Location { line: 5, column: 21 } &&
2984 f.ends_with("test.cml")
2985 );
2986 }
2987
2988 test_validate_cml! {
2989 test_cml_empty_include(
2991 json!(
2992 {
2993 "include": [],
2994 }
2995 ),
2996 Ok(())
2997 ),
2998 test_cml_some_include(
2999 json!(
3000 {
3001 "include": [ "some.cml" ],
3002 }
3003 ),
3004 Ok(())
3005 ),
3006 test_cml_couple_of_include(
3007 json!(
3008 {
3009 "include": [ "some1.cml", "some2.cml" ],
3010 }
3011 ),
3012 Ok(())
3013 ),
3014
3015 test_cml_empty_json(
3017 json!({}),
3018 Ok(())
3019 ),
3020 test_cml_program(
3021 json!(
3022 {
3023 "program": {
3024 "runner": "elf",
3025 "binary": "bin/app",
3026 },
3027 }
3028 ),
3029 Ok(())
3030 ),
3031 test_cml_program_use_runner(
3032 json!(
3033 {
3034 "program": {
3035 "binary": "bin/app",
3036 },
3037 "use": [
3038 { "runner": "elf", "from": "parent" }
3039 ]
3040 }
3041 ),
3042 Ok(())
3043 ),
3044 test_cml_program_use_runner_conflict(
3045 json!(
3046 {
3047 "program": {
3048 "runner": "elf",
3049 "binary": "bin/app",
3050 },
3051 "use": [
3052 { "runner": "elf", "from": "parent" }
3053 ]
3054 }
3055 ),
3056 Err(Error::Validate { err, .. }) if &err ==
3057 "Component has conflicting runners in `program` block and `use` block."
3058 ),
3059 test_cml_program_no_runner(
3060 json!({"program": { "binary": "bin/app" }}),
3061 Err(Error::Validate { err, .. }) if &err ==
3062 "Component has a `program` block defined, but doesn't specify a `runner`. \
3063 Components need to use a runner to actually execute code."
3064 ),
3065
3066 test_cml_use(
3068 json!({
3069 "use": [
3070 { "protocol": "CoolFonts", "path": "/svc/MyFonts" },
3071 { "protocol": "CoolFonts2", "path": "/svc/MyFonts2", "from": "parent/dict" },
3072 { "protocol": "fuchsia.test.hub.HubReport", "from": "framework" },
3073 { "protocol": "fuchsia.sys2.StorageAdmin", "from": "#data-storage" },
3074 { "protocol": ["fuchsia.ui.scenic.Scenic", "fuchsia.logger.LogSink"] },
3075 {
3076 "directory": "assets",
3077 "path": "/data/assets",
3078 "rights": ["rw*"],
3079 },
3080 {
3081 "directory": "config",
3082 "from": "parent",
3083 "path": "/data/config",
3084 "rights": ["rx*"],
3085 "subdir": "fonts/all",
3086 },
3087 { "storage": "data", "path": "/example" },
3088 { "storage": "cache", "path": "/tmp" },
3089 {
3090 "event_stream": ["started", "stopped", "running"],
3091 "scope":["#test"],
3092 "path":"/svc/testpath",
3093 "from":"parent",
3094 },
3095 { "runner": "usain", "from": "parent" }
3096 ],
3097 "capabilities": [
3098 {
3099 "storage": "data-storage",
3100 "from": "parent",
3101 "backing_dir": "minfs",
3102 "storage_id": "static_instance_id_or_moniker",
3103 }
3104 ]
3105 }),
3106 Ok(())
3107 ),
3108 test_cml_expose_event_stream_multiple_as(
3109 json!({
3110 "expose": [
3111 {
3112 "event_stream": ["started", "stopped"],
3113 "from" : "framework",
3114 "as": "something"
3115 },
3116 ]
3117 }),
3118 Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple event streams"
3119 ),
3120 test_cml_offer_event_stream_capability_requested_not_from_framework(
3121 json!({
3122 "offer": [
3123 {
3124 "event_stream": ["capability_requested", "stopped"],
3125 "from" : "parent",
3126 "to": "#something"
3127 },
3128 ]
3129 }),
3130 Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target from \"parent\" but it does not appear in \"children\" or \"collections\""
3131 ),
3132 test_cml_offer_event_stream_capability_requested_with_filter(
3133 json!({
3134 "offer": [
3135 {
3136 "event_stream": "capability_requested",
3137 "from" : "framework",
3138 "to": "#something",
3139 },
3140 ]
3141 }),
3142 Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target from \"framework\" but it does not appear in \"children\" or \"collections\""
3143 ),
3144 test_cml_offer_event_stream_multiple_as(
3145 json!({
3146 "offer": [
3147 {
3148 "event_stream": ["started", "stopped"],
3149 "from" : "framework",
3150 "to": "#self",
3151 "as": "something"
3152 },
3153 ]
3154 }),
3155 Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple events"
3156 ),
3157 test_cml_expose_event_stream_from_self(
3158 json!({
3159 "expose": [
3160 { "event_stream": ["started", "stopped"], "from" : "self" },
3161 ]
3162 }),
3163 Err(Error::Validate { err, .. }) if &err == "Cannot expose event_streams from self"
3164 ),
3165 test_cml_offer_event_stream_from_self(
3166 json!({
3167 "offer": [
3168 { "event_stream": ["started", "stopped"], "from" : "self", "to": "#self" },
3169 ]
3170 }),
3171 Err(Error::Validate { err, .. }) if &err == "cannot offer an event_stream from self"
3172 ),
3173 test_cml_offer_event_stream_from_anything_else(
3174 json!({
3175 "offer": [
3176 {
3177 "event_stream": ["started", "stopped"],
3178 "from" : "framework",
3179 "to": "#self"
3180 },
3181 ]
3182 }),
3183 Err(Error::Validate { err, .. }) if &err == "\"#self\" is an \"offer\" target from \"framework\" but it does not appear in \"children\" or \"collections\""
3184 ),
3185 test_cml_expose_event_stream_to_framework(
3186 json!({
3187 "expose": [
3188 {
3189 "event_stream": ["started", "stopped"],
3190 "from" : "self",
3191 "to": "framework"
3192 },
3193 ]
3194 }),
3195 Err(Error::Validate { err, .. }) if &err == "cannot expose an event_stream to framework"
3196 ),
3197 test_cml_expose_event_stream_scope_invalid_component(
3198 json!({
3199 "expose": [
3200 {
3201 "event_stream": ["started", "stopped"],
3202 "from" : "framework",
3203 "scope":["#invalid_component"]
3204 },
3205 ]
3206 }),
3207 Err(Error::Validate { err, .. }) if &err == "event_stream scope invalid_component did not match a component or collection in this .cml file."
3208 ),
3209
3210 test_cml_use_from_self(
3211 json!({
3212 "use": [
3213 {
3214 "protocol": [ "bar_protocol", "baz_protocol" ],
3215 "from": "self",
3216 },
3217 {
3218 "directory": "foo_directory",
3219 "from": "self",
3220 "path": "/dir",
3221 "rights": [ "r*" ],
3222 },
3223 {
3224 "service": "foo_service",
3225 "from": "self",
3226 },
3227 {
3228 "config": "foo_config",
3229 "type": "bool",
3230 "key": "k",
3231 "from": "self",
3232 },
3233 ],
3234 "capabilities": [
3235 {
3236 "protocol": "bar_protocol",
3237 },
3238 {
3239 "protocol": "baz_protocol",
3240 },
3241 {
3242 "directory": "foo_directory",
3243 "path": "/dir",
3244 "rights": [ "r*" ],
3245 },
3246 {
3247 "service": "foo_service",
3248 },
3249 {
3250 "config": "foo_config",
3251 "type": "bool",
3252 },
3253 ]
3254 }),
3255 Ok(())
3256 ),
3257 test_cml_use_protocol_from_self_missing(
3258 json!({
3259 "use": [
3260 {
3261 "protocol": "foo_protocol",
3262 "from": "self",
3263 },
3264 ],
3265 }),
3266 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is used from self, so it must be declared as a \"protocol\" in \"capabilities\""
3267 ),
3268 test_cml_use_directory_from_self_missing(
3269 json!({
3270 "use": [
3271 {
3272 "directory": "foo_directory",
3273 "from": "self",
3274 },
3275 ],
3276 }),
3277 Err(Error::Validate { err, .. }) if &err == "directory \"foo_directory\" is used from self, so it must be declared as a \"directory\" in \"capabilities\""
3278 ),
3279 test_cml_use_service_from_self_missing(
3280 json!({
3281 "use": [
3282 {
3283 "service": "foo_service",
3284 "from": "self",
3285 },
3286 ],
3287 }),
3288 Err(Error::Validate { err, .. }) if &err == "service \"foo_service\" is used from self, so it must be declared as a \"service\" in \"capabilities\""
3289 ),
3290 test_cml_use_config_from_self_missing(
3291 json!({
3292 "use": [
3293 {
3294 "config": "foo_config",
3295 "from": "self",
3296 },
3297 ],
3298 }),
3299 Err(Error::Validate { err, .. }) if &err == "config \"foo_config\" is used from self, so it must be declared as a \"config\" in \"capabilities\""
3300 ),
3301 test_cml_use_from_self_missing_dictionary(
3302 json!({
3303 "use": [
3304 {
3305 "protocol": "foo_protocol",
3306 "from": "self/dict/inner",
3307 },
3308 ],
3309 }),
3310 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is used from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
3311 ),
3312 test_cml_use_event_stream_duplicate(
3313 json!({
3314 "use": [
3315 { "event_stream": ["started", "started"], "from" : "parent" },
3316 ]
3317 }),
3318 Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a name or nonempty array of names, with unique elements"
3319 ),
3320 test_cml_use_event_stream_overlapping_path(
3321 json!({
3322 "use": [
3323 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
3324 {
3325 "event_stream": ["started"],
3326 "path": "/foo/bar/baz/er",
3327 "from": "parent",
3328 },
3329 ],
3330 }),
3331 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar/baz\" is a prefix of \"use\" target event_stream \"/foo/bar/baz/er\""
3332 ),
3333 test_cml_use_event_stream_invalid_path(
3334 json!({
3335 "use": [
3336 {
3337 "event_stream": ["started"],
3338 "path": "my_stream",
3339 "from": "parent",
3340 },
3341 ],
3342 }),
3343 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"my_stream\", expected a path with leading `/` and non-empty segments, where each segment is no more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., and cannot contain embedded NULs"
3344 ),
3345 test_cml_use_event_stream_self_ref(
3346 json!({
3347 "use": [
3348 {
3349 "event_stream": ["started"],
3350 "path": "/svc/my_stream",
3351 "from": "self",
3352 },
3353 ],
3354 }),
3355 Err(Error::Validate { err, .. }) if &err == "\"from: self\" cannot be used with \"event_stream\""
3356 ),
3357 test_cml_use_runner_debug_ref(
3358 json!({
3359 "use": [
3360 {
3361 "runner": "elf",
3362 "from": "debug",
3363 },
3364 ],
3365 }),
3366 Err(Error::Validate { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
3367 ),
3368 test_cml_use_runner_self_ref(
3369 json!({
3370 "use": [
3371 {
3372 "runner": "elf",
3373 "from": "self",
3374 },
3375 ],
3376 }),
3377 Err(Error::Validate { err, .. }) if &err == "\"from: self\" cannot be used with \"runner\""
3378 ),
3379 test_cml_use_missing_props(
3380 json!({
3381 "use": [ { "path": "/svc/fuchsia.logger.Log" } ]
3382 }),
3383 Err(Error::Validate { err, .. }) if &err == "`use` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"event_stream\", \"runner\", \"config\""
3384 ),
3385 test_cml_use_from_with_storage(
3386 json!({
3387 "use": [ { "storage": "cache", "from": "parent" } ]
3388 }),
3389 Err(Error::Validate { err, .. }) if &err == "\"from\" cannot be used with \"storage\""
3390 ),
3391 test_cml_use_invalid_from(
3392 json!({
3393 "use": [
3394 { "protocol": "CoolFonts", "from": "bad" }
3395 ]
3396 }),
3397 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
3398 ),
3399 test_cml_use_invalid_from_dictionary(
3400 json!({
3401 "use": [
3402 { "protocol": "CoolFonts", "from": "bad/dict" }
3403 ]
3404 }),
3405 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad/dict\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
3406 ),
3407 test_cml_use_from_missing_capability(
3408 json!({
3409 "use": [
3410 { "protocol": "fuchsia.sys2.Admin", "from": "#mystorage" }
3411 ]
3412 }),
3413 Err(Error::Validate { err, .. }) if &err == "\"use\" source \"#mystorage\" does not appear in \"children\" or \"capabilities\""
3414 ),
3415 test_cml_use_bad_path(
3416 json!({
3417 "use": [
3418 {
3419 "protocol": ["CoolFonts", "FunkyFonts"],
3420 "path": "/MyFonts"
3421 }
3422 ]
3423 }),
3424 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."
3425 ),
3426 test_cml_use_bad_duplicate_target_names(
3427 json!({
3428 "use": [
3429 { "protocol": "fuchsia.component.Realm" },
3430 { "protocol": "fuchsia.component.Realm" },
3431 ],
3432 }),
3433 Err(Error::Validate { err, .. }) if &err == "\"/svc/fuchsia.component.Realm\" is a duplicate \"use\" target protocol"
3434 ),
3435 test_cml_use_empty_protocols(
3436 json!({
3437 "use": [
3438 {
3439 "protocol": [],
3440 },
3441 ],
3442 }),
3443 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
3444 ),
3445 test_cml_use_bad_subdir(
3446 json!({
3447 "use": [
3448 {
3449 "directory": "config",
3450 "path": "/config",
3451 "from": "parent",
3452 "rights": [ "r*" ],
3453 "subdir": "/",
3454 },
3455 ]
3456 }),
3457 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
3458 ),
3459 test_cml_use_resolver_fails(
3460 json!({
3461 "use": [
3462 {
3463 "resolver": "pkg_resolver",
3464 },
3465 ]
3466 }),
3467 Err(Error::Parse { err, .. }) if err.starts_with("unknown field `resolver`, expected one of")
3468 ),
3469
3470 test_cml_use_disallows_nested_dirs_directory(
3471 json!({
3472 "use": [
3473 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
3474 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
3475 ],
3476 }),
3477 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target directory \"/foo/bar/baz\""
3478 ),
3479 test_cml_use_disallows_nested_dirs_storage(
3480 json!({
3481 "use": [
3482 { "storage": "foobar", "path": "/foo/bar" },
3483 { "storage": "foobarbaz", "path": "/foo/bar/baz" },
3484 ],
3485 }),
3486 Err(Error::Validate { err, .. }) if &err == "storage \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
3487 ),
3488 test_cml_use_disallows_nested_dirs_directory_and_storage(
3489 json!({
3490 "use": [
3491 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
3492 { "storage": "foobarbaz", "path": "/foo/bar/baz" },
3493 ],
3494 }),
3495 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
3496 ),
3497 test_cml_use_disallows_common_prefixes_service(
3498 json!({
3499 "use": [
3500 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
3501 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia" },
3502 ],
3503 }),
3504 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia\""
3505 ),
3506 test_cml_use_disallows_common_prefixes_protocol(
3507 json!({
3508 "use": [
3509 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
3510 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia.2" },
3511 ],
3512 }),
3513 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia.2\""
3514 ),
3515 test_cml_use_disallows_pkg_conflicts_for_directories(
3516 json!({
3517 "use": [
3518 { "directory": "dir", "path": "/pkg/dir", "rights": [ "r*" ] },
3519 ],
3520 }),
3521 Err(Error::Validate { err, .. }) if &err == "directory \"/pkg/dir\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
3522 ),
3523 test_cml_use_disallows_pkg_conflicts_for_protocols(
3524 json!({
3525 "use": [
3526 { "protocol": "prot", "path": "/pkg/protocol" },
3527 ],
3528 }),
3529 Err(Error::Validate { err, .. }) if &err == "protocol \"/pkg/protocol\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
3530 ),
3531 test_cml_use_disallows_pkg_conflicts_for_storage(
3532 json!({
3533 "use": [
3534 { "storage": "store", "path": "/pkg/storage" },
3535 ],
3536 }),
3537 Err(Error::Validate { err, .. }) if &err == "storage \"/pkg/storage\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
3538 ),
3539 test_cml_use_disallows_filter_on_non_events(
3540 json!({
3541 "use": [
3542 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ], "filter": {"path": "/diagnostics"} },
3543 ],
3544 }),
3545 Err(Error::Validate { err, .. }) if &err == "\"filter\" can only be used with \"event_stream\""
3546 ),
3547 test_cml_availability_not_supported_for_event_streams(
3548 json!({
3549 "use": [
3550 {
3551 "event_stream": ["destroyed"],
3552 "from": "parent",
3553 "availability": "optional",
3554 }
3555 ]
3556 }),
3557 Err(Error::Validate { err, .. }) if &err == "\"availability\" cannot be used with \"event_stream\""
3558 ),
3559 test_cml_use_from_child_offer_cycle_strong(
3560 json!({
3561 "capabilities": [
3562 { "protocol": ["fuchsia.example.Protocol"] },
3563 ],
3564 "children": [
3565 {
3566 "name": "child",
3567 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
3568 },
3569 ],
3570 "use": [
3571 {
3572 "protocol": "fuchsia.child.Protocol",
3573 "from": "#child",
3574 },
3575 ],
3576 "offer": [
3577 {
3578 "protocol": "fuchsia.example.Protocol",
3579 "from": "self",
3580 "to": [ "#child" ],
3581 },
3582 ],
3583 }),
3584 Err(Error::Validate { err, .. })
3585 if &err == "Strong dependency cycles were found. Break the cycle by removing a \
3586 dependency or marking an offer as weak. Cycles: \
3587 {{#child -> self -> #child}}"
3588 ),
3589 test_cml_use_from_parent_weak(
3590 json!({
3591 "use": [
3592 {
3593 "protocol": "fuchsia.parent.Protocol",
3594 "from": "parent",
3595 "dependency": "weak",
3596 },
3597 ],
3598 }),
3599 Err(Error::Validate { err, .. }) if &err == "Only `use` from children can have dependency: \"weak\""
3600 ),
3601 test_cml_use_from_child_offer_cycle_weak(
3602 json!({
3603 "capabilities": [
3604 { "protocol": ["fuchsia.example.Protocol"] },
3605 { "service": ["fuchsia.example.Service"] },
3606 ],
3607 "children": [
3608 {
3609 "name": "child",
3610 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
3611 },
3612 ],
3613 "use": [
3614 {
3615 "protocol": "fuchsia.example.Protocol",
3616 "from": "#child",
3617 "dependency": "weak",
3618 },
3619 {
3620 "service": "fuchsia.example.Service",
3621 "from": "#child",
3622 "dependency": "weak",
3623 },
3624 ],
3625 "offer": [
3626 {
3627 "protocol": "fuchsia.example.Protocol",
3628 "from": "self",
3629 "to": [ "#child" ],
3630 },
3631 {
3632 "service": "fuchsia.example.Service",
3633 "from": "self",
3634 "to": [ "#child" ],
3635 },
3636 ],
3637 }),
3638 Ok(())
3639 ),
3640 test_cml_use_from_child_offer_storage_no_cycle(
3641 json!({
3642 "capabilities": [
3643 {
3644 "storage": "cdata",
3645 "from": "#backend",
3646 "backing_dir": "blobfs",
3647 "storage_id": "static_instance_id_or_moniker",
3648 },
3649 {
3650 "storage": "pdata",
3651 "from": "parent",
3652 "backing_dir": "blobfs",
3653 "storage_id": "static_instance_id_or_moniker",
3654 },
3655 ],
3656 "children": [
3657 {
3658 "name": "child",
3659 "url": "#meta/child.cm",
3660 },
3661 {
3662 "name": "backend",
3663 "url": "#meta/backend.cm",
3664 },
3665 ],
3666 "use": [
3667 {
3668 "protocol": "fuchsia.example.Protocol",
3669 "from": "#child",
3670 },
3671 ],
3672 "offer": [
3673 {
3674 "storage": "cdata",
3675 "from": "self",
3676 "to": "#child",
3677 },
3678 {
3679 "storage": "pdata",
3680 "from": "self",
3681 "to": "#child",
3682 },
3683 ],
3684 }),
3685 Ok(())
3686 ),
3687 test_cml_use_from_child_offer_storage_cycle(
3688 json!({
3689 "capabilities": [
3690 {
3691 "storage": "data",
3692 "from": "self",
3693 "backing_dir": "blobfs",
3694 "storage_id": "static_instance_id_or_moniker",
3695 },
3696 ],
3697 "children": [
3698 {
3699 "name": "child",
3700 "url": "#meta/child.cm",
3701 },
3702 ],
3703 "use": [
3704 {
3705 "protocol": "fuchsia.example.Protocol",
3706 "from": "#child",
3707 },
3708 ],
3709 "offer": [
3710 {
3711 "storage": "data",
3712 "from": "self",
3713 "to": "#child",
3714 },
3715 ],
3716 }),
3717 Err(Error::Validate {
3718 err,
3719 ..
3720 }) if &err ==
3721 "Strong dependency cycles were found. Break the cycle by removing a dependency \
3722 or marking an offer as weak. Cycles: {{#child -> self -> #data -> #child}}"
3723 ),
3724
3725 test_cml_expose(
3727 json!({
3728 "expose": [
3729 {
3730 "protocol": "A",
3731 "from": "self",
3732 },
3733 {
3734 "protocol": ["B", "C"],
3735 "from": "self",
3736 },
3737 {
3738 "protocol": "D",
3739 "from": "#mystorage",
3740 },
3741 {
3742 "directory": "blobfs",
3743 "from": "self",
3744 "rights": ["r*"],
3745 "subdir": "blob",
3746 },
3747 { "directory": "data", "from": "framework" },
3748 { "runner": "elf", "from": "#logger", },
3749 { "resolver": "pkg_resolver", "from": "#logger" },
3750 ],
3751 "capabilities": [
3752 { "protocol": ["A", "B", "C"] },
3753 {
3754 "directory": "blobfs",
3755 "path": "/blobfs",
3756 "rights": ["rw*"],
3757 },
3758 {
3759 "storage": "mystorage",
3760 "from": "self",
3761 "backing_dir": "blobfs",
3762 "storage_id": "static_instance_id_or_moniker",
3763 }
3764 ],
3765 "children": [
3766 {
3767 "name": "logger",
3768 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
3769 },
3770 ]
3771 }),
3772 Ok(())
3773 ),
3774 test_cml_expose_all_valid_chars(
3775 json!({
3776 "expose": [
3777 {
3778 "protocol": "fuchsia.logger.Log",
3779 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-.",
3780 },
3781 ],
3782 "children": [
3783 {
3784 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
3785 "url": "https://www.google.com/gmail"
3786 },
3787 ],
3788 }),
3789 Ok(())
3790 ),
3791 test_cml_expose_missing_props(
3792 json!({
3793 "expose": [ {} ]
3794 }),
3795 Err(Error::Parse { err, .. }) if &err == "missing field `from`"
3796 ),
3797 test_cml_expose_missing_from(
3798 json!({
3799 "expose": [
3800 { "protocol": "fuchsia.logger.Log", "from": "#missing" },
3801 ],
3802 }),
3803 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
3804 ),
3805 test_cml_expose_duplicate_target_names(
3806 json!({
3807 "capabilities": [
3808 { "protocol": "logger" },
3809 ],
3810 "expose": [
3811 { "protocol": "logger", "from": "self", "as": "thing" },
3812 { "directory": "thing", "from": "#child" , "rights": ["rx*"] },
3813 ],
3814 "children": [
3815 {
3816 "name": "child",
3817 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
3818 },
3819 ],
3820 }),
3821 Err(Error::Validate { err, .. }) if &err == "\"thing\" is a duplicate \"expose\" target capability for \"parent\""
3822 ),
3823 test_cml_expose_invalid_multiple_from(
3824 json!({
3825 "capabilities": [
3826 { "protocol": "fuchsia.logger.Log" },
3827 ],
3828 "expose": [
3829 {
3830 "protocol": "fuchsia.logger.Log",
3831 "from": [ "self", "#logger" ],
3832 },
3833 ],
3834 "children": [
3835 {
3836 "name": "logger",
3837 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
3838 },
3839 ]
3840 }),
3841 Err(Error::Validate { err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses"
3842 ),
3843 test_cml_expose_from_missing_named_source(
3844 json!({
3845 "expose": [
3846 {
3847 "protocol": "fuchsia.logger.Log",
3848 "from": "#does-not-exist",
3849 },
3850 ],
3851 }),
3852 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\""
3853 ),
3854 test_cml_expose_bad_from(
3855 json!({
3856 "expose": [ {
3857 "protocol": "fuchsia.logger.Log", "from": "parent"
3858 } ]
3859 }),
3860 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected one or an array of \"framework\", \"self\", \"#<child-name>\", or a dictionary path"
3861 ),
3862 test_cml_expose_bad_as(
3864 json!({
3865 "expose": [
3866 {
3867 "protocol": ["A", "B"],
3868 "from": "#echo_server",
3869 "as": "thing"
3870 },
3871 ],
3872 "children": [
3873 {
3874 "name": "echo_server",
3875 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
3876 }
3877 ]
3878 }),
3879 Err(Error::Validate { err, .. }) if &err == "\"as\" can only be specified when one `protocol` is supplied."
3880 ),
3881 test_cml_expose_empty_protocols(
3882 json!({
3883 "expose": [
3884 {
3885 "protocol": [],
3886 "from": "#child",
3887 "as": "thing"
3888 },
3889 ],
3890 "children": [
3891 {
3892 "name": "child",
3893 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
3894 },
3895 ],
3896 }),
3897 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
3898 ),
3899 test_cml_expose_bad_subdir(
3900 json!({
3901 "expose": [
3902 {
3903 "directory": "blobfs",
3904 "from": "self",
3905 "rights": ["r*"],
3906 "subdir": "/",
3907 },
3908 ]
3909 }),
3910 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
3911 ),
3912 test_cml_expose_invalid_subdir_to_framework(
3913 json!({
3914 "capabilities": [
3915 {
3916 "directory": "foo",
3917 "rights": ["r*"],
3918 "path": "/foo",
3919 },
3920 ],
3921 "expose": [
3922 {
3923 "directory": "foo",
3924 "from": "self",
3925 "to": "framework",
3926 "subdir": "blob",
3927 },
3928 ],
3929 "children": [
3930 {
3931 "name": "child",
3932 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
3933 },
3934 ],
3935 }),
3936 Err(Error::Validate { err, .. }) if &err == "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
3937 ),
3938 test_cml_expose_from_self(
3939 json!({
3940 "expose": [
3941 {
3942 "protocol": "foo_protocol",
3943 "from": "self",
3944 },
3945 {
3946 "protocol": [ "bar_protocol", "baz_protocol" ],
3947 "from": "self",
3948 },
3949 {
3950 "directory": "foo_directory",
3951 "from": "self",
3952 },
3953 {
3954 "runner": "foo_runner",
3955 "from": "self",
3956 },
3957 {
3958 "resolver": "foo_resolver",
3959 "from": "self",
3960 },
3961 ],
3962 "capabilities": [
3963 {
3964 "protocol": "foo_protocol",
3965 },
3966 {
3967 "protocol": "bar_protocol",
3968 },
3969 {
3970 "protocol": "baz_protocol",
3971 },
3972 {
3973 "directory": "foo_directory",
3974 "path": "/dir",
3975 "rights": [ "r*" ],
3976 },
3977 {
3978 "runner": "foo_runner",
3979 "path": "/svc/runner",
3980 },
3981 {
3982 "resolver": "foo_resolver",
3983 "path": "/svc/resolver",
3984 },
3985 ]
3986 }),
3987 Ok(())
3988 ),
3989 test_cml_expose_protocol_from_self_missing(
3990 json!({
3991 "expose": [
3992 {
3993 "protocol": "pkg_protocol",
3994 "from": "self",
3995 },
3996 ],
3997 }),
3998 Err(Error::Validate { err, .. }) if &err == "protocol \"pkg_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
3999 ),
4000 test_cml_expose_protocol_from_self_missing_multiple(
4001 json!({
4002 "expose": [
4003 {
4004 "protocol": [ "foo_protocol", "bar_protocol" ],
4005 "from": "self",
4006 },
4007 ],
4008 }),
4009 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
4010 ),
4011 test_cml_expose_directory_from_self_missing(
4012 json!({
4013 "expose": [
4014 {
4015 "directory": "pkg_directory",
4016 "from": "self",
4017 },
4018 ],
4019 }),
4020 Err(Error::Validate { err, .. }) if &err == "directory \"pkg_directory\" is exposed from self, so it must be declared as a \"directory\" in \"capabilities\""
4021 ),
4022 test_cml_expose_service_from_self_missing(
4023 json!({
4024 "expose": [
4025 {
4026 "service": "pkg_service",
4027 "from": "self",
4028 },
4029 ],
4030 }),
4031 Err(Error::Validate { err, .. }) if &err == "service \"pkg_service\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\""
4032 ),
4033 test_cml_expose_runner_from_self_missing(
4034 json!({
4035 "expose": [
4036 {
4037 "runner": "dart",
4038 "from": "self",
4039 },
4040 ],
4041 }),
4042 Err(Error::Validate { err, .. }) if &err == "runner \"dart\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\""
4043 ),
4044 test_cml_expose_resolver_from_self_missing(
4045 json!({
4046 "expose": [
4047 {
4048 "resolver": "pkg_resolver",
4049 "from": "self",
4050 },
4051 ],
4052 }),
4053 Err(Error::Validate { err, .. }) if &err == "resolver \"pkg_resolver\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\""
4054 ),
4055 test_cml_expose_from_self_missing_dictionary(
4056 json!({
4057 "expose": [
4058 {
4059 "protocol": "foo_protocol",
4060 "from": "self/dict/inner",
4061 },
4062 ],
4063 }),
4064 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is exposed from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
4065 ),
4066 test_cml_expose_from_dictionary_invalid(
4067 json!({
4068 "expose": [
4069 {
4070 "protocol": "pkg_protocol",
4071 "from": "bad/a",
4072 },
4073 ],
4074 }),
4075 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad/a\", expected one or an array of \"framework\", \"self\", \"#<child-name>\", or a dictionary path"
4076 ),
4077 test_cml_expose_from_dictionary_parent(
4078 json!({
4079 "expose": [
4080 {
4081 "protocol": "pkg_protocol",
4082 "from": "parent/a",
4083 },
4084 ],
4085 }),
4086 Err(Error::Validate { err, .. }) if &err == "`expose` dictionary path must begin with `self` or `#<child-name>`"
4087 ),
4088 test_cml_expose_protocol_from_collection_invalid(
4089 json!({
4090 "collections": [ {
4091 "name": "coll",
4092 "durability": "transient",
4093 } ],
4094 "expose": [
4095 { "protocol": "fuchsia.logger.Log", "from": "#coll" },
4096 ]
4097 }),
4098 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\" or \"capabilities\""
4099 ),
4100 test_cml_expose_directory_from_collection_invalid(
4101 json!({
4102 "collections": [ {
4103 "name": "coll",
4104 "durability": "transient",
4105 } ],
4106 "expose": [
4107 { "directory": "temp", "from": "#coll" },
4108 ]
4109 }),
4110 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
4111 ),
4112 test_cml_expose_runner_from_collection_invalid(
4113 json!({
4114 "collections": [ {
4115 "name": "coll",
4116 "durability": "transient",
4117 } ],
4118 "expose": [
4119 { "runner": "elf", "from": "#coll" },
4120 ]
4121 }),
4122 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
4123 ),
4124 test_cml_expose_resolver_from_collection_invalid(
4125 json!({
4126 "collections": [ {
4127 "name": "coll",
4128 "durability": "transient",
4129 } ],
4130 "expose": [
4131 { "resolver": "base", "from": "#coll" },
4132 ]
4133 }),
4134 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
4135 ),
4136 test_cml_expose_to_framework_ok(
4137 json!({
4138 "capabilities": [
4139 {
4140 "directory": "foo",
4141 "path": "/foo",
4142 "rights": ["r*"],
4143 },
4144 ],
4145 "expose": [
4146 {
4147 "directory": "foo",
4148 "from": "self",
4149 "to": "framework"
4150 }
4151 ],
4152 "children": [
4153 {
4154 "name": "child",
4155 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
4156 },
4157 ],
4158 }),
4159 Ok(())
4160 ),
4161 test_cml_offer(
4163 json!({
4164 "offer": [
4165 {
4166 "protocol": "fuchsia.fonts.LegacyProvider",
4167 "from": "parent",
4168 "to": [ "#echo_server" ],
4169 "dependency": "weak"
4170 },
4171 {
4172 "protocol": "fuchsia.sys2.StorageAdmin",
4173 "from": "#data",
4174 "to": [ "#echo_server" ]
4175 },
4176 {
4177 "protocol": [
4178 "fuchsia.settings.Accessibility",
4179 "fuchsia.ui.scenic.Scenic"
4180 ],
4181 "from": "parent",
4182 "to": [ "#echo_server" ],
4183 "dependency": "strong"
4184 },
4185 {
4186 "directory": "assets",
4187 "from": "self",
4188 "to": [ "#echo_server" ],
4189 "rights": ["r*"]
4190 },
4191 {
4192 "directory": "index",
4193 "subdir": "files",
4194 "from": "parent",
4195 "to": [ "#modular" ],
4196 "dependency": "weak"
4197 },
4198 {
4199 "directory": "config",
4200 "from": "framework",
4201 "to": [ "#modular" ],
4202 "as": "config",
4203 "dependency": "strong"
4204 },
4205 {
4206 "storage": "data",
4207 "from": "self",
4208 "to": [ "#modular", "#logger" ]
4209 },
4210 {
4211 "runner": "elf",
4212 "from": "parent",
4213 "to": [ "#modular", "#logger" ]
4214 },
4215 {
4216 "resolver": "pkg_resolver",
4217 "from": "parent",
4218 "to": [ "#modular" ],
4219 },
4220 ],
4221 "children": [
4222 {
4223 "name": "logger",
4224 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
4225 },
4226 {
4227 "name": "echo_server",
4228 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
4229 },
4230 ],
4231 "collections": [
4232 {
4233 "name": "modular",
4234 "durability": "transient",
4235 },
4236 ],
4237 "capabilities": [
4238 {
4239 "directory": "assets",
4240 "path": "/data/assets",
4241 "rights": [ "rw*" ],
4242 },
4243 {
4244 "storage": "data",
4245 "from": "parent",
4246 "backing_dir": "minfs",
4247 "storage_id": "static_instance_id_or_moniker",
4248 },
4249 ],
4250 }),
4251 Ok(())
4252 ),
4253 test_cml_offer_all_valid_chars(
4254 json!({
4255 "offer": [
4256 {
4257 "protocol": "fuchsia.logger.Log",
4258 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
4259 "to": [ "#abcdefghijklmnopqrstuvwxyz0123456789_-to" ],
4260 },
4261 ],
4262 "children": [
4263 {
4264 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
4265 "url": "https://www.google.com/gmail"
4266 },
4267 {
4268 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-to",
4269 "url": "https://www.google.com/gmail"
4270 },
4271 ],
4272 "capabilities": [
4273 {
4274 "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage",
4275 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
4276 "backing_dir": "example",
4277 "storage_id": "static_instance_id_or_moniker",
4278 }
4279 ]
4280 }),
4281 Ok(())
4282 ),
4283 test_cml_offer_singleton_to (
4284 json!({
4285 "offer": [
4286 {
4287 "protocol": "fuchsia.fonts.LegacyProvider",
4288 "from": "parent",
4289 "to": "#echo_server",
4290 "dependency": "weak"
4291 },
4292 ],
4293 "children": [
4294 {
4295 "name": "echo_server",
4296 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
4297 },
4298 ],
4299 }),
4300 Ok(())
4301 ),
4302 test_cml_offer_missing_props(
4303 json!({
4304 "offer": [ {} ]
4305 }),
4306 Err(Error::Parse { err, .. }) if &err == "missing field `from`"
4307 ),
4308 test_cml_offer_missing_from(
4309 json!({
4310 "offer": [
4311 {
4312 "protocol": "fuchsia.logger.Log",
4313 "from": "#missing",
4314 "to": [ "#echo_server" ],
4315 },
4316 ],
4317 "children": [
4318 {
4319 "name": "echo_server",
4320 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
4321 },
4322 ],
4323 }),
4324 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
4325 ),
4326 test_cml_storage_offer_from_child(
4327 json!({
4328 "offer": [
4329 {
4330 "storage": "cache",
4331 "from": "#storage_provider",
4332 "to": [ "#echo_server" ],
4333 },
4334 ],
4335 "children": [
4336 {
4337 "name": "echo_server",
4338 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
4339 },
4340 {
4341 "name": "storage_provider",
4342 "url": "fuchsia-pkg://fuchsia.com/storage_provider#meta/storage_provider.cm",
4343 },
4344 ],
4345 }),
4346 Err(Error::Validate { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
4347 ),
4348 test_cml_offer_bad_from(
4349 json!({
4350 "offer": [ {
4351 "protocol": "fuchsia.logger.Log",
4352 "from": "#invalid@",
4353 "to": [ "#echo_server" ],
4354 } ]
4355 }),
4356 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"#invalid@\", expected one or an array of \"parent\", \"framework\", \"self\", \"#<child-name>\", \"#<collection-name>\", or a dictionary path"
4357 ),
4358 test_cml_offer_invalid_multiple_from(
4359 json!({
4360 "offer": [
4361 {
4362 "protocol": "fuchsia.logger.Log",
4363 "from": [ "parent", "#logger" ],
4364 "to": [ "#echo_server" ],
4365 },
4366 ],
4367 "children": [
4368 {
4369 "name": "logger",
4370 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
4371 },
4372 {
4373 "name": "echo_server",
4374 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4375 },
4376 ]
4377 }),
4378 Err(Error::Validate { err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses"
4379 ),
4380 test_cml_offer_from_missing_named_source(
4381 json!({
4382 "offer": [
4383 {
4384 "protocol": "fuchsia.logger.Log",
4385 "from": "#does-not-exist",
4386 "to": ["#echo_server" ],
4387 },
4388 ],
4389 "children": [
4390 {
4391 "name": "echo_server",
4392 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4393 },
4394 ]
4395 }),
4396 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\""
4397 ),
4398 test_cml_offer_protocol_from_collection_invalid(
4399 json!({
4400 "collections": [ {
4401 "name": "coll",
4402 "durability": "transient",
4403 } ],
4404 "children": [ {
4405 "name": "echo_server",
4406 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4407 } ],
4408 "offer": [
4409 { "protocol": "fuchsia.logger.Log", "from": "#coll", "to": [ "#echo_server" ] },
4410 ]
4411 }),
4412 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\" or \"capabilities\""
4413 ),
4414 test_cml_offer_directory_from_collection_invalid(
4415 json!({
4416 "collections": [ {
4417 "name": "coll",
4418 "durability": "transient",
4419 } ],
4420 "children": [ {
4421 "name": "echo_server",
4422 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4423 } ],
4424 "offer": [
4425 { "directory": "temp", "from": "#coll", "to": [ "#echo_server" ] },
4426 ]
4427 }),
4428 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
4429 ),
4430 test_cml_offer_storage_from_collection_invalid(
4431 json!({
4432 "collections": [ {
4433 "name": "coll",
4434 "durability": "transient",
4435 } ],
4436 "children": [ {
4437 "name": "echo_server",
4438 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4439 } ],
4440 "offer": [
4441 { "storage": "cache", "from": "#coll", "to": [ "#echo_server" ] },
4442 ]
4443 }),
4444 Err(Error::Validate { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
4445 ),
4446 test_cml_offer_runner_from_collection_invalid(
4447 json!({
4448 "collections": [ {
4449 "name": "coll",
4450 "durability": "transient",
4451 } ],
4452 "children": [ {
4453 "name": "echo_server",
4454 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4455 } ],
4456 "offer": [
4457 { "runner": "elf", "from": "#coll", "to": [ "#echo_server" ] },
4458 ]
4459 }),
4460 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
4461 ),
4462 test_cml_offer_resolver_from_collection_invalid(
4463 json!({
4464 "collections": [ {
4465 "name": "coll",
4466 "durability": "transient",
4467 } ],
4468 "children": [ {
4469 "name": "echo_server",
4470 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4471 } ],
4472 "offer": [
4473 { "resolver": "base", "from": "#coll", "to": [ "#echo_server" ] },
4474 ]
4475 }),
4476 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
4477 ),
4478 test_cml_offer_from_dictionary_invalid(
4479 json!({
4480 "offer": [
4481 {
4482 "protocol": "pkg_protocol",
4483 "from": "bad/a",
4484 "to": "#child",
4485 },
4486 ],
4487 "children": [
4488 {
4489 "name": "child",
4490 "url": "fuchsia-pkg://child",
4491 },
4492 ],
4493 }),
4494 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad/a\", expected one or an array of \"parent\", \"framework\", \"self\", \"#<child-name>\", \"#<collection-name>\", or a dictionary path"
4495 ),
4496 test_cml_offer_to_non_dictionary(
4497 json!({
4498 "offer": [
4499 {
4500 "protocol": "p",
4501 "from": "parent",
4502 "to": "self/dict",
4503 },
4504 ],
4505 "capabilities": [
4506 {
4507 "protocol": "dict",
4508 },
4509 ],
4510 }),
4511 Err(Error::Validate { err, .. }) if &err == "\"offer\" has dictionary target \
4512 \"self/dict\" but \"dict\" is not a dictionary capability defined by \
4513 this component"
4514 ),
4515
4516 test_cml_offer_empty_targets(
4517 json!({
4518 "offer": [
4519 {
4520 "protocol": "fuchsia.logger.Log",
4521 "from": "#child",
4522 "to": []
4523 },
4524 ],
4525 "children": [
4526 {
4527 "name": "child",
4528 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
4529 },
4530 ],
4531 }),
4532 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected one or an array of \"#<child-name>\", \"#<collection-name>\", or \"self/<dictionary>\", with unique elements"
4533 ),
4534 test_cml_offer_duplicate_targets(
4535 json!({
4536 "offer": [ {
4537 "protocol": "fuchsia.logger.Log",
4538 "from": "#logger",
4539 "to": ["#a", "#a"]
4540 } ]
4541 }),
4542 Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected one or an array of \"#<child-name>\", \"#<collection-name>\", or \"self/<dictionary>\", with unique elements"
4543 ),
4544 test_cml_offer_target_missing_props(
4545 json!({
4546 "offer": [ {
4547 "protocol": "fuchsia.logger.Log",
4548 "from": "#logger",
4549 "as": "fuchsia.logger.SysLog",
4550 } ]
4551 }),
4552 Err(Error::Parse { err, .. }) if &err == "missing field `to`"
4553 ),
4554 test_cml_offer_target_missing_to(
4555 json!({
4556 "offer": [ {
4557 "protocol": "fuchsia.logger.Log",
4558 "from": "#logger",
4559 "to": [ "#missing" ],
4560 } ],
4561 "children": [ {
4562 "name": "logger",
4563 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
4564 } ]
4565 }),
4566 Err(Error::Validate { err, .. }) if &err == "\"#missing\" is an \"offer\" target from \"#logger\" but it does not appear in \"children\" or \"collections\""
4567 ),
4568 test_cml_offer_target_bad_to(
4569 json!({
4570 "offer": [ {
4571 "protocol": "fuchsia.logger.Log",
4572 "from": "#logger",
4573 "to": [ "self" ],
4574 "as": "fuchsia.logger.SysLog",
4575 } ]
4576 }),
4577 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"self\", expected \"#<child-name>\", \"#<collection-name>\", or \"self/<dictionary>\""
4578 ),
4579 test_cml_offer_empty_protocols(
4580 json!({
4581 "offer": [
4582 {
4583 "protocol": [],
4584 "from": "parent",
4585 "to": [ "#echo_server" ],
4586 "as": "thing"
4587 },
4588 ],
4589 }),
4590 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
4591 ),
4592 test_cml_offer_target_equals_from(
4593 json!({
4594 "children": [
4595 {
4596 "name": "child",
4597 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
4598 },
4599 ],
4600 "offer": [
4601 {
4602 "protocol": "fuchsia.example.Protocol",
4603 "from": "#child",
4604 "to": [ "#child" ],
4605 },
4606 ],
4607 }),
4608 Err(Error::Validate { err, .. }) if &err == "Offer target \"#child\" is same as source"
4609 ),
4610 test_cml_offer_target_equals_from_weak(
4611 json!({
4612 "children": [
4613 {
4614 "name": "child",
4615 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
4616 },
4617 ],
4618 "offer": [
4619 {
4620 "protocol": "fuchsia.example.Protocol",
4621 "from": "#child",
4622 "to": [ "#child" ],
4623 "dependency": "weak",
4624 },
4625 {
4626 "directory": "data",
4627 "from": "#child",
4628 "to": [ "#child" ],
4629 "dependency": "weak",
4630 },
4631 ],
4632 }),
4633 Ok(())
4634 ),
4635 test_cml_storage_offer_target_equals_from(
4636 json!({
4637 "offer": [ {
4638 "storage": "minfs",
4639 "from": "self",
4640 "to": [ "#logger" ],
4641 } ],
4642 "children": [ {
4643 "name": "logger",
4644 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
4645 } ],
4646 "capabilities": [ {
4647 "storage": "minfs",
4648 "from": "#logger",
4649 "backing_dir": "minfs-dir",
4650 "storage_id": "static_instance_id_or_moniker",
4651 } ],
4652 }),
4653 Err(Error::Validate { err, .. }) if &err == "Storage offer target \"#logger\" is same as source"
4654 ),
4655 test_cml_offer_duplicate_target_names(
4656 json!({
4657 "offer": [
4658 {
4659 "protocol": "logger",
4660 "from": "parent",
4661 "to": [ "#echo_server" ],
4662 "as": "thing"
4663 },
4664 {
4665 "protocol": "logger",
4666 "from": "parent",
4667 "to": [ "#scenic" ],
4668 },
4669 {
4670 "directory": "thing",
4671 "from": "parent",
4672 "to": [ "#echo_server" ],
4673 }
4674 ],
4675 "children": [
4676 {
4677 "name": "scenic",
4678 "url": "fuchsia-pkg://fuchsia.com/scenic/stable#meta/scenic.cm"
4679 },
4680 {
4681 "name": "echo_server",
4682 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
4683 },
4684 ],
4685 }),
4686 Err(Error::Validate { err, .. }) if &err == "\"thing\" is a duplicate \"offer\" target capability for \"#echo_server\""
4687 ),
4688 test_cml_offer_duplicate_storage_names(
4689 json!({
4690 "offer": [
4691 {
4692 "storage": "cache",
4693 "from": "parent",
4694 "to": [ "#echo_server" ]
4695 },
4696 {
4697 "storage": "cache",
4698 "from": "self",
4699 "to": [ "#echo_server" ]
4700 }
4701 ],
4702 "capabilities": [ {
4703 "storage": "cache",
4704 "from": "self",
4705 "backing_dir": "minfs",
4706 "storage_id": "static_instance_id_or_moniker",
4707 } ],
4708 "children": [ {
4709 "name": "echo_server",
4710 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
4711 } ]
4712 }),
4713 Err(Error::Validate { err, .. }) if &err == "\"cache\" is a duplicate \"offer\" target capability for \"#echo_server\""
4714 ),
4715 test_cml_offer_bad_as(
4717 json!({
4718 "offer": [
4719 {
4720 "protocol": ["A", "B"],
4721 "from": "parent",
4722 "to": [ "#echo_server" ],
4723 "as": "thing"
4724 },
4725 ],
4726 "children": [
4727 {
4728 "name": "echo_server",
4729 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
4730 }
4731 ]
4732 }),
4733 Err(Error::Validate { err, .. }) if &err == "\"as\" can only be specified when one `protocol` is supplied."
4734 ),
4735 test_cml_offer_bad_subdir(
4736 json!({
4737 "offer": [
4738 {
4739 "directory": "index",
4740 "subdir": "/",
4741 "from": "parent",
4742 "to": [ "#modular" ],
4743 },
4744 ],
4745 "children": [
4746 {
4747 "name": "modular",
4748 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4749 }
4750 ]
4751 }),
4752 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
4753 ),
4754 test_cml_offer_from_self(
4755 json!({
4756 "offer": [
4757 {
4758 "protocol": "foo_protocol",
4759 "from": "self",
4760 "to": [ "#modular" ],
4761 },
4762 {
4763 "protocol": [ "bar_protocol", "baz_protocol" ],
4764 "from": "self",
4765 "to": [ "#modular" ],
4766 },
4767 {
4768 "directory": "foo_directory",
4769 "from": "self",
4770 "to": [ "#modular" ],
4771 },
4772 {
4773 "runner": "foo_runner",
4774 "from": "self",
4775 "to": [ "#modular" ],
4776 },
4777 {
4778 "resolver": "foo_resolver",
4779 "from": "self",
4780 "to": [ "#modular" ],
4781 },
4782 ],
4783 "children": [
4784 {
4785 "name": "modular",
4786 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4787 },
4788 ],
4789 "capabilities": [
4790 {
4791 "protocol": "foo_protocol",
4792 },
4793 {
4794 "protocol": "bar_protocol",
4795 },
4796 {
4797 "protocol": "baz_protocol",
4798 },
4799 {
4800 "directory": "foo_directory",
4801 "path": "/dir",
4802 "rights": [ "r*" ],
4803 },
4804 {
4805 "runner": "foo_runner",
4806 "path": "/svc/fuchsia.sys2.ComponentRunner",
4807 },
4808 {
4809 "resolver": "foo_resolver",
4810 "path": "/svc/fuchsia.component.resolution.Resolver",
4811 },
4812 ]
4813 }),
4814 Ok(())
4815 ),
4816 test_cml_offer_service_from_self_missing(
4817 json!({
4818 "offer": [
4819 {
4820 "service": "pkg_service",
4821 "from": "self",
4822 "to": [ "#modular" ],
4823 },
4824 ],
4825 "children": [
4826 {
4827 "name": "modular",
4828 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4829 },
4830 ],
4831 }),
4832 Err(Error::Validate { err, .. }) if &err == "service \"pkg_service\" is offered from self, so it must be declared as a \"service\" in \"capabilities\""
4833 ),
4834 test_cml_offer_protocol_from_self_missing(
4835 json!({
4836 "offer": [
4837 {
4838 "protocol": "pkg_protocol",
4839 "from": "self",
4840 "to": [ "#modular" ],
4841 },
4842 ],
4843 "children": [
4844 {
4845 "name": "modular",
4846 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4847 },
4848 ],
4849 }),
4850 Err(Error::Validate { err, .. }) if &err == "protocol \"pkg_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
4851 ),
4852 test_cml_offer_protocol_from_self_missing_multiple(
4853 json!({
4854 "offer": [
4855 {
4856 "protocol": [ "foo_protocol", "bar_protocol" ],
4857 "from": "self",
4858 "to": [ "#modular" ],
4859 },
4860 ],
4861 "children": [
4862 {
4863 "name": "modular",
4864 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4865 },
4866 ],
4867 }),
4868 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
4869 ),
4870 test_cml_offer_directory_from_self_missing(
4871 json!({
4872 "offer": [
4873 {
4874 "directory": "pkg_directory",
4875 "from": "self",
4876 "to": [ "#modular" ],
4877 },
4878 ],
4879 "children": [
4880 {
4881 "name": "modular",
4882 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4883 },
4884 ],
4885 }),
4886 Err(Error::Validate { err, .. }) if &err == "directory \"pkg_directory\" is offered from self, so it must be declared as a \"directory\" in \"capabilities\""
4887 ),
4888 test_cml_offer_runner_from_self_missing(
4889 json!({
4890 "offer": [
4891 {
4892 "runner": "dart",
4893 "from": "self",
4894 "to": [ "#modular" ],
4895 },
4896 ],
4897 "children": [
4898 {
4899 "name": "modular",
4900 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4901 },
4902 ],
4903 }),
4904 Err(Error::Validate { err, .. }) if &err == "runner \"dart\" is offered from self, so it must be declared as a \"runner\" in \"capabilities\""
4905 ),
4906 test_cml_offer_resolver_from_self_missing(
4907 json!({
4908 "offer": [
4909 {
4910 "resolver": "pkg_resolver",
4911 "from": "self",
4912 "to": [ "#modular" ],
4913 },
4914 ],
4915 "children": [
4916 {
4917 "name": "modular",
4918 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4919 },
4920 ],
4921 }),
4922 Err(Error::Validate { err, .. }) if &err == "resolver \"pkg_resolver\" is offered from self, so it must be declared as a \"resolver\" in \"capabilities\""
4923 ),
4924 test_cml_offer_storage_from_self_missing(
4925 json!({
4926 "offer": [
4927 {
4928 "storage": "cache",
4929 "from": "self",
4930 "to": [ "#echo_server" ],
4931 },
4932 ],
4933 "children": [
4934 {
4935 "name": "echo_server",
4936 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
4937 },
4938 ],
4939 }),
4940 Err(Error::Validate { err, .. }) if &err == "storage \"cache\" is offered from self, so it must be declared as a \"storage\" in \"capabilities\""
4941 ),
4942 test_cml_offer_from_self_missing_dictionary(
4943 json!({
4944 "offer": [
4945 {
4946 "protocol": "foo_protocol",
4947 "from": "self/dict/inner",
4948 "to": [ "#modular" ],
4949 },
4950 ],
4951 "children": [
4952 {
4953 "name": "modular",
4954 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
4955 },
4956 ],
4957 }),
4958 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is offered from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
4959 ),
4960 test_cml_offer_dependency_on_wrong_type(
4961 json!({
4962 "offer": [ {
4963 "resolver": "fuchsia.logger.Log",
4964 "from": "parent",
4965 "to": [ "#echo_server" ],
4966 "dependency": "strong",
4967 } ],
4968 "children": [ {
4969 "name": "echo_server",
4970 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
4971 } ],
4972 }),
4973 Err(Error::Validate { err, .. }) if err.starts_with("Dependency can only be provided for")
4974 ),
4975 test_cml_offer_dependency_cycle(
4976 json!({
4977 "offer": [
4978 {
4979 "protocol": "fuchsia.logger.Log",
4980 "from": "#a",
4981 "to": [ "#b" ],
4982 "dependency": "strong"
4983 },
4984 {
4985 "directory": "data",
4986 "from": "#b",
4987 "to": [ "#c" ],
4988 },
4989 {
4990 "protocol": "ethernet",
4991 "from": "#c",
4992 "to": [ "#a" ],
4993 },
4994 {
4995 "runner": "elf",
4996 "from": "#b",
4997 "to": [ "#d" ],
4998 },
4999 {
5000 "resolver": "http",
5001 "from": "#d",
5002 "to": [ "#b" ],
5003 },
5004 ],
5005 "children": [
5006 {
5007 "name": "a",
5008 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm"
5009 },
5010 {
5011 "name": "b",
5012 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm"
5013 },
5014 {
5015 "name": "c",
5016 "url": "fuchsia-pkg://fuchsia.com/b#meta/c.cm"
5017 },
5018 {
5019 "name": "d",
5020 "url": "fuchsia-pkg://fuchsia.com/b#meta/d.cm"
5021 },
5022 ]
5023 }),
5024 Err(Error::Validate {
5025 err,
5026 ..
5027 }) if &err ==
5028 "Strong dependency cycles were found. Break the cycle by removing a \
5029 dependency or marking an offer as weak. Cycles: \
5030 {{#a -> #b -> #c -> #a}, {#b -> #d -> #b}}"
5031 ),
5032 test_cml_offer_dependency_cycle_storage(
5033 json!({
5034 "capabilities": [
5035 {
5036 "storage": "data",
5037 "from": "#backend",
5038 "backing_dir": "blobfs",
5039 "storage_id": "static_instance_id_or_moniker",
5040 },
5041 ],
5042 "children": [
5043 {
5044 "name": "child",
5045 "url": "#meta/child.cm",
5046 },
5047 {
5048 "name": "backend",
5049 "url": "#meta/backend.cm",
5050 },
5051 ],
5052 "offer": [
5053 {
5054 "protocol": "fuchsia.example.Protocol",
5055 "from": "#child",
5056 "to": "#backend",
5057 },
5058 {
5059 "storage": "data",
5060 "from": "self",
5061 "to": "#child",
5062 },
5063 ],
5064 }),
5065 Err(Error::Validate {
5066 err,
5067 ..
5068 }) if &err ==
5069 "Strong dependency cycles were found. Break the cycle by removing a \
5070 dependency or marking an offer as weak. Cycles: \
5071 {{#backend -> #data -> #child -> #backend}}"
5072 ),
5073 test_cml_offer_weak_dependency_cycle(
5074 json!({
5075 "offer": [
5076 {
5077 "protocol": "fuchsia.logger.Log",
5078 "from": "#child_a",
5079 "to": [ "#child_b" ],
5080 "dependency": "weak"
5081 },
5082 {
5083 "service": "fuchsia.service.Test",
5084 "from": "#child_a",
5085 "to": [ "#child_b" ],
5086 "dependency": "weak"
5087 },
5088 {
5089 "directory": "data",
5090 "from": "#child_b",
5091 "to": [ "#child_a" ],
5092 },
5093 ],
5094 "children": [
5095 {
5096 "name": "child_a",
5097 "url": "fuchsia-pkg://fuchsia.com/child_a#meta/child_a.cm"
5098 },
5099 {
5100 "name": "child_b",
5101 "url": "fuchsia-pkg://fuchsia.com/child_b#meta/child_b.cm"
5102 },
5103 ]
5104 }),
5105 Ok(())
5106 ),
5107
5108 test_cml_children(
5110 json!({
5111 "children": [
5112 {
5113 "name": "logger",
5114 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5115 "on_terminate": "reboot",
5116 },
5117 {
5118 "name": "gmail",
5119 "url": "https://www.google.com/gmail",
5120 "startup": "eager",
5121 },
5122 ]
5123 }),
5124 Ok(())
5125 ),
5126 test_cml_children_missing_props(
5127 json!({
5128 "children": [ {} ]
5129 }),
5130 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
5131 ),
5132 test_cml_children_duplicate_names(
5133 json!({
5134 "children": [
5135 {
5136 "name": "logger",
5137 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
5138 },
5139 {
5140 "name": "logger",
5141 "url": "fuchsia-pkg://fuchsia.com/logger/beta#meta/logger.cm"
5142 }
5143 ]
5144 }),
5145 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"children\" and once in \"children\""
5146 ),
5147 test_cml_children_url_ends_in_cml(
5148 json!({
5149 "children": [
5150 {
5151 "name": "logger",
5152 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cml"
5153 },
5154 ]
5155 }),
5156 Err(Error::Validate { err, ..}) if &err == "child URL ends in .cml instead of .cm, which is almost certainly a mistake: fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cml"
5157 ),
5158 test_cml_children_bad_startup(
5159 json!({
5160 "children": [
5161 {
5162 "name": "logger",
5163 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5164 "startup": "zzz",
5165 },
5166 ],
5167 }),
5168 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `lazy` or `eager`"
5169 ),
5170 test_cml_children_bad_on_terminate(
5171 json!({
5172 "children": [
5173 {
5174 "name": "logger",
5175 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5176 "on_terminate": "zzz",
5177 },
5178 ],
5179 }),
5180 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `none` or `reboot`"
5181 ),
5182
5183
5184 test_cml_children_bad_environment(
5185 json!({
5186 "children": [
5187 {
5188 "name": "logger",
5189 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5190 "environment": "parent",
5191 }
5192 ]
5193 }),
5194 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\""
5195 ),
5196 test_cml_children_unknown_environment(
5197 json!({
5198 "children": [
5199 {
5200 "name": "logger",
5201 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5202 "environment": "#foo_env",
5203 }
5204 ]
5205 }),
5206 Err(Error::Validate { err, .. }) if &err == "\"foo_env\" does not appear in \"environments\""
5207 ),
5208 test_cml_children_environment(
5209 json!({
5210 "children": [
5211 {
5212 "name": "logger",
5213 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5214 "environment": "#foo_env",
5215 }
5216 ],
5217 "environments": [
5218 {
5219 "name": "foo_env",
5220 }
5221 ]
5222 }),
5223 Ok(())
5224 ),
5225 test_cml_collections_bad_environment(
5226 json!({
5227 "collections": [
5228 {
5229 "name": "tests",
5230 "durability": "transient",
5231 "environment": "parent",
5232 }
5233 ]
5234 }),
5235 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\""
5236 ),
5237 test_cml_collections_unknown_environment(
5238 json!({
5239 "collections": [
5240 {
5241 "name": "tests",
5242 "durability": "transient",
5243 "environment": "#foo_env",
5244 }
5245 ]
5246 }),
5247 Err(Error::Validate { err, .. }) if &err == "\"foo_env\" does not appear in \"environments\""
5248 ),
5249 test_cml_collections_environment(
5250 json!({
5251 "collections": [
5252 {
5253 "name": "tests",
5254 "durability": "transient",
5255 "environment": "#foo_env",
5256 }
5257 ],
5258 "environments": [
5259 {
5260 "name": "foo_env",
5261 }
5262 ]
5263 }),
5264 Ok(())
5265 ),
5266
5267 test_cml_environment_timeout(
5268 json!({
5269 "environments": [
5270 {
5271 "name": "foo_env",
5272 "__stop_timeout_ms": 10000,
5273 }
5274 ]
5275 }),
5276 Ok(())
5277 ),
5278
5279 test_cml_environment_bad_timeout(
5280 json!({
5281 "environments": [
5282 {
5283 "name": "foo_env",
5284 "__stop_timeout_ms": -3,
5285 }
5286 ]
5287 }),
5288 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `-3`, expected an unsigned 32-bit integer"
5289 ),
5290 test_cml_environment_debug(
5291 json!({
5292 "capabilities": [
5293 {
5294 "protocol": "fuchsia.logger.Log2",
5295 },
5296 ],
5297 "environments": [
5298 {
5299 "name": "foo_env",
5300 "extends": "realm",
5301 "debug": [
5302 {
5303 "protocol": "fuchsia.module.Module",
5304 "from": "#modular",
5305 },
5306 {
5307 "protocol": "fuchsia.logger.OtherLog",
5308 "from": "parent",
5309 },
5310 {
5311 "protocol": "fuchsia.logger.Log2",
5312 "from": "self",
5313 },
5314 ]
5315 }
5316 ],
5317 "children": [
5318 {
5319 "name": "modular",
5320 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5321 },
5322 ],
5323 }),
5324 Ok(())
5325 ),
5326 test_cml_environment_debug_missing_capability(
5327 json!({
5328 "environments": [
5329 {
5330 "name": "foo_env",
5331 "extends": "realm",
5332 "debug": [
5333 {
5334 "protocol": "fuchsia.module.Module",
5335 "from": "#modular",
5336 },
5337 {
5338 "protocol": "fuchsia.logger.OtherLog",
5339 "from": "parent",
5340 },
5341 {
5342 "protocol": "fuchsia.logger.Log2",
5343 "from": "self",
5344 },
5345 ]
5346 }
5347 ],
5348 "children": [
5349 {
5350 "name": "modular",
5351 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5352 },
5353 ],
5354 }),
5355 Err(Error::Validate { err, .. }) if &err == "protocol \"fuchsia.logger.Log2\" is registered as debug from self, so it must be declared as a \"protocol\" in \"capabilities\""
5356 ),
5357 test_cml_environment_invalid_from_child(
5358 json!({
5359 "capabilities": [
5360 {
5361 "protocol": "fuchsia.logger.Log2",
5362 },
5363 ],
5364 "environments": [
5365 {
5366 "name": "foo_env",
5367 "extends": "realm",
5368 "debug": [
5369 {
5370 "protocol": "fuchsia.module.Module",
5371 "from": "#missing",
5372 },
5373 {
5374 "protocol": "fuchsia.logger.OtherLog",
5375 "from": "parent",
5376 },
5377 {
5378 "protocol": "fuchsia.logger.Log2",
5379 "from": "self",
5380 },
5381 ]
5382 }
5383 ],
5384 "children": [
5385 {
5386 "name": "modular",
5387 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5388 },
5389 ],
5390 }),
5391 Err(Error::Validate { err, .. }) if &err == "\"debug\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
5392 ),
5393
5394
5395 test_cml_collections(
5397 json!({
5398 "collections": [
5399 {
5400 "name": "test_single_run_coll",
5401 "durability": "single_run"
5402 },
5403 {
5404 "name": "test_transient_coll",
5405 "durability": "transient"
5406 },
5407 ]
5408 }),
5409 Ok(())
5410 ),
5411 test_cml_collections_missing_props(
5412 json!({
5413 "collections": [ {} ]
5414 }),
5415 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
5416 ),
5417 test_cml_collections_duplicate_names(
5418 json!({
5419 "collections": [
5420 {
5421 "name": "duplicate",
5422 "durability": "single_run"
5423 },
5424 {
5425 "name": "duplicate",
5426 "durability": "transient"
5427 }
5428 ]
5429 }),
5430 Err(Error::Validate { err, .. }) if &err == "identifier \"duplicate\" is defined twice, once in \"collections\" and once in \"collections\""
5431 ),
5432 test_cml_collections_bad_durability(
5433 json!({
5434 "collections": [
5435 {
5436 "name": "modular",
5437 "durability": "zzz",
5438 },
5439 ],
5440 }),
5441 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `transient` or `single_run`"
5442 ),
5443
5444 test_cml_protocol(
5446 json!({
5447 "capabilities": [
5448 {
5449 "protocol": "a",
5450 "path": "/minfs",
5451 },
5452 {
5453 "protocol": "b",
5454 "path": "/data",
5455 },
5456 {
5457 "protocol": "c",
5458 },
5459 ],
5460 }),
5461 Ok(())
5462 ),
5463 test_cml_protocol_multi(
5464 json!({
5465 "capabilities": [
5466 {
5467 "protocol": ["a", "b", "c"],
5468 },
5469 ],
5470 }),
5471 Ok(())
5472 ),
5473 test_cml_protocol_multi_invalid_path(
5474 json!({
5475 "capabilities": [
5476 {
5477 "protocol": ["a", "b", "c"],
5478 "path": "/minfs",
5479 },
5480 ],
5481 }),
5482 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."
5483 ),
5484 test_cml_protocol_all_valid_chars(
5485 json!({
5486 "capabilities": [
5487 {
5488 "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
5489 },
5490 ],
5491 }),
5492 Ok(())
5493 ),
5494 test_cml_directory(
5495 json!({
5496 "capabilities": [
5497 {
5498 "directory": "a",
5499 "path": "/minfs",
5500 "rights": ["connect"],
5501 },
5502 {
5503 "directory": "b",
5504 "path": "/data",
5505 "rights": ["connect"],
5506 },
5507 ],
5508 }),
5509 Ok(())
5510 ),
5511 test_cml_directory_all_valid_chars(
5512 json!({
5513 "capabilities": [
5514 {
5515 "directory": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
5516 "path": "/data",
5517 "rights": ["connect"],
5518 },
5519 ],
5520 }),
5521 Ok(())
5522 ),
5523 test_cml_directory_missing_path(
5524 json!({
5525 "capabilities": [
5526 {
5527 "directory": "dir",
5528 "rights": ["connect"],
5529 },
5530 ]
5531 }),
5532 Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"directory\""
5533 ),
5534 test_cml_directory_missing_rights(
5535 json!({
5536 "capabilities": [
5537 {
5538 "directory": "dir",
5539 "path": "/dir",
5540 },
5541 ]
5542 }),
5543 Err(Error::Validate { err, .. }) if &err == "\"rights\" should be present with \"directory\""
5544 ),
5545 test_cml_storage(
5546 json!({
5547 "capabilities": [
5548 {
5549 "storage": "a",
5550 "from": "#minfs",
5551 "backing_dir": "minfs",
5552 "storage_id": "static_instance_id",
5553 },
5554 {
5555 "storage": "b",
5556 "from": "parent",
5557 "backing_dir": "data",
5558 "storage_id": "static_instance_id_or_moniker",
5559 },
5560 {
5561 "storage": "c",
5562 "from": "self",
5563 "backing_dir": "storage",
5564 "storage_id": "static_instance_id_or_moniker",
5565 },
5566 ],
5567 "children": [
5568 {
5569 "name": "minfs",
5570 "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm",
5571 },
5572 ],
5573 }),
5574 Ok(())
5575 ),
5576 test_cml_storage_all_valid_chars(
5577 json!({
5578 "capabilities": [
5579 {
5580 "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage",
5581 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
5582 "backing_dir": "example",
5583 "storage_id": "static_instance_id_or_moniker",
5584 },
5585 ],
5586 "children": [
5587 {
5588 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
5589 "url": "https://www.google.com/gmail",
5590 },
5591 ],
5592 }),
5593 Ok(())
5594 ),
5595 test_cml_storage_invalid_from(
5596 json!({
5597 "capabilities": [ {
5598 "storage": "minfs",
5599 "from": "#missing",
5600 "backing_dir": "minfs",
5601 "storage_id": "static_instance_id_or_moniker",
5602 } ]
5603 }),
5604 Err(Error::Validate { err, .. }) if &err == "\"capabilities\" source \"#missing\" does not appear in \"children\""
5605 ),
5606 test_cml_storage_missing_path_or_backing_dir(
5607 json!({
5608 "capabilities": [ {
5609 "storage": "minfs",
5610 "from": "self",
5611 "storage_id": "static_instance_id_or_moniker",
5612 } ]
5613 }),
5614 Err(Error::Validate { err, .. }) if &err == "\"backing_dir\" should be present with \"storage\""
5615
5616 ),
5617 test_cml_storage_missing_storage_id(
5618 json!({
5619 "capabilities": [ {
5620 "storage": "minfs",
5621 "from": "self",
5622 "backing_dir": "storage",
5623 }, ]
5624 }),
5625 Err(Error::Validate { err, .. }) if &err == "\"storage_id\" should be present with \"storage\""
5626 ),
5627 test_cml_storage_path(
5628 json!({
5629 "capabilities": [ {
5630 "storage": "minfs",
5631 "from": "self",
5632 "path": "/minfs",
5633 "storage_id": "static_instance_id_or_moniker",
5634 } ]
5635 }),
5636 Err(Error::Validate { err, .. }) if &err == "\"path\" can not be present with \"storage\", use \"backing_dir\""
5637 ),
5638 test_cml_runner(
5639 json!({
5640 "capabilities": [
5641 {
5642 "runner": "a",
5643 "path": "/minfs",
5644 },
5645 ],
5646 }),
5647 Ok(())
5648 ),
5649 test_cml_runner_all_valid_chars(
5650 json!({
5651 "children": [
5652 {
5653 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
5654 "url": "https://www.google.com/gmail"
5655 },
5656 ],
5657 "capabilities": [
5658 {
5659 "runner": "abcdefghijklmnopqrstuvwxyz0123456789_-runner",
5660 "path": "/example",
5661 },
5662 ]
5663 }),
5664 Ok(())
5665 ),
5666 test_cml_runner_extraneous_from(
5667 json!({
5668 "capabilities": [
5669 {
5670 "runner": "a",
5671 "path": "/example",
5672 "from": "self",
5673 },
5674 ]
5675 }),
5676 Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"runner\""
5677 ),
5678 test_cml_capability_missing_name(
5679 json!({
5680 "capabilities": [
5681 {
5682 "path": "/svc/fuchsia.component.resolution.Resolver",
5683 },
5684 ]
5685 }),
5686 Err(Error::Validate { err, .. }) if &err == "`capability` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"runner\", \"resolver\", \"event_stream\", \"dictionary\", \"config\""
5687 ),
5688 test_cml_resolver_missing_path(
5689 json!({
5690 "capabilities": [
5691 {
5692 "resolver": "pkg_resolver",
5693 },
5694 ]
5695 }),
5696 Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"resolver\""
5697 ),
5698 test_cml_capabilities_extraneous_from(
5699 json!({
5700 "capabilities": [
5701 {
5702 "resolver": "pkg_resolver",
5703 "path": "/svc/fuchsia.component.resolution.Resolver",
5704 "from": "self",
5705 },
5706 ]
5707 }),
5708 Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"resolver\""
5709 ),
5710 test_cml_capabilities_duplicates(
5711 json!({
5712 "capabilities": [
5713 {
5714 "runner": "pkg_resolver",
5715 "path": "/svc/fuchsia.component.resolution.Resolver",
5716 },
5717 {
5718 "resolver": "pkg_resolver",
5719 "path": "/svc/my-resolver",
5720 },
5721 ]
5722 }),
5723 Err(Error::Validate { err, .. }) if &err == "identifier \"pkg_resolver\" is defined twice, once in \"resolvers\" and once in \"runners\""
5724 ),
5725
5726 test_cml_environments(
5728 json!({
5729 "environments": [
5730 {
5731 "name": "my_env_a",
5732 },
5733 {
5734 "name": "my_env_b",
5735 "extends": "realm",
5736 },
5737 {
5738 "name": "my_env_c",
5739 "extends": "none",
5740 "__stop_timeout_ms": 8000,
5741 },
5742 ],
5743 }),
5744 Ok(())
5745 ),
5746
5747 test_invalid_cml_environment_no_stop_timeout(
5748 json!({
5749 "environments": [
5750 {
5751 "name": "my_env",
5752 "extends": "none",
5753 },
5754 ],
5755 }),
5756 Err(Error::Validate { err, .. }) if &err ==
5757 "'__stop_timeout_ms' must be provided if the environment does not extend \
5758 another environment"
5759 ),
5760
5761 test_cml_environment_invalid_extends(
5762 json!({
5763 "environments": [
5764 {
5765 "name": "my_env",
5766 "extends": "some_made_up_string",
5767 },
5768 ],
5769 }),
5770 Err(Error::Parse { err, .. }) if &err == "unknown variant `some_made_up_string`, expected `realm` or `none`"
5771 ),
5772 test_cml_environment_missing_props(
5773 json!({
5774 "environments": [ {} ]
5775 }),
5776 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
5777 ),
5778
5779 test_cml_environment_with_runners(
5780 json!({
5781 "environments": [
5782 {
5783 "name": "my_env",
5784 "extends": "realm",
5785 "runners": [
5786 {
5787 "runner": "dart",
5788 "from": "parent",
5789 }
5790 ]
5791 }
5792 ],
5793 }),
5794 Ok(())
5795 ),
5796 test_cml_environment_with_runners_alias(
5797 json!({
5798 "environments": [
5799 {
5800 "name": "my_env",
5801 "extends": "realm",
5802 "runners": [
5803 {
5804 "runner": "dart",
5805 "from": "parent",
5806 "as": "my-dart",
5807 }
5808 ]
5809 }
5810 ],
5811 }),
5812 Ok(())
5813 ),
5814 test_cml_environment_with_runners_missing(
5815 json!({
5816 "environments": [
5817 {
5818 "name": "my_env",
5819 "extends": "realm",
5820 "runners": [
5821 {
5822 "runner": "dart",
5823 "from": "self",
5824 }
5825 ]
5826 }
5827 ],
5828 "capabilities": [
5829 {
5830 "runner": "dart",
5831 "path": "/svc/fuchsia.component.Runner",
5832 }
5833 ],
5834 }),
5835 Ok(())
5836 ),
5837 test_cml_environment_with_runners_bad_name(
5838 json!({
5839 "environments": [
5840 {
5841 "name": "my_env",
5842 "extends": "realm",
5843 "runners": [
5844 {
5845 "runner": "elf",
5846 "from": "parent",
5847 "as": "#elf",
5848 }
5849 ]
5850 }
5851 ],
5852 }),
5853 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"#elf\", expected a \
5854 name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]"
5855 ),
5856 test_cml_environment_with_runners_duplicate_name(
5857 json!({
5858 "environments": [
5859 {
5860 "name": "my_env",
5861 "extends": "realm",
5862 "runners": [
5863 {
5864 "runner": "dart",
5865 "from": "parent",
5866 },
5867 {
5868 "runner": "other-dart",
5869 "from": "parent",
5870 "as": "dart",
5871 }
5872 ]
5873 }
5874 ],
5875 }),
5876 Err(Error::Validate { err, .. }) if &err == "Duplicate runners registered under name \"dart\": \"other-dart\" and \"dart\"."
5877 ),
5878 test_cml_environment_with_runner_from_missing_child(
5879 json!({
5880 "environments": [
5881 {
5882 "name": "my_env",
5883 "extends": "realm",
5884 "runners": [
5885 {
5886 "runner": "elf",
5887 "from": "#missing_child",
5888 }
5889 ]
5890 }
5891 ]
5892 }),
5893 Err(Error::Validate { err, .. }) if &err == "\"elf\" runner source \"#missing_child\" does not appear in \"children\""
5894 ),
5895 test_cml_environment_with_runner_cycle(
5896 json!({
5897 "environments": [
5898 {
5899 "name": "my_env",
5900 "extends": "realm",
5901 "runners": [
5902 {
5903 "runner": "elf",
5904 "from": "#child",
5905 "as": "my-elf",
5906 }
5907 ]
5908 }
5909 ],
5910 "children": [
5911 {
5912 "name": "child",
5913 "url": "fuchsia-pkg://child",
5914 "environment": "#my_env",
5915 }
5916 ]
5917 }),
5918 Err(Error::Validate { err, .. }) if &err ==
5919 "Strong dependency cycles were found. Break the cycle by removing a \
5920 dependency or marking an offer as weak. Cycles: \
5921 {{#child -> #my_env -> #child}}"
5922 ),
5923 test_cml_environment_with_resolvers(
5924 json!({
5925 "environments": [
5926 {
5927 "name": "my_env",
5928 "extends": "realm",
5929 "resolvers": [
5930 {
5931 "resolver": "pkg_resolver",
5932 "from": "parent",
5933 "scheme": "fuchsia-pkg",
5934 }
5935 ]
5936 }
5937 ],
5938 }),
5939 Ok(())
5940 ),
5941 test_cml_environment_with_resolvers_bad_scheme(
5942 json!({
5943 "environments": [
5944 {
5945 "name": "my_env",
5946 "extends": "realm",
5947 "resolvers": [
5948 {
5949 "resolver": "pkg_resolver",
5950 "from": "parent",
5951 "scheme": "9scheme",
5952 }
5953 ]
5954 }
5955 ],
5956 }),
5957 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"9scheme\", expected a valid URL scheme"
5958 ),
5959 test_cml_environment_with_resolvers_duplicate_scheme(
5960 json!({
5961 "environments": [
5962 {
5963 "name": "my_env",
5964 "extends": "realm",
5965 "resolvers": [
5966 {
5967 "resolver": "pkg_resolver",
5968 "from": "parent",
5969 "scheme": "fuchsia-pkg",
5970 },
5971 {
5972 "resolver": "base_resolver",
5973 "from": "parent",
5974 "scheme": "fuchsia-pkg",
5975 }
5976 ]
5977 }
5978 ],
5979 }),
5980 Err(Error::Validate { err, .. }) if &err == "scheme \"fuchsia-pkg\" for resolver \"base_resolver\" is already registered; previously registered to resolver \"pkg_resolver\"."
5981 ),
5982 test_cml_environment_with_resolver_from_missing_child(
5983 json!({
5984 "environments": [
5985 {
5986 "name": "my_env",
5987 "extends": "realm",
5988 "resolvers": [
5989 {
5990 "resolver": "pkg_resolver",
5991 "from": "#missing_child",
5992 "scheme": "fuchsia-pkg",
5993 }
5994 ]
5995 }
5996 ]
5997 }),
5998 Err(Error::Validate { err, .. }) if &err == "\"pkg_resolver\" resolver source \"#missing_child\" does not appear in \"children\""
5999 ),
6000 test_cml_environment_with_resolver_cycle(
6001 json!({
6002 "environments": [
6003 {
6004 "name": "my_env",
6005 "extends": "realm",
6006 "resolvers": [
6007 {
6008 "resolver": "pkg_resolver",
6009 "from": "#child",
6010 "scheme": "fuchsia-pkg",
6011 }
6012 ]
6013 }
6014 ],
6015 "children": [
6016 {
6017 "name": "child",
6018 "url": "fuchsia-pkg://child",
6019 "environment": "#my_env",
6020 }
6021 ]
6022 }),
6023 Err(Error::Validate { err, .. }) if &err ==
6024 "Strong dependency cycles were found. Break the cycle by removing a \
6025 dependency or marking an offer as weak. \
6026 Cycles: {{#child -> #my_env -> #child}}"
6027 ),
6028 test_cml_environment_with_cycle_multiple_components(
6029 json!({
6030 "environments": [
6031 {
6032 "name": "my_env",
6033 "extends": "realm",
6034 "resolvers": [
6035 {
6036 "resolver": "pkg_resolver",
6037 "from": "#b",
6038 "scheme": "fuchsia-pkg",
6039 }
6040 ]
6041 }
6042 ],
6043 "children": [
6044 {
6045 "name": "a",
6046 "url": "fuchsia-pkg://a",
6047 "environment": "#my_env",
6048 },
6049 {
6050 "name": "b",
6051 "url": "fuchsia-pkg://b",
6052 }
6053 ],
6054 "offer": [
6055 {
6056 "protocol": "fuchsia.logger.Log",
6057 "from": "#a",
6058 "to": [ "#b" ],
6059 "dependency": "strong"
6060 },
6061 ]
6062 }),
6063 Err(Error::Validate { err, .. }) if &err ==
6064 "Strong dependency cycles were found. Break the cycle by removing a dependency \
6065 or marking an offer as weak. \
6066 Cycles: {{#a -> #b -> #my_env -> #a}}"
6067 ),
6068
6069 test_cml_facets(
6071 json!({
6072 "facets": {
6073 "metadata": {
6074 "title": "foo",
6075 "authors": [ "me", "you" ],
6076 "year": 2018
6077 }
6078 }
6079 }),
6080 Ok(())
6081 ),
6082 test_cml_facets_wrong_type(
6083 json!({
6084 "facets": 55
6085 }),
6086 Err(Error::Parse { err, .. }) if &err == "invalid type: integer `55`, expected a map"
6087 ),
6088
6089 test_cml_rights_all(
6091 json!({
6092 "use": [
6093 {
6094 "directory": "mydir",
6095 "path": "/mydir",
6096 "rights": ["connect", "enumerate", "read_bytes", "write_bytes",
6097 "execute", "update_attributes", "get_attributes", "traverse",
6098 "modify_directory"],
6099 },
6100 ]
6101 }),
6102 Ok(())
6103 ),
6104 test_cml_rights_invalid(
6105 json!({
6106 "use": [
6107 {
6108 "directory": "mydir",
6109 "path": "/mydir",
6110 "rights": ["cAnnect", "enumerate"],
6111 },
6112 ]
6113 }),
6114 Err(Error::Parse { err, .. }) if &err == "unknown variant `cAnnect`, expected one of `connect`, `enumerate`, `execute`, `get_attributes`, `modify_directory`, `read_bytes`, `traverse`, `update_attributes`, `write_bytes`, `r*`, `w*`, `x*`, `rw*`, `rx*`"
6115 ),
6116 test_cml_rights_duplicate(
6117 json!({
6118 "use": [
6119 {
6120 "directory": "mydir",
6121 "path": "/mydir",
6122 "rights": ["connect", "connect"],
6123 },
6124 ]
6125 }),
6126 Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a nonempty array of rights, with unique elements"
6127 ),
6128 test_cml_rights_empty(
6129 json!({
6130 "use": [
6131 {
6132 "directory": "mydir",
6133 "path": "/mydir",
6134 "rights": [],
6135 },
6136 ]
6137 }),
6138 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a nonempty array of rights, with unique elements"
6139 ),
6140 test_cml_rights_alias_star_expansion(
6141 json!({
6142 "use": [
6143 {
6144 "directory": "mydir",
6145 "rights": ["r*"],
6146 "path": "/mydir",
6147 },
6148 ]
6149 }),
6150 Ok(())
6151 ),
6152 test_cml_rights_alias_star_expansion_with_longform(
6153 json!({
6154 "use": [
6155 {
6156 "directory": "mydir",
6157 "rights": ["w*", "read_bytes"],
6158 "path": "/mydir",
6159 },
6160 ]
6161 }),
6162 Ok(())
6163 ),
6164 test_cml_rights_alias_star_expansion_with_longform_collision(
6165 json!({
6166 "use": [
6167 {
6168 "directory": "mydir",
6169 "path": "/mydir",
6170 "rights": ["r*", "read_bytes"],
6171 },
6172 ]
6173 }),
6174 Err(Error::Validate { err, .. }) if &err == "\"read_bytes\" is duplicated in the rights clause."
6175 ),
6176 test_cml_rights_alias_star_expansion_collision(
6177 json!({
6178 "use": [
6179 {
6180 "directory": "mydir",
6181 "path": "/mydir",
6182 "rights": ["w*", "x*"],
6183 },
6184 ]
6185 }),
6186 Err(Error::Validate { err, .. }) if &err == "\"x*\" is duplicated in the rights clause."
6187 ),
6188 test_cml_rights_use_invalid(
6189 json!({
6190 "use": [
6191 { "directory": "mydir", "path": "/mydir" },
6192 ]
6193 }),
6194 Err(Error::Validate { err, .. }) if &err == "This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer."
6195 ),
6196
6197 test_cml_path(
6198 json!({
6199 "capabilities": [
6200 {
6201 "protocol": "foo",
6202 "path": "/foo/in.-_/Bar",
6203 },
6204 ]
6205 }),
6206 Ok(())
6207 ),
6208 test_cml_path_invalid_empty(
6209 json!({
6210 "capabilities": [
6211 { "protocol": "foo", "path": "" },
6212 ]
6213 }),
6214 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH bytes in length"
6215 ),
6216 test_cml_path_invalid_root(
6217 json!({
6218 "capabilities": [
6219 { "protocol": "foo", "path": "/" },
6220 ]
6221 }),
6222 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with leading `/` and non-empty segments, where each segment is no more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., and cannot contain embedded NULs"
6223 ),
6224 test_cml_path_invalid_absolute_is_relative(
6225 json!({
6226 "capabilities": [
6227 { "protocol": "foo", "path": "foo/bar" },
6228 ]
6229 }),
6230 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"foo/bar\", expected a path with leading `/` and non-empty segments, where each segment is no more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., and cannot contain embedded NULs"
6231 ),
6232 test_cml_path_invalid_trailing(
6233 json!({
6234 "capabilities": [
6235 { "protocol": "foo", "path":"/foo/bar/" },
6236 ]
6237 }),
6238 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/foo/bar/\", expected a path with leading `/` and non-empty segments, where each segment is no more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., and cannot contain embedded NULs"
6239 ),
6240 test_cml_path_too_long(
6241 json!({
6242 "capabilities": [
6243 { "protocol": "foo", "path": format!("/{}", "a".repeat(4095)) },
6244 ]
6245 }),
6246 Err(Error::Parse { err, .. }) if &err == "invalid length 4096, expected a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH bytes in length"
6247 ),
6248 test_cml_path_invalid_segment(
6249 json!({
6250 "capabilities": [
6251 { "protocol": "foo", "path": "/foo/../bar" },
6252 ]
6253 }),
6254 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/foo/../bar\", expected a path with leading `/` and non-empty segments, where each segment is no more than fuchsia.io/MAX_NAME_LENGTH bytes in length, cannot be . or .., and cannot contain embedded NULs"
6255 ),
6256 test_cml_relative_path(
6257 json!({
6258 "use": [
6259 {
6260 "directory": "foo",
6261 "path": "/foo",
6262 "rights": ["r*"],
6263 "subdir": "Baz/Bar",
6264 },
6265 ]
6266 }),
6267 Ok(())
6268 ),
6269 test_cml_relative_path_invalid_empty(
6270 json!({
6271 "use": [
6272 {
6273 "directory": "foo",
6274 "path": "/foo",
6275 "rights": ["r*"],
6276 "subdir": "",
6277 },
6278 ]
6279 }),
6280 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH characters in length"
6281 ),
6282 test_cml_relative_path_invalid_root(
6283 json!({
6284 "use": [
6285 {
6286 "directory": "foo",
6287 "path": "/foo",
6288 "rights": ["r*"],
6289 "subdir": "/",
6290 },
6291 ]
6292 }),
6293 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
6294 ),
6295 test_cml_relative_path_invalid_absolute(
6296 json!({
6297 "use": [
6298 {
6299 "directory": "foo",
6300 "path": "/foo",
6301 "rights": ["r*"],
6302 "subdir": "/bar",
6303 },
6304 ]
6305 }),
6306 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bar\", expected a path with no leading `/` and non-empty segments"
6307 ),
6308 test_cml_relative_path_invalid_trailing(
6309 json!({
6310 "use": [
6311 {
6312 "directory": "foo",
6313 "path": "/foo",
6314 "rights": ["r*"],
6315 "subdir": "bar/",
6316 },
6317 ]
6318 }),
6319 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bar/\", expected a path with no leading `/` and non-empty segments"
6320 ),
6321 test_cml_relative_path_too_long(
6322 json!({
6323 "use": [
6324 {
6325 "directory": "foo",
6326 "path": "/foo",
6327 "rights": ["r*"],
6328 "subdir": format!("{}", "a".repeat(4096)),
6329 },
6330 ]
6331 }),
6332 Err(Error::Parse { err, .. }) if &err == "invalid length 4096, expected a non-empty path no more than fuchsia.io/MAX_PATH_LENGTH characters in length"
6333 ),
6334 test_cml_relative_ref_too_long(
6335 json!({
6336 "expose": [
6337 {
6338 "protocol": "fuchsia.logger.Log",
6339 "from": &format!("#{}", "a".repeat(256)),
6340 },
6341 ],
6342 "children": [
6343 {
6344 "name": "logger",
6345 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
6346 },
6347 ]
6348 }),
6349 Err(Error::Parse { err, .. }) if &err == "invalid length 257, expected one or an array of \"framework\", \"self\", \"#<child-name>\", or a dictionary path"
6350 ),
6351 test_cml_dictionary_ref_invalid_root(
6352 json!({
6353 "use": [
6354 {
6355 "protocol": "a",
6356 "from": "bad/a",
6357 },
6358 ],
6359 }),
6360 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad/a\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
6361 ),
6362 test_cml_dictionary_ref_invalid_path(
6363 json!({
6364 "use": [
6365 {
6366 "protocol": "a",
6367 "from": "parent//a",
6368 },
6369 ],
6370 }),
6371 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent//a\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
6372 ),
6373 test_cml_dictionary_ref_too_long(
6374 json!({
6375 "use": [
6376 {
6377 "protocol": "a",
6378 "from": format!("parent/{}", "a".repeat(4089)),
6379 },
6380 ],
6381 }),
6382 Err(Error::Parse { err, .. }) if &err == "invalid length 4096, expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
6383 ),
6384 test_cml_capability_name(
6385 json!({
6386 "use": [
6387 {
6388 "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
6389 },
6390 ]
6391 }),
6392 Ok(())
6393 ),
6394 test_cml_capability_name_invalid(
6395 json!({
6396 "use": [
6397 {
6398 "protocol": "/bad",
6399 },
6400 ]
6401 }),
6402 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a name or nonempty array of names, with unique elements"
6403 ),
6404 test_cml_child_name(
6405 json!({
6406 "children": [
6407 {
6408 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
6409 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
6410 },
6411 ]
6412 }),
6413 Ok(())
6414 ),
6415 test_cml_child_name_invalid(
6416 json!({
6417 "children": [
6418 {
6419 "name": "/bad",
6420 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
6421 },
6422 ]
6423 }),
6424 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a \
6425 name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]"
6426 ),
6427 test_cml_child_name_too_long(
6428 json!({
6429 "children": [
6430 {
6431 "name": "a".repeat(256),
6432 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
6433 }
6434 ]
6435 }),
6436 Err(Error::Parse { err, .. }) if &err == "invalid length 256, expected a non-empty name no more than 255 characters in length"
6437 ),
6438 test_cml_url(
6439 json!({
6440 "children": [
6441 {
6442 "name": "logger",
6443 "url": "my+awesome-scheme.2://abc123!@$%.com",
6444 },
6445 ]
6446 }),
6447 Ok(())
6448 ),
6449 test_cml_url_host_pound_invalid(
6450 json!({
6451 "children": [
6452 {
6453 "name": "logger",
6454 "url": "my+awesome-scheme.2://abc123!@#$%.com",
6455 },
6456 ]
6457 }),
6458 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"my+awesome-scheme.2://abc123!@#$%.com\", expected a valid URL"
6459 ),
6460 test_cml_url_invalid(
6461 json!({
6462 "children": [
6463 {
6464 "name": "logger",
6465 "url": "fuchsia-pkg",
6466 },
6467 ]
6468 }),
6469 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"fuchsia-pkg\", expected a valid URL"
6470 ),
6471 test_cml_url_too_long(
6472 json!({
6473 "children": [
6474 {
6475 "name": "logger",
6476 "url": &format!("fuchsia-pkg://{}", "a".repeat(4083)),
6477 },
6478 ]
6479 }),
6480 Err(Error::Parse { err, .. }) if &err == "invalid length 4097, expected a non-empty URL no more than 4096 characters in length"
6481 ),
6482 test_cml_duplicate_identifiers_children_collection(
6483 json!({
6484 "children": [
6485 {
6486 "name": "logger",
6487 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6488 }
6489 ],
6490 "collections": [
6491 {
6492 "name": "logger",
6493 "durability": "transient"
6494 }
6495 ]
6496 }),
6497 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"collections\" and once in \"children\""
6498 ),
6499 test_cml_duplicate_identifiers_children_storage(
6500 json!({
6501 "children": [
6502 {
6503 "name": "logger",
6504 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6505 }
6506 ],
6507 "capabilities": [
6508 {
6509 "storage": "logger",
6510 "path": "/logs",
6511 "from": "parent"
6512 }
6513 ]
6514 }),
6515 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"children\""
6516 ),
6517 test_cml_duplicate_identifiers_collection_storage(
6518 json!({
6519 "collections": [
6520 {
6521 "name": "logger",
6522 "durability": "transient"
6523 }
6524 ],
6525 "capabilities": [
6526 {
6527 "storage": "logger",
6528 "path": "/logs",
6529 "from": "parent"
6530 }
6531 ]
6532 }),
6533 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"collections\""
6534 ),
6535 test_cml_duplicate_identifiers_children_runners(
6536 json!({
6537 "children": [
6538 {
6539 "name": "logger",
6540 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6541 }
6542 ],
6543 "capabilities": [
6544 {
6545 "runner": "logger",
6546 "from": "parent"
6547 }
6548 ]
6549 }),
6550 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"runners\" and once in \"children\""
6551 ),
6552 test_cml_duplicate_identifiers_environments(
6553 json!({
6554 "children": [
6555 {
6556 "name": "logger",
6557 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6558 }
6559 ],
6560 "environments": [
6561 {
6562 "name": "logger",
6563 }
6564 ]
6565 }),
6566 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"environments\" and once in \"children\""
6567 ),
6568
6569 test_deny_unknown_fields(
6571 json!(
6572 {
6573 "program": {
6574 "runner": "elf",
6575 "binary": "bin/app",
6576 },
6577 "unknown_field": {},
6578 }
6579 ),
6580 Err(Error::Parse { err, .. }) if err.starts_with("unknown field `unknown_field`, expected one of ")
6581 ),
6582 test_offer_source_availability_unknown(
6583 json!({
6584 "children": [
6585 {
6586 "name": "foo",
6587 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6588 },
6589 ],
6590 "offer": [
6591 {
6592 "protocol": "fuchsia.examples.Echo",
6593 "from": "#bar",
6594 "to": "#foo",
6595 "availability": "optional",
6596 "source_availability": "unknown",
6597 },
6598 ],
6599 }),
6600 Ok(())
6601 ),
6602 test_offer_source_availability_required(
6603 json!({
6604 "children": [
6605 {
6606 "name": "foo",
6607 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6608 },
6609 ],
6610 "offer": [
6611 {
6612 "protocol": "fuchsia.examples.Echo",
6613 "from": "#bar",
6614 "to": "#foo",
6615 "source_availability": "required",
6616 },
6617 ],
6618 }),
6619 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
6620 ),
6621 test_offer_source_availability_omitted(
6622 json!({
6623 "children": [
6624 {
6625 "name": "foo",
6626 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6627 },
6628 ],
6629 "offer": [
6630 {
6631 "protocol": "fuchsia.examples.Echo",
6632 "from": "#bar",
6633 "to": "#foo",
6634 },
6635 ],
6636 }),
6637 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
6638 ),
6639 test_cml_use_invalid_availability(
6640 json!({
6641 "use": [
6642 {
6643 "protocol": "fuchsia.examples.Echo",
6644 "availability": "same_as_target",
6645 },
6646 ],
6647 }),
6648 Err(Error::Validate { err, .. }) if &err == "\"availability: same_as_target\" cannot be used with use declarations"
6649 ),
6650 test_offer_source_void_availability_required(
6651 json!({
6652 "children": [
6653 {
6654 "name": "foo",
6655 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6656 },
6657 ],
6658 "offer": [
6659 {
6660 "protocol": "fuchsia.examples.Echo",
6661 "from": "void",
6662 "to": "#foo",
6663 "availability": "required",
6664 },
6665 ],
6666 }),
6667 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
6668 ),
6669 test_offer_source_void_availability_same_as_target(
6670 json!({
6671 "children": [
6672 {
6673 "name": "foo",
6674 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6675 },
6676 ],
6677 "offer": [
6678 {
6679 "protocol": "fuchsia.examples.Echo",
6680 "from": "void",
6681 "to": "#foo",
6682 "availability": "same_as_target",
6683 },
6684 ],
6685 }),
6686 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
6687 ),
6688 test_offer_source_missing_availability_required(
6689 json!({
6690 "children": [
6691 {
6692 "name": "foo",
6693 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6694 },
6695 ],
6696 "offer": [
6697 {
6698 "protocol": "fuchsia.examples.Echo",
6699 "from": "#bar",
6700 "to": "#foo",
6701 "availability": "required",
6702 "source_availability": "unknown",
6703 },
6704 ],
6705 }),
6706 Err(Error::Validate { err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"#bar\""
6707 ),
6708 test_offer_source_missing_availability_same_as_target(
6709 json!({
6710 "children": [
6711 {
6712 "name": "foo",
6713 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
6714 },
6715 ],
6716 "offer": [
6717 {
6718 "protocol": "fuchsia.examples.Echo",
6719 "from": "#bar",
6720 "to": "#foo",
6721 "availability": "same_as_target",
6722 "source_availability": "unknown",
6723 },
6724 ],
6725 }),
6726 Err(Error::Validate { err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"#bar\""
6727 ),
6728 test_expose_source_availability_unknown(
6729 json!({
6730 "expose": [
6731 {
6732 "protocol": "fuchsia.examples.Echo",
6733 "from": "#bar",
6734 "availability": "optional",
6735 "source_availability": "unknown",
6736 },
6737 ],
6738 }),
6739 Ok(())
6740 ),
6741 test_expose_source_availability_required(
6742 json!({
6743 "expose": [
6744 {
6745 "protocol": "fuchsia.examples.Echo",
6746 "from": "#bar",
6747 "source_availability": "required",
6748 },
6749 ],
6750 }),
6751 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
6752 ),
6753 test_expose_source_availability_omitted(
6754 json!({
6755 "expose": [
6756 {
6757 "protocol": "fuchsia.examples.Echo",
6758 "from": "#bar",
6759 },
6760 ],
6761 }),
6762 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
6763 ),
6764 test_expose_source_void_availability_required(
6765 json!({
6766 "expose": [
6767 {
6768 "protocol": "fuchsia.examples.Echo",
6769 "from": "void",
6770 "availability": "required",
6771 },
6772 ],
6773 }),
6774 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
6775 ),
6776 test_expose_source_void_availability_same_as_target(
6777 json!({
6778 "expose": [
6779 {
6780 "protocol": "fuchsia.examples.Echo",
6781 "from": "void",
6782 "availability": "same_as_target",
6783 },
6784 ],
6785 }),
6786 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
6787 ),
6788 test_expose_source_missing_availability_required(
6789 json!({
6790 "expose": [
6791 {
6792 "protocol": "fuchsia.examples.Echo",
6793 "from": "#bar",
6794 "availability": "required",
6795 "source_availability": "unknown",
6796 },
6797 ],
6798 }),
6799 Err(Error::Validate { err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"#bar\""
6800 ),
6801 test_expose_source_missing_availability_same_as_target(
6802 json!({
6803 "expose": [
6804 {
6805 "protocol": "fuchsia.examples.Echo",
6806 "from": "#bar",
6807 "availability": "same_as_target",
6808 "source_availability": "unknown",
6809 },
6810 ],
6811 }),
6812 Err(Error::Validate { err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"#bar\""
6813 ),
6814 }
6815
6816 test_validate_cml! {
6818 test_cml_validate_use_service(
6819 json!({
6820 "use": [
6821 { "service": "CoolFonts", "path": "/svc/fuchsia.fonts.Provider" },
6822 { "service": "fuchsia.component.Realm", "from": "framework" },
6823 ],
6824 }),
6825 Ok(())
6826 ),
6827 test_cml_use_invalid_from_with_service(
6828 json!({
6829 "use": [ { "service": "foo", "from": "debug" } ]
6830 }),
6831 Err(Error::Validate { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
6832 ),
6833 test_cml_validate_offer_service(
6834 json!({
6835 "offer": [
6836 {
6837 "service": "fuchsia.logger.Log",
6838 "from": "#logger",
6839 "to": [ "#echo_server", "#modular" ],
6840 "as": "fuchsia.logger.SysLog"
6841 },
6842 {
6843 "service": "fuchsia.fonts.Provider",
6844 "from": "parent",
6845 "to": [ "#echo_server" ]
6846 },
6847 {
6848 "service": "fuchsia.net.Netstack",
6849 "from": "self",
6850 "to": [ "#echo_server" ]
6851 },
6852 ],
6853 "children": [
6854 {
6855 "name": "logger",
6856 "url": "fuchsia-pkg://logger",
6857 },
6858 {
6859 "name": "echo_server",
6860 "url": "fuchsia-pkg://echo_server",
6861 }
6862 ],
6863 "collections": [
6864 {
6865 "name": "modular",
6866 "durability": "transient",
6867 },
6868 ],
6869 "capabilities": [
6870 { "service": "fuchsia.net.Netstack" },
6871 ],
6872 }),
6873 Ok(())
6874 ),
6875 test_cml_validate_expose_service(
6876 json!(
6877 {
6878 "expose": [
6879 {
6880 "service": "fuchsia.fonts.Provider",
6881 "from": "self",
6882 },
6883 {
6884 "service": "fuchsia.logger.Log",
6885 "from": "#logger",
6886 "as": "logger"
6887 },
6888 ],
6889 "capabilities": [
6890 { "service": "fuchsia.fonts.Provider" },
6891 ],
6892 "children": [
6893 {
6894 "name": "logger",
6895 "url": "fuchsia-pkg://logger",
6896 },
6897 ]
6898 }
6899 ),
6900 Ok(())
6901 ),
6902 test_cml_validate_expose_service_multi_source(
6903 json!(
6904 {
6905 "expose": [
6906 {
6907 "service": "fuchsia.my.Service",
6908 "from": [ "self", "#a" ],
6909 },
6910 {
6911 "service": "fuchsia.my.Service",
6912 "from": "#coll",
6913 },
6914 ],
6915 "capabilities": [
6916 { "service": "fuchsia.my.Service" },
6917 ],
6918 "children": [
6919 {
6920 "name": "a",
6921 "url": "fuchsia-pkg://a",
6922 },
6923 ],
6924 "collections": [
6925 {
6926 "name": "coll",
6927 "durability": "transient",
6928 },
6929 ],
6930 }
6931 ),
6932 Ok(())
6933 ),
6934 test_cml_validate_offer_service_multi_source(
6935 json!(
6936 {
6937 "offer": [
6938 {
6939 "service": "fuchsia.my.Service",
6940 "from": [ "self", "parent" ],
6941 "to": "#b",
6942 },
6943 {
6944 "service": "fuchsia.my.Service",
6945 "from": [ "#a", "#coll" ],
6946 "to": "#b",
6947 },
6948 ],
6949 "capabilities": [
6950 { "service": "fuchsia.my.Service" },
6951 ],
6952 "children": [
6953 {
6954 "name": "a",
6955 "url": "fuchsia-pkg://a",
6956 },
6957 {
6958 "name": "b",
6959 "url": "fuchsia-pkg://b",
6960 },
6961 ],
6962 "collections": [
6963 {
6964 "name": "coll",
6965 "durability": "transient",
6966 },
6967 ],
6968 }
6969 ),
6970 Ok(())
6971 ),
6972 test_cml_service(
6973 json!({
6974 "capabilities": [
6975 {
6976 "protocol": "a",
6977 "path": "/minfs",
6978 },
6979 {
6980 "protocol": "b",
6981 "path": "/data",
6982 },
6983 {
6984 "protocol": "c",
6985 },
6986 ],
6987 }),
6988 Ok(())
6989 ),
6990 test_cml_service_multi(
6991 json!({
6992 "capabilities": [
6993 {
6994 "service": ["a", "b", "c"],
6995 },
6996 ],
6997 }),
6998 Ok(())
6999 ),
7000 test_cml_service_multi_invalid_path(
7001 json!({
7002 "capabilities": [
7003 {
7004 "service": ["a", "b", "c"],
7005 "path": "/minfs",
7006 },
7007 ],
7008 }),
7009 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `service` is supplied."
7010 ),
7011 test_cml_service_all_valid_chars(
7012 json!({
7013 "capabilities": [
7014 {
7015 "service": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
7016 },
7017 ],
7018 }),
7019 Ok(())
7020 ),
7021 }
7022
7023 test_validate_cml_with_feature! { FeatureSet::from(vec![]), {
7025 test_cml_configs(
7026 json!({
7027 "config": {
7028 "verbosity": {
7029 "type": "string",
7030 "max_size": 20,
7031 },
7032 "timeout": { "type": "uint64" },
7033 "tags": {
7034 "type": "vector",
7035 "max_count": 10,
7036 "element": {
7037 "type": "string",
7038 "max_size": 50
7039 }
7040 }
7041 }
7042 }),
7043 Ok(())
7044 ),
7045
7046 test_cml_configs_not_object(
7047 json!({
7048 "config": "abcd"
7049 }),
7050 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a map"
7051 ),
7052
7053 test_cml_configs_empty(
7054 json!({
7055 "config": {
7056 }
7057 }),
7058 Err(Error::Validate { err, .. }) if &err == "'config' section is empty"
7059 ),
7060
7061 test_cml_configs_bad_type(
7062 json!({
7063 "config": {
7064 "verbosity": 123456
7065 }
7066 }),
7067 Err(Error::Parse { err, .. }) if &err == "invalid type: integer `123456`, expected internally tagged enum ConfigValueType"
7068 ),
7069
7070 test_cml_configs_unknown_type(
7071 json!({
7072 "config": {
7073 "verbosity": {
7074 "type": "foo"
7075 }
7076 }
7077 }),
7078 Err(Error::Parse { err, .. }) if &err == "unknown variant `foo`, expected one of `bool`, `uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`, `int64`, `string`, `vector`"
7079 ),
7080
7081 test_cml_configs_no_max_count_vector(
7082 json!({
7083 "config": {
7084 "tags": {
7085 "type": "vector",
7086 "element": {
7087 "type": "string",
7088 "max_size": 50,
7089 }
7090 }
7091 }
7092 }),
7093 Err(Error::Parse { err, .. }) if &err == "missing field `max_count`"
7094 ),
7095
7096 test_cml_configs_no_max_size_string(
7097 json!({
7098 "config": {
7099 "verbosity": {
7100 "type": "string",
7101 }
7102 }
7103 }),
7104 Err(Error::Parse { err, .. }) if &err == "missing field `max_size`"
7105 ),
7106
7107 test_cml_configs_no_max_size_string_vector(
7108 json!({
7109 "config": {
7110 "tags": {
7111 "type": "vector",
7112 "max_count": 10,
7113 "element": {
7114 "type": "string",
7115 }
7116 }
7117 }
7118 }),
7119 Err(Error::Parse { err, .. }) if &err == "missing field `max_size`"
7120 ),
7121
7122 test_cml_configs_empty_key(
7123 json!({
7124 "config": {
7125 "": {
7126 "type": "bool"
7127 }
7128 }
7129 }),
7130 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty name no more than 64 characters in length"
7131 ),
7132
7133 test_cml_configs_too_long_key(
7134 json!({
7135 "config": {
7136 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": {
7137 "type": "bool"
7138 }
7139 }
7140 }),
7141 Err(Error::Parse { err, .. }) if &err == "invalid length 74, expected a non-empty name no more than 64 characters in length"
7142 ),
7143 test_cml_configs_key_starts_with_number(
7144 json!({
7145 "config": {
7146 "8abcd": { "type": "uint8" }
7147 }
7148 }),
7149 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"8abcd\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore"
7150 ),
7151
7152 test_cml_configs_key_ends_with_underscore(
7153 json!({
7154 "config": {
7155 "abcd_": { "type": "uint8" }
7156 }
7157 }),
7158 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"abcd_\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore"
7159 ),
7160
7161 test_cml_configs_capitals_in_key(
7162 json!({
7163 "config": {
7164 "ABCD": { "type": "uint8" }
7165 }
7166 }),
7167 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"ABCD\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore"
7168 ),
7169
7170 test_cml_configs_special_chars_in_key(
7171 json!({
7172 "config": {
7173 "!@#$": { "type": "uint8" }
7174 }
7175 }),
7176 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"!@#$\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore"
7177 ),
7178
7179 test_cml_configs_dashes_in_key(
7180 json!({
7181 "config": {
7182 "abcd-efgh": { "type": "uint8" }
7183 }
7184 }),
7185 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"abcd-efgh\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore"
7186 ),
7187
7188 test_cml_configs_bad_max_size_string(
7189 json!({
7190 "config": {
7191 "verbosity": {
7192 "type": "string",
7193 "max_size": "abcd"
7194 }
7195 }
7196 }),
7197 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
7198 ),
7199
7200 test_cml_configs_zero_max_size_string(
7201 json!({
7202 "config": {
7203 "verbosity": {
7204 "type": "string",
7205 "max_size": 0
7206 }
7207 }
7208 }),
7209 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
7210 ),
7211
7212 test_cml_configs_bad_max_count_on_vector(
7213 json!({
7214 "config": {
7215 "toggles": {
7216 "type": "vector",
7217 "max_count": "abcd",
7218 "element": {
7219 "type": "bool"
7220 }
7221 }
7222 }
7223 }),
7224 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
7225 ),
7226
7227 test_cml_configs_zero_max_count_on_vector(
7228 json!({
7229 "config": {
7230 "toggles": {
7231 "type": "vector",
7232 "max_count": 0,
7233 "element": {
7234 "type": "bool"
7235 }
7236 }
7237 }
7238 }),
7239 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
7240 ),
7241
7242 test_cml_configs_bad_max_size_string_vector(
7243 json!({
7244 "config": {
7245 "toggles": {
7246 "type": "vector",
7247 "max_count": 100,
7248 "element": {
7249 "type": "string",
7250 "max_size": "abcd"
7251 }
7252 }
7253 }
7254 }),
7255 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
7256 ),
7257
7258 test_cml_configs_zero_max_size_string_vector(
7259 json!({
7260 "config": {
7261 "toggles": {
7262 "type": "vector",
7263 "max_count": 100,
7264 "element": {
7265 "type": "string",
7266 "max_size": 0
7267 }
7268 }
7269 }
7270 }),
7271 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
7272 ),
7273 }}
7274
7275 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::AllowLongNames]), {
7277 test_cml_validate_set_allow_long_names_true(
7278 json!({
7279 "collections": [
7280 {
7281 "name": "foo",
7282 "durability": "transient",
7283 "allow_long_names": true
7284 },
7285 ],
7286 }),
7287 Ok(())
7288 ),
7289 test_cml_validate_set_allow_long_names_false(
7290 json!({
7291 "collections": [
7292 {
7293 "name": "foo",
7294 "durability": "transient",
7295 "allow_long_names": false
7296 },
7297 ],
7298 }),
7299 Ok(())
7300 ),
7301 }}
7302
7303 test_validate_cml! {
7306 test_cml_allow_long_names_without_feature(
7307 json!({
7308 "collections": [
7309 {
7310 "name": "foo",
7311 "durability": "transient",
7312 "allow_long_names": true
7313 },
7314 ],
7315 }),
7316 Err(Error::RestrictedFeature(s)) if s == "allow_long_names"
7317 ),
7318 }
7319
7320 test_validate_cml! {
7322 test_valid_empty_facets(
7323 json!({
7324 "facets": {}
7325 }),
7326 Ok(())
7327 ),
7328
7329 test_invalid_empty_facets(
7330 json!({
7331 "facets": ""
7332 }),
7333 Err(err) if err.to_string().contains("invalid type: string")
7334 ),
7335 test_valid_empty_fuchsia_test_facet(
7336 json!({
7337 "facets": {TEST_FACET_KEY: {}}
7338 }),
7339 Ok(())
7340 ),
7341
7342 test_valid_allowed_pkg_without_feature(
7343 json!({
7344 "facets": {
7345 TEST_TYPE_FACET_KEY: "some_realm",
7346 TEST_FACET_KEY: {
7347 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
7348 }
7349 }
7350 }),
7351 Ok(())
7352 ),
7353 }
7354
7355 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::RestrictTestTypeInFacet]), {
7357 test_valid_empty_facets_with_test_type_feature_enabled(
7358 json!({
7359 "facets": {}
7360 }),
7361 Ok(())
7362 ),
7363 test_valid_empty_fuchsia_test_facet_with_test_type_feature_enabled(
7364 json!({
7365 "facets": {TEST_FACET_KEY: {}}
7366 }),
7367 Ok(())
7368 ),
7369
7370 test_invalid_test_type_with_feature_enabled(
7371 json!({
7372 "facets": {
7373 TEST_FACET_KEY: {
7374 TEST_TYPE_FACET_KEY: "some_realm",
7375 }
7376 }
7377 }),
7378 Err(err) if err.to_string().contains(TEST_TYPE_FACET_KEY)
7379 ),
7380 }}
7381
7382 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::AllowNonHermeticPackages]), {
7384 test_valid_empty_facets_with_feature_disabled(
7385 json!({
7386 "facets": {}
7387 }),
7388 Ok(())
7389 ),
7390 test_valid_empty_fuchsia_test_facet_with_feature_disabled(
7391 json!({
7392 "facets": {TEST_FACET_KEY: {}}
7393 }),
7394 Ok(())
7395 ),
7396
7397 test_valid_allowed_pkg_with_feature_disabled(
7398 json!({
7399 "facets": {
7400 TEST_FACET_KEY: {
7401 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
7402 }
7403 }
7404 }),
7405 Ok(())
7406 ),
7407 }}
7408
7409 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature]), {
7411 test_valid_empty_facets_with_feature_enabled(
7412 json!({
7413 "facets": {}
7414 }),
7415 Ok(())
7416 ),
7417 test_valid_empty_fuchsia_test_facet_with_feature_enabled(
7418 json!({
7419 "facets": {TEST_FACET_KEY: {}}
7420 }),
7421 Ok(())
7422 ),
7423
7424 test_invalid_allowed_pkg_with_feature_enabled(
7425 json!({
7426 "facets": {
7427 TEST_FACET_KEY: {
7428 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
7429 }
7430 }
7431 }),
7432 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
7433 ),
7434 }}
7435
7436 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature, Feature::AllowNonHermeticPackages]), {
7438 test_invalid_empty_facets_with_feature_enabled(
7439 json!({
7440 "facets": {}
7441 }),
7442 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
7443 ),
7444 test_invalid_empty_fuchsia_test_facet_with_feature_enabled(
7445 json!({
7446 "facets": {TEST_FACET_KEY: {}}
7447 }),
7448 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
7449 ),
7450
7451 test_valid_allowed_pkg_with_feature_enabled(
7452 json!({
7453 "facets": {
7454 TEST_FACET_KEY: {
7455 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
7456 }
7457 }
7458 }),
7459 Ok(())
7460 ),
7461 }}
7462
7463 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::DynamicDictionaries]), {
7464 test_cml_offer_to_dictionary_unsupported(
7465 json!({
7466 "offer": [
7467 {
7468 "event_stream": "p",
7469 "from": "parent",
7470 "to": "self/dict",
7471 },
7472 ],
7473 "capabilities": [
7474 {
7475 "dictionary": "dict",
7476 },
7477 ],
7478 }),
7479 Err(Error::Validate { err, .. }) if &err == "\"offer\" to dictionary \
7480 \"self/dict\" for \"event_stream\" but dictionaries do not support this type yet."
7481 ),
7482 test_cml_dictionary_ref(
7483 json!({
7484 "use": [
7485 {
7486 "protocol": "a",
7487 "from": "parent/a",
7488 },
7489 {
7490 "protocol": "b",
7491 "from": "#child/a/b",
7492 },
7493 {
7494 "protocol": "c",
7495 "from": "self/a/b/c",
7496 },
7497 ],
7498 "capabilities": [
7499 {
7500 "dictionary": "a",
7501 },
7502 ],
7503 "children": [
7504 {
7505 "name": "child",
7506 "url": "fuchsia-pkg://child",
7507 },
7508 ],
7509 }),
7510 Ok(())
7511 ),
7512 test_cml_use_dictionary_disallowed(
7513 json!({
7514 "use": [
7515 {
7516 "dictionary": "dict",
7517 },
7518 ],
7519 }),
7520 Err(Error::Parse { err, .. }) if err.starts_with("unknown field `dictionary`")
7521 ),
7522 test_cml_expose_dictionary_from_self(
7523 json!({
7524 "expose": [
7525 {
7526 "dictionary": "foo_dictionary",
7527 "from": "self",
7528 },
7529 ],
7530 "capabilities": [
7531 {
7532 "dictionary": "foo_dictionary",
7533 },
7534 ]
7535 }),
7536 Ok(())
7537 ),
7538 test_cml_offer_to_dictionary_duplicate(
7539 json!({
7540 "offer": [
7541 {
7542 "protocol": "p",
7543 "from": "parent",
7544 "to": "self/dict",
7545 },
7546 {
7547 "protocol": "p",
7548 "from": "#child",
7549 "to": "self/dict",
7550 },
7551 ],
7552 "capabilities": [
7553 {
7554 "dictionary": "dict",
7555 },
7556 ],
7557 "children": [
7558 {
7559 "name": "child",
7560 "url": "fuchsia-pkg://child",
7561 },
7562 ],
7563 }),
7564 Err(Error::Validate { err, .. }) if &err == "\"p\" is a duplicate \"offer\" target capability for \"self/dict\""
7565 ),
7566 test_cml_offer_to_dictionary_dynamic(
7567 json!({
7568 "offer": [
7569 {
7570 "protocol": "p",
7571 "from": "parent",
7572 "to": "self/dict",
7573 },
7574 ],
7575 "capabilities": [
7576 {
7577 "dictionary": "dict",
7578 "path": "/out/dir",
7579 },
7580 ],
7581 }),
7582 Err(Error::Validate { err, .. }) if &err == "\"offer\" has dictionary target \"self/dict\" but \"dict\" sets \"path\". Therefore, it is a dynamic dictionary that does not allow offers into it."
7583 ),
7584 test_cml_offer_dependency_cycle_from_dictionary(
7585 json!({
7586 "offer": [
7587 {
7588 "protocol": "1",
7589 "from": "#a/in/dict",
7590 "to": [ "#b" ],
7591 "dependency": "strong"
7592 },
7593 {
7594 "directory": "2",
7595 "from": "#b/in/dict",
7596 "to": [ "#a" ],
7597 },
7598 ],
7599 "children": [
7600 {
7601 "name": "a",
7602 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm"
7603 },
7604 {
7605 "name": "b",
7606 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm"
7607 },
7608 ]
7609 }),
7610 Err(Error::Validate {
7611 err,
7612 ..
7613 }) if &err ==
7614 "Strong dependency cycles were found. Break the cycle by removing a \
7615 dependency or marking an offer as weak. Cycles: \
7616 {{#a -> #b -> #a}}"
7617 ),
7618 test_cml_offer_dependency_cycle_with_dictionary(
7619 json!({
7620 "capabilities": [
7621 {
7622 "dictionary": "dict",
7623 },
7624 ],
7625 "children": [
7626 {
7627 "name": "a",
7628 "url": "#meta/a.cm",
7629 },
7630 {
7631 "name": "b",
7632 "url": "#meta/b.cm",
7633 },
7634 ],
7635 "offer": [
7636 {
7637 "dictionary": "dict",
7638 "from": "self",
7639 "to": "#a",
7640 },
7641 {
7642 "protocol": "1",
7643 "from": "#b",
7644 "to": "self/dict",
7645 },
7646 {
7647 "protocol": "2",
7648 "from": "#a",
7649 "to": "#b",
7650 },
7651 ],
7652 }),
7653 Err(Error::Validate {
7654 err,
7655 ..
7656 }) if &err ==
7657 "Strong dependency cycles were found. Break the cycle by removing a \
7658 dependency or marking an offer as weak. Cycles: {{#a -> #b -> #dict -> #a}}"
7659 ),
7660 test_cml_offer_dependency_cycle_with_dictionary_indirect(
7661 json!({
7662 "capabilities": [
7663 {
7664 "dictionary": "dict",
7665 },
7666 ],
7667 "children": [
7668 {
7669 "name": "a",
7670 "url": "#meta/a.cm",
7671 },
7672 {
7673 "name": "b",
7674 "url": "#meta/b.cm",
7675 },
7676 ],
7677 "offer": [
7678 {
7679 "protocol": "3",
7680 "from": "self/dict",
7681 "to": "#a",
7682 },
7683 {
7684 "protocol": "1",
7685 "from": "#b",
7686 "to": "self/dict",
7687 },
7688 {
7689 "protocol": "2",
7690 "from": "#a",
7691 "to": "#b",
7692 },
7693 ],
7694 }),
7695 Err(Error::Validate {
7696 err,
7697 ..
7698 }) if &err ==
7699 "Strong dependency cycles were found. Break the cycle by removing a \
7700 dependency or marking an offer as weak. Cycles: {{#a -> #b -> #dict -> #a}}"
7701 ),
7702 test_cml_use_dependency_cycle_with_dictionary(
7703 json!({
7704 "capabilities": [
7705 {
7706 "protocol": "1",
7707 },
7708 {
7709 "dictionary": "dict",
7710 },
7711 ],
7712 "children": [
7713 {
7714 "name": "a",
7715 "url": "#meta/a.cm",
7716 },
7717 ],
7718 "use": [
7719 {
7720 "protocol": "2",
7721 "from": "#a",
7722 },
7723 ],
7724 "offer": [
7725 {
7726 "dictionary": "dict",
7727 "from": "self",
7728 "to": "#a",
7729 },
7730 {
7731 "protocol": "1",
7732 "from": "self",
7733 "to": "self/dict",
7734 },
7735 ],
7736 }),
7737 Err(Error::Validate {
7738 err,
7739 ..
7740 }) if &err ==
7741 "Strong dependency cycles were found. Break the cycle by removing a \
7742 dependency or marking an offer as weak. Cycles: {{#a -> self -> #dict -> #a}}"
7743 ),
7744 }}
7745
7746 test_validate_cml! {
7749 test_cml_aggregate_expose(
7750 json!({
7751 "expose": [
7752 {
7753 "service": "fuchsia.foo.Bar",
7754 "from": ["#a", "#b"],
7755 },
7756 ],
7757 "children": [
7758 {
7759 "name": "a",
7760 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm",
7761 },
7762 {
7763 "name": "b",
7764 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm",
7765 },
7766 ],
7767 }),
7768 Ok(())
7769 ),
7770 test_cml_aggregate_offer(
7771 json!({
7772 "offer": [
7773 {
7774 "service": "fuchsia.foo.Bar",
7775 "from": ["#a", "#b"],
7776 "to": "#target",
7777 },
7778 ],
7779 "children": [
7780 {
7781 "name": "a",
7782 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm",
7783 },
7784 {
7785 "name": "b",
7786 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm",
7787 },
7788 {
7789 "name": "target",
7790 "url": "fuchsia-pkg://fuchsia.com/target#meta/target.cm",
7791 },
7792 ],
7793 }),
7794 Ok(())
7795 ),
7796 }
7797
7798 use crate::translate::test_util::must_parse_cml;
7799 use crate::translate::{compile, CompileOptions};
7800
7801 #[test]
7802 fn test_cml_use_bad_config_from_self() {
7803 let input = must_parse_cml!({
7804 "use": [
7805 {
7806 "config": "fuchsia.config.MyConfig",
7807 "key": "my_config",
7808 "type": "bool",
7809 "from": "self",
7810 },
7811 ],
7812 });
7813
7814 let options = CompileOptions::new();
7815 assert_matches!(compile(&input, options), Err(Error::Validate { .. }));
7816 }
7817
7818 test_validate_cml! {
7820 a_test_cml_use_config(
7821 json!({"use": [
7822 {
7823 "config": "fuchsia.config.MyConfig",
7824 "key": "my_config",
7825 "type": "bool",
7826 },
7827 ],}),
7828 Ok(())
7829 ),
7830 test_cml_use_config_good_vector(
7831 json!({"use": [
7832 {
7833 "config": "fuchsia.config.MyConfig",
7834 "key": "my_config",
7835 "type": "vector",
7836 "element": { "type": "bool"},
7837 "max_count": 1,
7838 },
7839 ],}),
7840 Ok(())
7841 ),
7842 test_cml_use_config_bad_vector(
7843 json!({"use": [
7844 {
7845 "config": "fuchsia.config.MyConfig",
7846 "key": "my_config",
7847 "type": "vector",
7848 "element": { "type": "bool"},
7849 },
7851 ],}),
7852 Err(Error::Validate {err, .. })
7853 if &err == "Config 'fuchsia.config.MyConfig' is type Vector but is missing field 'max_count'"
7854 ),
7855 test_cml_use_config_bad_string(
7856 json!({"use": [
7857 {
7858 "config": "fuchsia.config.MyConfig",
7859 "key": "my_config",
7860 "type": "string",
7861 },
7863 ],}),
7864 Err(Error::Validate { err, .. })
7865 if &err == "Config 'fuchsia.config.MyConfig' is type String but is missing field 'max_size'"
7866 ),
7867
7868 test_cml_optional_use_no_config(
7869 json!({"use": [
7870 {
7871 "config": "fuchsia.config.MyConfig",
7872 "key": "my_config",
7873 "type": "bool",
7874 "availability": "optional",
7875 },
7876 ],}),
7877 Err(Error::Validate {err, ..})
7878 if &err == "Optionally using a config capability without a default requires a matching 'config' section."
7879 ),
7880 test_cml_transitional_use_no_config(
7881 json!({"use": [
7882 {
7883 "config": "fuchsia.config.MyConfig",
7884 "key": "my_config",
7885 "type": "bool",
7886 "availability": "transitional",
7887 },
7888 ],}),
7889 Err(Error::Validate {err, ..})
7890 if &err == "Optionally using a config capability without a default requires a matching 'config' section."
7891 ),
7892 test_cml_optional_use_bad_type(
7893 json!({"use": [
7894 {
7895 "config": "fuchsia.config.MyConfig",
7896 "key": "my_config",
7897 "type": "bool",
7898 "availability": "optional",
7899 },
7900 ],
7901 "config": {
7902 "my_config": { "type": "uint8"}
7903 }}),
7904 Err(Error::Validate {err, ..})
7905 if &err == "Use and config block differ on type for key 'my_config'"
7906 ),
7907
7908 test_config_required_with_default(
7909 json!({"use": [
7910 {
7911 "config": "fuchsia.config.MyConfig",
7912 "key": "my_config",
7913 "type": "bool",
7914 "default": "true",
7915 },
7916 ]}),
7917 Err(Error::Validate {err, ..})
7918 if &err == "Config 'fuchsia.config.MyConfig' is required and has a default value"
7919 ),
7920
7921 test_cml_optional_use_good(
7922 json!({"use": [
7923 {
7924 "config": "fuchsia.config.MyConfig",
7925 "key": "my_config",
7926 "type": "bool",
7927 "availability": "optional",
7928 },
7929 ],
7930 "config": {
7931 "my_config": { "type": "bool"},
7932 }
7933 }),
7934 Ok(())
7935 ),
7936 test_cml_use_two_types_bad(
7937 json!({"use": [
7938 {
7939 "protocol": "fuchsia.protocol.MyProtocol",
7940 "service": "fuchsia.service.MyService",
7941 },
7942 ],
7943 }),
7944 Err(Error::Validate {err, ..})
7945 if &err == "use declaration has multiple capability types defined: [\"service\", \"protocol\"]"
7946 ),
7947 test_cml_offer_two_types_bad(
7948 json!({"offer": [
7949 {
7950 "protocol": "fuchsia.protocol.MyProtocol",
7951 "service": "fuchsia.service.MyService",
7952 "from": "self",
7953 "to" : "#child",
7954 },
7955 ],
7956 }),
7957 Err(Error::Validate {err, ..})
7958 if &err == "offer declaration has multiple capability types defined: [\"service\", \"protocol\"]"
7959 ),
7960 test_cml_expose_two_types_bad(
7961 json!({"expose": [
7962 {
7963 "protocol": "fuchsia.protocol.MyProtocol",
7964 "service": "fuchsia.service.MyService",
7965 "from" : "self",
7966 },
7967 ],
7968 }),
7969 Err(Error::Validate {err, ..})
7970 if &err == "expose declaration has multiple capability types defined: [\"service\", \"protocol\"]"
7971 ),
7972 }
7973}