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