1use crate::features::{Feature, FeatureSet};
6use crate::types::capability::{Capability, CapabilityFromRef, ContextCapability};
7use crate::types::capability_id::CapabilityId;
8use crate::types::child::{Child, ContextChild};
9use crate::types::collection::{Collection, ContextCollection};
10use crate::types::document::{Document, DocumentContext};
11use crate::types::environment::{
12 ContextEnvironment, Environment, EnvironmentExtends, RegistrationRef,
13};
14use crate::types::expose::{ContextExpose, Expose, ExposeFromRef, ExposeToRef};
15use crate::types::offer::{
16 ContextOffer, Offer, OfferFromRef, OfferToAllCapability, OfferToRef, TargetAvailability,
17 offer_to_all_would_duplicate, offer_to_all_would_duplicate_context,
18};
19use crate::types::right::Rights;
20use crate::types::r#use::{ContextUse, Use, UseFromRef};
21use crate::{
22 AnyRef, Availability, CapabilityClause, ConfigKey, ConfigType, ConfigValueType,
23 ContextCapabilityClause, ContextSpanned, DependencyType, DictionaryRef, Error, EventScope,
24 FromClause, FromClauseContext, OneOrMany, Program, RootDictionaryRef, SourceAvailability,
25};
26use cm_types::{BorrowedName, IterablePath, Name};
27use std::collections::{BTreeMap, HashMap, HashSet};
28use std::hash::Hash;
29use std::path::Path;
30use std::{fmt, iter};
31
32use std::path::PathBuf;
33use std::sync::Arc;
34
35#[derive(Default, Clone)]
36pub struct CapabilityRequirements<'a> {
37 pub must_offer: &'a [OfferToAllCapability<'a>],
38 pub must_use: &'a [MustUseRequirement<'a>],
39}
40
41#[derive(PartialEq)]
42pub enum MustUseRequirement<'a> {
43 Protocol(&'a str),
44}
45
46impl<'a> MustUseRequirement<'a> {
47 fn name(&self) -> &str {
48 match self {
49 MustUseRequirement::Protocol(name) => name,
50 }
51 }
52}
53
54macro_rules! val {
55 ($field:expr) => {
56 $field.as_ref().map(|s| &s.value)
57 };
58}
59
60pub(crate) fn validate_cml(
62 document: &Document,
63 file: Option<&Path>,
64 features: &FeatureSet,
65 capability_requirements: &CapabilityRequirements<'_>,
66) -> Result<(), Error> {
67 let mut ctx = ValidationContext::new(&document, features, capability_requirements);
68 let mut res = ctx.validate();
69 if let Err(Error::Validate { filename, .. }) = &mut res {
70 if let Some(file) = file {
71 *filename = Some(file.to_string_lossy().into_owned());
72 }
73 }
74 res
75}
76
77#[allow(dead_code)]
78pub(crate) fn validate_cml_context(
79 document: &DocumentContext,
80 features: &FeatureSet,
81 capability_requirements: &CapabilityRequirements<'_>,
82) -> Result<(), Error> {
83 let mut ctx = ValidationContextV2::new(&document, features, capability_requirements);
84 ctx.validate()
85}
86
87fn offer_can_have_dependency_no_span(offer: &Offer) -> bool {
88 offer.directory.is_some()
89 || offer.protocol.is_some()
90 || offer.service.is_some()
91 || offer.dictionary.is_some()
92}
93
94fn offer_can_have_dependency(offer: &ContextOffer) -> bool {
95 offer.directory.is_some()
96 || offer.protocol.is_some()
97 || offer.service.is_some()
98 || offer.dictionary.is_some()
99}
100
101fn offer_dependency_no_span(offer: &Offer) -> DependencyType {
102 offer.dependency.clone().unwrap_or(DependencyType::Strong)
103}
104
105fn offer_dependency(offer: &ContextOffer) -> DependencyType {
106 match offer.dependency.clone() {
107 Some(cs_dep) => cs_dep.value,
108 None => DependencyType::Strong,
109 }
110}
111
112type ConflictInfo<'a> = (CapabilityId<'a>, Arc<PathBuf>);
113
114struct ValidationContextV2<'a> {
115 document: &'a DocumentContext,
116 features: &'a FeatureSet,
117 _capability_requirements: &'a CapabilityRequirements<'a>,
118 all_children: HashSet<Name>,
119 all_collections: HashSet<Name>,
120 all_resolvers: HashSet<Name>,
121 all_runners: HashSet<Name>,
122 all_storages: HashMap<Name, &'a CapabilityFromRef>,
123 all_capability_names: HashSet<Name>,
124 all_dictionaries: HashMap<Name, &'a ContextCapability>,
125 all_directories: HashSet<Name>,
126 all_protocols: HashSet<Name>,
127 all_configs: HashSet<Name>,
128 all_services: HashSet<Name>,
129}
130
131impl<'a> ValidationContextV2<'a> {
132 fn new(
133 document: &'a DocumentContext,
134 features: &'a FeatureSet,
135 _capability_requirements: &'a CapabilityRequirements<'a>,
136 ) -> Self {
137 let all_children = document.all_children_names();
138 let all_collections = document.all_collection_names();
139 let all_storages = document.all_storage_with_sources();
140 let all_capability_names = document.all_capability_names();
141 let all_dictionaries = document.all_dictionaries();
142 let all_configs = document.all_config_names();
143 let all_services = document
144 .capabilities
145 .as_ref()
146 .map(|caps| {
147 caps.iter()
148 .filter(|c| c.value.service.is_some())
149 .flat_map(|c| c.value.names())
150 .map(|n| n.value)
151 .collect()
152 })
153 .unwrap_or_default();
154
155 let all_directories = document
156 .capabilities
157 .as_ref()
158 .map(|caps| {
159 caps.iter()
160 .filter(|c| c.value.directory.is_some())
161 .flat_map(|c| c.value.names())
162 .map(|n| n.value)
163 .collect()
164 })
165 .unwrap_or_default();
166
167 let all_runners = document
168 .capabilities
169 .as_ref()
170 .map(|caps| {
171 caps.iter()
172 .filter(|c| c.value.runner.is_some())
173 .flat_map(|c| c.value.names())
174 .map(|n| n.value)
175 .collect()
176 })
177 .unwrap_or_default();
178
179 let all_resolvers = document
180 .capabilities
181 .as_ref()
182 .map(|caps| {
183 caps.iter()
184 .filter(|c| c.value.resolver.is_some())
185 .flat_map(|c| c.value.names())
186 .map(|n| n.value)
187 .collect()
188 })
189 .unwrap_or_default();
190
191 let all_protocols = document
192 .capabilities
193 .as_ref()
194 .map(|caps| {
195 caps.iter()
196 .filter(|c| c.value.protocol.is_some())
197 .flat_map(|c| c.value.names())
198 .map(|n| n.value)
199 .collect()
200 })
201 .unwrap_or_default();
202
203 ValidationContextV2 {
204 document,
205 features,
206 _capability_requirements,
207 all_children,
208 all_collections,
209 all_resolvers,
210 all_runners,
211 all_storages,
212 all_capability_names,
213 all_dictionaries,
214 all_directories,
215 all_protocols,
216 all_configs,
217 all_services,
218 }
219 }
220
221 fn validate(&mut self) -> Result<(), Error> {
222 if let Some(children) = self.document.children.as_ref() {
223 for child in children {
224 self.validate_child(&child)?;
225 }
226 }
227
228 if let Some(collections) = self.document.collections.as_ref() {
229 for collection in collections {
230 self.validate_collection(&collection)?;
231 }
232 }
233
234 if let Some(capabilities) = self.document.capabilities.as_ref() {
235 let mut used_ids = HashMap::new();
236 for capability in capabilities.iter() {
237 self.validate_capability(&capability, &mut used_ids)?;
238 }
239 }
240
241 if let Some(uses) = self.document.r#use.as_ref() {
242 let mut used_ids = HashMap::new();
243 for use_ in uses.iter() {
244 self.validate_use(&use_, &mut used_ids)?;
245 }
246 }
247
248 if let Some(exposes) = self.document.expose.as_ref() {
249 let mut used_ids = HashMap::new();
250 let mut exposed_to_framework_ids = HashMap::new();
251 for expose in exposes.iter() {
252 self.validate_expose(&expose, &mut used_ids, &mut exposed_to_framework_ids)?;
253 }
254 }
255
256 if let Some(offers) = self.document.offer.as_ref() {
257 let mut problem_protocols: Vec<ConflictInfo<'a>> = Vec::new();
258 let mut problem_dictionaries: Vec<ConflictInfo<'a>> = Vec::new();
259 let mut offered_ids: HashSet<CapabilityId<'a>> = HashSet::new();
260
261 offers
262 .iter()
263 .filter(|o_span| matches!(o_span.value.to.value, OneOrMany::One(OfferToRef::All)))
264 .try_for_each(|offer_wrapper| -> Result<(), Error> {
265 let offer = &offer_wrapper.value;
266 let mut process_cap = |field_is_some: bool,
267 conflict_list: &mut Vec<ConflictInfo<'a>>|
268 -> Result<(), Error> {
269 if field_is_some {
270 for (cap_id, cap_origin) in
271 CapabilityId::from_context_offer(offer_wrapper)?
272 {
273 if !offered_ids.insert(cap_id.clone()) {
274 conflict_list.push((cap_id, cap_origin));
275 }
276 }
277 }
278 Ok(())
279 };
280
281 process_cap(offer.protocol.is_some(), &mut problem_protocols)?;
282 process_cap(offer.dictionary.is_some(), &mut problem_dictionaries)?;
283 Ok(())
284 })?;
285
286 if !problem_protocols.is_empty() {
287 return Err(Error::validate_contexts(
288 format!(
289 r#"{} {:?} offered to "all" multiple times"#,
290 "Protocol(s)",
291 problem_protocols.iter().map(|(p, _o)| format!("{p}")).collect::<Vec<_>>()
292 ),
293 problem_protocols.iter().map(|(_p, o)| o.clone()).collect::<Vec<_>>(),
294 ));
295 }
296
297 if !problem_dictionaries.is_empty() {
298 return Err(Error::validate_contexts(
299 format!(
300 r#"{} {:?} offered to "all" multiple times"#,
301 "Dictionary(s)",
302 problem_dictionaries
303 .iter()
304 .map(|(p, _)| format!("{p}"))
305 .collect::<Vec<_>>()
306 ),
307 problem_dictionaries.iter().map(|(_p, o)| o.clone()).collect::<Vec<_>>(),
308 ));
309 }
310
311 let offered_to_all = offers
312 .iter()
313 .filter(|o| matches!(o.value.to.value, OneOrMany::One(OfferToRef::All)))
314 .filter(|o| o.value.protocol.is_some() || o.value.dictionary.is_some())
315 .collect::<Vec<&ContextSpanned<ContextOffer>>>();
316
317 let mut offered_ids = HashMap::new();
318 for offer in offers.iter() {
319 self.validate_offer(&offer, &mut offered_ids, &offered_to_all)?;
320 }
321 }
322
323 if let Some(environments) = self.document.environments.as_ref() {
324 for environment in environments {
325 self.validate_environment(&environment)?;
326 }
327 }
328
329 self.validate_facets()?;
330
331 self.validate_config()?;
332
333 Ok(())
334 }
335
336 fn validate_child(
337 &mut self,
338 child_wrapper: &'a ContextSpanned<ContextChild>,
339 ) -> Result<(), Error> {
340 let child = &child_wrapper.value;
341
342 if let Some(resource) = child.url.value.resource() {
343 if resource.ends_with(".cml") {
344 return Err(Error::validate_context(
345 format!(
346 "child URL ends in .cml instead of .cm, \
347which is almost certainly a mistake: {}",
348 child.url.value
349 ),
350 Some(child.url.origin.clone()),
351 ));
352 }
353 }
354
355 Ok(())
356 }
357
358 fn validate_capability(
359 &mut self,
360 capability_wrapper: &'a ContextSpanned<ContextCapability>,
361 used_ids: &mut HashMap<String, Arc<PathBuf>>,
362 ) -> Result<(), Error> {
363 let capability = &capability_wrapper.value;
364
365 if let Some(cs_directory) = &capability.directory {
366 if capability.path.is_none() {
367 return Err(Error::validate_context(
368 "\"path\" should be present with \"directory\"",
369 Some(cs_directory.origin.clone()),
370 ));
371 }
372
373 if capability.rights.is_none() {
374 return Err(Error::validate_context(
375 "\"rights\" should be present with \"directory\"",
376 Some(cs_directory.origin.clone()),
377 ));
378 }
379 }
380
381 if let Some(cs_storage) = capability.storage.as_ref() {
382 if capability.from.is_none() {
383 return Err(Error::validate_context(
384 "\"from\" should be present with \"storage\"",
385 Some(cs_storage.origin.clone()),
386 ));
387 }
388 if let Some(cs_path) = &capability.path {
389 return Err(Error::validate_context(
390 "\"path\" cannot be present with \"storage\", use \"backing_dir\"",
391 Some(cs_path.origin.clone()),
392 ));
393 }
394 if capability.backing_dir.is_none() {
395 return Err(Error::validate_context(
396 "\"backing_dir\" should be present with \"storage\"",
397 Some(cs_storage.origin.clone()),
398 ));
399 }
400 if capability.storage_id.is_none() {
401 return Err(Error::validate_context(
402 "\"storage_id\" should be present with \"storage\"",
403 Some(cs_storage.origin.clone()),
404 ));
405 }
406 }
407
408 if let Some(cs_runner) = &capability.runner {
409 if let Some(cs_from) = &capability.from {
410 return Err(Error::validate_context(
411 "\"from\" should not be present with \"runner\"",
412 Some(cs_from.origin.clone()),
413 ));
414 }
415
416 if capability.path.is_none() {
417 return Err(Error::validate_context(
418 "\"path\" should be present with \"runner\"",
419 Some(cs_runner.origin.clone()),
420 ));
421 }
422 }
423
424 if let Some(cs_resolver) = &capability.resolver {
425 if let Some(cs_from) = &capability.from {
426 return Err(Error::validate_context(
427 "\"from\" should not be present with \"resolver\"",
428 Some(cs_from.origin.clone()),
429 ));
430 }
431
432 if capability.path.is_none() {
433 return Err(Error::validate_context(
434 "\"path\" should be present with \"resolver\"",
435 Some(cs_resolver.origin.clone()),
436 ));
437 }
438 }
439
440 if capability.dictionary.as_ref().is_some() && capability.path.is_some() {
441 self.features.check(Feature::DynamicDictionaries)?;
442 }
443 if capability.delivery.is_some() {
444 self.features.check(Feature::DeliveryType)?;
445 }
446 if let Some(from) = capability.from.as_ref() {
447 self.validate_component_child_ref(
448 "\"capabilities\" source",
449 &AnyRef::from(&from.value),
450 Some(&capability_wrapper.origin),
451 )?;
452 }
453
454 let capability_ids = CapabilityId::from_context_capability(capability_wrapper)?;
456 for (capability_id, cap_origin) in capability_ids {
457 if let Some(conflict_origin) =
458 used_ids.insert(capability_id.to_string(), cap_origin.clone())
459 {
460 return Err(Error::validate_contexts(
461 format!("\"{}\" is a duplicate \"capability\" name", capability_id,),
462 vec![cap_origin, conflict_origin],
463 ));
464 }
465 }
466
467 Ok(())
468 }
469
470 fn validate_use(
471 &mut self,
472 use_wrapper: &'a ContextSpanned<ContextUse>,
473 used_ids: &mut HashMap<String, (CapabilityId<'a>, Arc<PathBuf>)>,
474 ) -> Result<(), Error> {
475 use_wrapper.capability_type(Some(use_wrapper.origin.clone()))?;
476 let use_ = &use_wrapper.value;
477
478 for checker in [
479 self.service_from_self_checker(use_),
480 self.protocol_from_self_checker(use_),
481 self.directory_from_self_checker(use_),
482 self.config_from_self_checker(use_),
483 ] {
484 checker.validate("used")?;
485 }
486
487 if val!(&use_.from) == Some(&UseFromRef::Debug) && val!(&use_.protocol).is_none() {
488 return Err(Error::validate_context(
489 "only \"protocol\" supports source from \"debug\"",
490 use_.from.as_ref().map(|s| s.origin.clone()),
491 ));
492 }
493
494 if use_.event_stream.is_some() {
495 if let Some(avail) = &use_.availability {
496 return Err(Error::validate_context(
497 "\"availability\" cannot be used with \"event_stream\"",
498 Some(avail.origin.clone()),
499 ));
500 }
501 if val!(&use_.from) == Some(&UseFromRef::Self_) {
502 return Err(Error::validate_context(
503 "\"from: self\" cannot be used with \"event_stream\"",
504 use_.from.as_ref().map(|s| s.origin.clone()),
505 ));
506 }
507 } else {
508 if let Some(filter) = &use_.filter {
510 return Err(Error::validate_context(
511 "\"filter\" can only be used with \"event_stream\"",
512 Some(filter.origin.clone()),
513 ));
514 }
515 }
516
517 if use_.storage.is_some() {
518 if let Some(from) = &use_.from {
519 return Err(Error::validate_context(
520 "\"from\" cannot be used with \"storage\"",
521 Some(from.origin.clone()),
522 ));
523 }
524 }
525
526 if use_.runner.is_some() {
527 if let Some(avail) = &use_.availability {
528 return Err(Error::validate_context(
529 "\"availability\" cannot be used with \"runner\"",
530 Some(avail.origin.clone()),
531 ));
532 }
533 if val!(&use_.from) == Some(&UseFromRef::Self_) {
534 return Err(Error::validate_context(
535 "\"from: self\" cannot be used with \"runner\"",
536 use_.from.as_ref().map(|s| s.origin.clone()),
537 ));
538 }
539 }
540
541 if let Some(avail) = &use_.availability {
542 if avail.value == Availability::SameAsTarget {
543 return Err(Error::validate_context(
544 "\"availability: same_as_target\" cannot be used with use declarations",
545 Some(avail.origin.clone()),
546 ));
547 }
548 }
549
550 if use_.dictionary.is_some() {
551 self.features.check(Feature::UseDictionaries)?;
552 }
553 if let Some(ContextSpanned { value: UseFromRef::Dictionary(_), origin: _ }) =
554 use_.from.as_ref()
555 {
556 if let Some(storage) = &use_.storage {
557 return Err(Error::validate_context(
558 "Dictionaries do not support \"storage\" capabilities",
559 Some(storage.origin.clone()),
560 ));
561 }
562 if let Some(event_stream) = &use_.event_stream {
563 return Err(Error::validate_context(
564 "Dictionaries do not support \"event_stream\" capabilities",
565 Some(event_stream.origin.clone()),
566 ));
567 }
568 }
569
570 if let Some(config) = &use_.config {
571 if use_.key.is_none() {
572 return Err(Error::validate_context(
573 format!("Config '{}' missing field 'key'", config.value),
574 Some(config.origin.clone()),
575 ));
576 }
577 let _ = use_config_to_value_type_context(use_)?;
578
579 let availability = val!(&use_.availability).cloned().unwrap_or(Availability::Required);
580 if availability == Availability::Required {
581 if let Some(default) = &use_.config_default {
582 return Err(Error::validate_context(
583 format!("Config '{}' is required and has a default value", config.value),
584 Some(default.origin.clone()),
585 ));
586 }
587 }
588 }
589
590 if let Some(handle) = &use_.numbered_handle {
591 if use_.protocol.is_some() {
592 if let Some(path) = &use_.path {
593 return Err(Error::validate_context(
594 "`path` and `numbered_handle` are incompatible",
595 Some(path.origin.clone()),
596 ));
597 }
598 } else {
599 return Err(Error::validate_context(
600 "`numbered_handle` is only supported for `use protocol`",
601 Some(handle.origin.clone()),
602 ));
603 }
604 }
605
606 let capability_ids_with_origins = CapabilityId::from_context_use(use_wrapper)?;
607 for (capability_id, origin) in capability_ids_with_origins {
608 if let Some((conflicting_id, conflicting_origin)) =
609 used_ids.insert(capability_id.to_string(), (capability_id.clone(), origin.clone()))
610 {
611 if !matches!(
612 (&capability_id, &conflicting_id),
613 (CapabilityId::UsedDictionary(_), CapabilityId::UsedDictionary(_))
614 ) {
615 return Err(Error::validate_contexts(
616 format!(
617 "\"{}\" is a duplicate \"use\" target {}",
618 capability_id,
619 capability_id.type_str()
620 ),
621 vec![origin, conflicting_origin],
622 ));
623 }
624 }
625
626 let dir = capability_id.get_dir_path();
627
628 let pkg_path = cm_types::NamespacePath::new("/pkg").unwrap();
630 if let Some(ref dir) = dir {
631 if dir.has_prefix(&pkg_path) {
632 return Err(Error::validate_context(
633 format!(
634 "{} \"{}\" conflicts with the protected path \"/pkg\", please use this capability with a different path",
635 capability_id.type_str(),
636 capability_id,
637 ),
638 Some(origin),
639 ));
640 }
641 }
642
643 for (_, (used_id, origin)) in used_ids.iter() {
646 if capability_id == *used_id {
647 continue;
648 }
649 let Some(ref path_b) = capability_id.get_target_path() else {
650 continue;
651 };
652 let Some(path_a) = used_id.get_target_path() else {
653 continue;
654 };
655 #[derive(Debug, Clone, Copy)]
656 enum NodeType {
657 Service,
658 Directory,
659 #[allow(unused)]
662 Dictionary,
663 }
664 fn capability_id_to_type(id: &CapabilityId<'_>) -> Option<NodeType> {
665 match id {
666 CapabilityId::UsedConfiguration(_) => None,
667 #[cfg(fuchsia_api_level_at_least = "30")]
668 CapabilityId::UsedDictionary(_) => Some(NodeType::Dictionary),
669 CapabilityId::UsedDirectory(_) => Some(NodeType::Directory),
670 CapabilityId::UsedEventStream(_) => Some(NodeType::Service),
671 CapabilityId::UsedProtocol(_) => Some(NodeType::Service),
672 #[cfg(fuchsia_api_level_at_least = "HEAD")]
673 CapabilityId::UsedRunner(_) => None,
674 CapabilityId::UsedService(_) => Some(NodeType::Directory),
675 CapabilityId::UsedStorage(_) => Some(NodeType::Directory),
676 _ => None,
677 }
678 }
679 let Some(type_a) = capability_id_to_type(&used_id) else {
680 continue;
681 };
682 let Some(type_b) = capability_id_to_type(&capability_id) else {
683 continue;
684 };
685 let mut conflicts = false;
686 match (type_a, type_b) {
687 (NodeType::Service, NodeType::Service)
688 | (NodeType::Directory, NodeType::Service)
689 | (NodeType::Service, NodeType::Directory)
690 | (NodeType::Directory, NodeType::Directory) => {
691 if path_a.has_prefix(&path_b) || path_b.has_prefix(&path_a) {
692 conflicts = true;
693 }
694 }
695 (NodeType::Dictionary, NodeType::Service)
696 | (NodeType::Dictionary, NodeType::Directory) => {
697 if path_a.has_prefix(&path_b) {
698 conflicts = true;
699 }
700 }
701 (NodeType::Service, NodeType::Dictionary)
702 | (NodeType::Directory, NodeType::Dictionary) => {
703 if path_b.has_prefix(&path_a) {
704 conflicts = true;
705 }
706 }
707 (NodeType::Dictionary, NodeType::Dictionary) => {
708 }
710 }
711 if conflicts {
712 return Err(Error::validate_contexts(
713 format!(
714 "{} \"{}\" is a prefix of \"use\" target {} \"{}\"",
715 used_id.type_str(),
716 used_id,
717 capability_id.type_str(),
718 capability_id,
719 ),
720 vec![origin.clone()],
721 ));
722 }
723 }
724 }
725
726 if let Some(dir) = &use_.directory {
727 match &use_.rights {
728 Some(rights) => {
729 self.validate_directory_rights(&rights.value, Some(&rights.origin))?
730 }
731 None => {
732 return Err(Error::validate_contexts(
733 "This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer.",
734 vec![dir.origin.clone()],
735 ));
736 }
737 };
738 }
739
740 Ok(())
741 }
742
743 fn validate_expose(
744 &self,
745 expose_wrapper: &'a ContextSpanned<ContextExpose>,
746 used_ids: &mut HashMap<String, Arc<PathBuf>>,
747 exposed_to_framework_ids: &mut HashMap<String, Arc<PathBuf>>,
748 ) -> Result<(), Error> {
749 let expose = &expose_wrapper.value;
750
751 expose.capability_type(Some(expose_wrapper.origin.clone()))?;
752
753 for checker in [
754 self.service_from_self_checker(expose),
755 self.protocol_from_self_checker(expose),
756 self.directory_from_self_checker(expose),
757 self.runner_from_self_checker(expose),
758 self.resolver_from_self_checker(expose),
759 self.dictionary_from_self_checker(expose),
760 self.config_from_self_checker(expose),
761 ] {
762 checker.validate("exposed")?;
763 }
764
765 if let Some(_) = expose.directory.as_ref() {
767 if expose.from.value.iter().any(|r| *r == ExposeFromRef::Self_)
768 || expose.rights.is_some()
769 {
770 if let Some(rights) = expose.rights.as_ref() {
771 self.validate_directory_rights(&rights.value, Some(&rights.origin))?;
772 }
773 }
774
775 if let Some(e2) = &expose.to {
778 if e2.value == ExposeToRef::Framework
779 && let Some(expose_subdir) = &expose.subdir
780 {
781 return Err(Error::validate_context(
782 "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead.",
783 Some(expose_subdir.origin.clone()),
784 ));
785 }
786 }
787 }
788
789 if let Some(event_stream) = &expose.event_stream {
790 if event_stream.value.iter().len() > 1 && expose.r#as.is_some() {
791 return Err(Error::validate_context(
792 format!("as cannot be used with multiple event streams"),
793 Some(expose.r#as.clone().unwrap().origin),
794 ));
795 }
796 if let Some(e2) = &expose.to
797 && e2.value == ExposeToRef::Framework
798 {
799 return Err(Error::validate_context(
800 format!("cannot expose an event_stream to framework"),
801 Some(event_stream.origin.clone()),
802 ));
803 }
804 for from in expose.from.value.iter() {
805 if from == &ExposeFromRef::Self_ {
806 return Err(Error::validate_context(
807 format!("Cannot expose event_streams from self"),
808 Some(event_stream.origin.clone()),
809 ));
810 }
811 }
812 if let Some(scopes) = &expose.scope {
813 for scope in &scopes.value {
814 match scope {
815 EventScope::Named(name) => {
816 if !self.all_children.contains::<BorrowedName>(name)
817 && !self.all_collections.contains::<BorrowedName>(name)
818 {
819 return Err(Error::validate_context(
820 format!(
821 "event_stream scope {} did not match a component or collection in this .cml file.",
822 name.as_str()
823 ),
824 Some(scopes.origin.clone()),
825 ));
826 }
827 }
828 }
829 }
830 }
831 }
832
833 for ref_ in expose.from.value.iter() {
834 if let ExposeFromRef::Dictionary(d) = ref_ {
835 if expose.event_stream.is_some() {
836 return Err(Error::validate_context(
837 "Dictionaries do not support \"event_stream\" capabilities",
838 Some(expose.event_stream.clone().unwrap().origin),
839 ));
840 }
841 match &d.root {
842 RootDictionaryRef::Self_ | RootDictionaryRef::Named(_) => {}
843 RootDictionaryRef::Parent => {
844 return Err(Error::validate_context(
845 "`expose` dictionary path must begin with `self` or `#<child-name>`",
846 Some(expose.from.origin.clone()),
847 ));
848 }
849 }
850 }
851 }
852
853 let target_cap_ids_with_origin = CapabilityId::from_context_expose(expose_wrapper)?;
855 for (capability_id, cap_origin) in target_cap_ids_with_origin {
856 let mut ids = &mut *used_ids;
857 if let Some(e2) = &expose.to
858 && e2.value == ExposeToRef::Framework
859 {
860 ids = &mut *exposed_to_framework_ids;
861 }
862 if let Some(conflict_origin) = ids.insert(capability_id.to_string(), cap_origin.clone())
863 {
864 if let CapabilityId::Service(_) = capability_id {
865 } else {
867 let expose_print = match &expose.to {
868 Some(expose_to) => &expose_to.value,
869 None => &ExposeToRef::Parent,
870 };
871 return Err(Error::validate_contexts(
872 format!(
873 "\"{}\" is a duplicate \"expose\" target capability for \"{}\"",
874 capability_id, expose_print
875 ),
876 vec![cap_origin, conflict_origin],
877 ));
878 }
879 }
880 }
881
882 self.validate_from_clause(
885 "expose",
886 expose,
887 &expose.source_availability.as_ref().map(|s| s.value.clone()),
888 &expose.availability.as_ref().map(|s| s.value.clone()),
889 expose.from.origin.clone(),
890 )?;
891
892 Ok(())
893 }
894
895 fn validate_offer(
896 &mut self,
897 offer_wrapper: &'a ContextSpanned<ContextOffer>,
898 used_ids: &mut HashMap<Name, HashMap<String, Arc<PathBuf>>>,
899 protocols_offered_to_all: &[&'a ContextSpanned<ContextOffer>],
900 ) -> Result<(), Error> {
901 let offer = &offer_wrapper.value;
902 offer.capability_type(Some(offer_wrapper.origin.clone()))?;
903
904 for checker in [
905 self.service_from_self_checker(offer),
906 self.protocol_from_self_checker(offer),
907 self.directory_from_self_checker(offer),
908 self.storage_from_self_checker(offer),
909 self.runner_from_self_checker(offer),
910 self.resolver_from_self_checker(offer),
911 self.dictionary_from_self_checker(offer),
912 self.config_from_self_checker(offer),
913 ] {
914 checker.validate("offered")?;
915 }
916
917 let from_wrapper = &offer.from;
918 let from_one_or_many = &from_wrapper.value;
919 let from_self = self.from_self(from_one_or_many);
920
921 if let Some(stream_span) = offer.event_stream.as_ref() {
922 if stream_span.value.iter().len() > 1 {
923 if let Some(as_span) = offer.r#as.as_ref() {
924 return Err(Error::validate_context(
925 "as cannot be used with multiple events",
926 Some(as_span.origin.clone()),
927 ));
928 }
929 }
930
931 if from_self {
932 return Err(Error::validate_context(
933 "cannot offer an event_stream from self",
934 Some(from_wrapper.origin.clone()),
935 ));
936 }
937 }
938
939 if offer.directory.as_ref().is_some() {
940 if from_self || offer.rights.is_some() {
941 if let Some(rights_span) = offer.rights.as_ref() {
942 self.validate_directory_rights(&rights_span.value, Some(&rights_span.origin))?;
943 }
944 }
945 }
946
947 if let Some(storages) = offer.storage.as_ref() {
948 for storage in &storages.value {
949 if offer.from.value.iter().any(|r| r.is_named()) {
950 return Err(Error::validate_contexts(
951 format!(
952 "Storage \"{}\" is offered from a child, but storage capabilities cannot be exposed",
953 storage
954 ),
955 vec![storages.origin.clone()],
956 ));
957 }
958 }
959 }
960
961 for from_ref in offer.from.value.iter() {
962 if let OfferFromRef::Dictionary(_) = &from_ref {
963 if let Some(storage_span) = offer.storage.as_ref() {
964 return Err(Error::validate_context(
965 "Dictionaries do not support \"storage\" capabilities",
966 Some(storage_span.origin.clone()),
967 ));
968 }
969 if let Some(stream_span) = offer.event_stream.as_ref() {
970 return Err(Error::validate_context(
971 "Dictionaries do not support \"event_stream\" capabilities",
972 Some(stream_span.origin.clone()),
973 ));
974 }
975 }
976 }
977
978 if !offer_can_have_dependency(offer) {
979 if let Some(dep_span) = offer.dependency.as_ref() {
980 return Err(Error::validate_context(
981 "Dependency can only be provided for protocol, directory, and service capabilities",
982 Some(dep_span.origin.clone()),
983 ));
984 }
985 }
986
987 let target_cap_ids_with_origin = CapabilityId::from_context_offer(offer_wrapper)?;
988
989 let to_wrapper = &offer.to;
990
991 let to_field_origin = &to_wrapper.origin;
992 let to_targets = &to_wrapper.value;
993
994 for target_ref in to_targets.iter() {
995 let to_target = match target_ref {
996 OfferToRef::All => continue,
997 OfferToRef::Named(to_target) => {
998 for offer_to_all in protocols_offered_to_all {
1001 offer_to_all_would_duplicate_context(
1002 offer_to_all,
1003 offer_wrapper,
1004 &to_target,
1005 )?;
1006 }
1007
1008 let target_availability_is_unknown = offer
1009 .target_availability
1010 .as_ref()
1011 .map_or(false, |a| a.value == TargetAvailability::Unknown);
1012
1013 if self.all_children.contains::<BorrowedName>(to_target.as_ref())
1015 || self.all_collections.contains::<BorrowedName>(to_target.as_ref())
1016 || target_availability_is_unknown
1017 {
1018 } else {
1020 if let OneOrMany::One(from) = &offer.from.value {
1021 return Err(Error::validate_context(
1022 format!(
1023 "\"{target_ref}\" is an \"offer\" target from \"{from}\" but \"{target_ref}\" does \
1024 not appear in \"children\" or \"collections\"",
1025 ),
1026 Some(to_field_origin.clone()),
1027 ));
1028 } else {
1029 return Err(Error::validate_context(
1030 format!(
1031 "\"{target_ref}\" is an \"offer\" target but \"{target_ref}\" does not appear in \
1032 \"children\" or \"collections\"",
1033 ),
1034 Some(to_field_origin.clone()),
1035 ));
1036 }
1037 }
1038
1039 if let Some(storage) = offer.storage.as_ref() {
1041 for storage in &storage.value {
1042 if let OneOrMany::One(OfferFromRef::Self_) = &offer.from.value {
1045 if let Some(CapabilityFromRef::Named(source)) =
1046 self.all_storages.get::<BorrowedName>(storage.as_ref())
1047 {
1048 if *to_target == *source {
1049 return Err(Error::validate_context(
1050 format!(
1051 "Storage offer target \"{}\" is same as source",
1052 target_ref
1053 ),
1054 Some(to_field_origin.clone()),
1055 ));
1056 }
1057 }
1058 }
1059 }
1060 } else {
1061 for reference in offer.from.value.iter() {
1062 if offer_dependency(offer) == DependencyType::Weak {
1064 continue;
1065 }
1066 match reference {
1067 OfferFromRef::Named(name) if name == to_target => {
1068 return Err(Error::validate_context(
1069 format!(
1070 "Offer target \"{}\" is same as source",
1071 target_ref
1072 ),
1073 Some(to_field_origin.clone()),
1074 ));
1075 }
1076 _ => {}
1077 }
1078 }
1079 }
1080 to_target
1081 }
1082 OfferToRef::OwnDictionary(to_target) => {
1083 let r2 = CapabilityId::from_context_offer(offer_wrapper)?;
1084 for (id, cap_origin) in r2 {
1085 match &id {
1086 CapabilityId::Protocol(_)
1087 | CapabilityId::Dictionary(_)
1088 | CapabilityId::Directory(_)
1089 | CapabilityId::Runner(_)
1090 | CapabilityId::Resolver(_)
1091 | CapabilityId::Service(_)
1092 | CapabilityId::Configuration(_) => {}
1093 CapabilityId::Storage(_) | CapabilityId::EventStream(_) => {
1094 let type_name = id.type_str();
1095 return Err(Error::validate_context(
1096 format!(
1097 "\"offer\" to dictionary \"{target_ref}\" for \"{type_name}\" but \
1098 dictionaries do not support this type yet."
1099 ),
1100 Some(cap_origin),
1101 ));
1102 }
1103 CapabilityId::UsedService(_)
1104 | CapabilityId::UsedProtocol(_)
1105 | CapabilityId::UsedProtocolNumberedHandle(_)
1106 | CapabilityId::UsedDirectory(_)
1107 | CapabilityId::UsedStorage(_)
1108 | CapabilityId::UsedEventStream(_)
1109 | CapabilityId::UsedRunner(_)
1110 | CapabilityId::UsedConfiguration(_)
1111 | CapabilityId::UsedDictionary(_) => {
1112 unreachable!("this is not a use")
1113 }
1114 }
1115 }
1116 let target_availability_is_unknown = offer
1118 .target_availability
1119 .as_ref()
1120 .map_or(false, |a| a.value == TargetAvailability::Unknown);
1121
1122 if let Some(d) = self.all_dictionaries.get::<BorrowedName>(to_target.as_ref()) {
1124 if d.path.is_some() {
1126 return Err(Error::validate_context(
1127 format!(
1128 "\"offer\" has dictionary target \"{target_ref}\" but \"{to_target}\" \
1129 sets \"path\". Therefore, it is a dynamic dictionary that \
1130 does not allow offers into it."
1131 ),
1132 Some(to_field_origin.clone()),
1133 ));
1134 }
1135 } else if !target_availability_is_unknown {
1136 return Err(Error::validate_context(
1138 format!(
1139 "\"offer\" has dictionary target \"{target_ref}\" but \"{to_target}\" \
1140 is not a dictionary capability defined by this component"
1141 ),
1142 Some(to_field_origin.clone()),
1143 ));
1144 }
1145 to_target
1146 }
1147 };
1148
1149 let ids_for_entity = used_ids.entry(to_target.clone()).or_insert(HashMap::new());
1151 for (target_cap_id, target_origin) in &target_cap_ids_with_origin {
1152 if let Some(conflict_origin) =
1153 ids_for_entity.insert(target_cap_id.to_string(), target_origin.clone())
1154 {
1155 if let CapabilityId::Service(_) = target_cap_id {
1156 } else {
1158 return Err(Error::validate_contexts(
1159 format!(
1160 "\"{}\" is a duplicate \"offer\" target capability for \"{}\"",
1161 target_cap_id, target_ref
1162 ),
1163 vec![target_origin.clone(), conflict_origin],
1164 ));
1165 }
1166 }
1167 }
1168 }
1169
1170 self.validate_from_clause(
1171 "offer",
1172 offer,
1173 &offer.source_availability.as_ref().map(|s| s.value.clone()),
1174 &offer.availability.as_ref().map(|s| s.value.clone()),
1175 offer.from.origin.clone(),
1176 )?;
1177
1178 Ok(())
1179 }
1180
1181 fn validate_collection(
1182 &mut self,
1183 collection: &'a ContextSpanned<ContextCollection>,
1184 ) -> Result<(), Error> {
1185 if collection.value.allow_long_names.is_some() {
1186 self.features.check(Feature::AllowLongNames)?;
1187 }
1188 Ok(())
1189 }
1190
1191 fn validate_environment(
1192 &mut self,
1193 environment_wrapper: &'a ContextSpanned<ContextEnvironment>,
1194 ) -> Result<(), Error> {
1195 let environment = &environment_wrapper.value;
1196
1197 if let Some(extends_span) = &environment.extends {
1198 if extends_span.value == EnvironmentExtends::None {
1199 if environment.stop_timeout_ms.is_none() {
1200 return Err(Error::validate_context(
1201 "'__stop_timeout_ms' must be provided if the environment extends 'none'",
1202 Some(extends_span.origin.clone()),
1203 ));
1204 }
1205 }
1206 }
1207
1208 if let Some(runners) = &environment.runners {
1209 let mut used_names = HashMap::new();
1210 for registration_span in runners {
1211 let reg = ®istration_span.value;
1212
1213 let (target_name, name_origin) = if let Some(as_span) = ®.r#as {
1214 (&as_span.value, &as_span.origin)
1215 } else {
1216 (®.runner.value, ®.runner.origin)
1217 };
1218
1219 if let Some((prev_runner, prev_origin)) =
1220 used_names.insert(target_name, (®.runner.value, name_origin))
1221 {
1222 return Err(Error::validate_contexts(
1223 format!(
1224 "Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".",
1225 target_name, ®.runner.value, prev_runner
1226 ),
1227 vec![prev_origin.clone(), name_origin.clone()],
1228 ));
1229 }
1230
1231 let runner_ref: Name = reg.runner.value.clone();
1233 if reg.from.value == RegistrationRef::Self_
1234 && !self.all_runners.contains(&runner_ref)
1235 {
1236 return Err(Error::validate_context(
1237 format!(
1238 "Runner \"{}\" is not defined in the root \"runners\" section",
1239 ®.runner.value
1240 ),
1241 Some(reg.runner.origin.clone()),
1242 ));
1243 }
1244
1245 self.validate_component_child_ref(
1246 &format!("\"{}\" runner source", ®.runner.value),
1247 &AnyRef::from(®.from.value),
1248 Some(®.from.origin),
1249 )?;
1250 }
1251 }
1252
1253 if let Some(resolvers) = &environment.resolvers {
1254 let mut used_schemes = HashMap::new();
1255 for registration_span in resolvers {
1256 let reg = ®istration_span.value;
1257
1258 if let Some((prev_resolver, prev_origin)) = used_schemes
1259 .insert(®.scheme.value, (®.resolver.value, ®.scheme.origin))
1260 {
1261 return Err(Error::validate_contexts(
1262 format!(
1263 "scheme \"{}\" for resolver \"{}\" is already registered to \"{}\".",
1264 ®.scheme.value, ®.resolver.value, prev_resolver
1265 ),
1266 vec![prev_origin.clone(), reg.scheme.origin.clone()],
1267 ));
1268 }
1269
1270 self.validate_component_child_ref(
1271 &format!("\"{}\" resolver source", ®.resolver.value),
1272 &AnyRef::from(®.from.value),
1273 Some(®.from.origin),
1274 )?;
1275 }
1276 }
1277
1278 if let Some(debug_capabilities) = &environment.debug {
1279 for debug_span in debug_capabilities {
1280 let debug = &debug_span.value;
1281 self.protocol_from_self_checker(debug).validate("registered as debug")?;
1282 self.validate_from_clause("debug", debug, &None, &None, debug.from.origin.clone())?;
1283 }
1284 }
1285
1286 Ok(())
1287 }
1288
1289 fn get_test_facet(&self) -> Option<&ContextSpanned<serde_json::Value>> {
1290 match &self.document.facets {
1291 Some(m) => m.get(TEST_FACET_KEY),
1292 None => None,
1293 }
1294 }
1295
1296 fn validate_facets(&self) -> Result<(), Error> {
1297 let test_facet_spanned = self.get_test_facet();
1298 let enable_allow_non_hermetic_packages =
1299 self.features.has(&Feature::EnableAllowNonHermeticPackagesFeature);
1300
1301 if let Some(spanned) = test_facet_spanned {
1302 let test_facet_origin = &spanned.origin;
1303 let test_facet_map = match &spanned.value {
1304 serde_json::Value::Object(m) => m,
1305 _ => {
1306 return Err(Error::validate_context(
1307 format!("'{}' is not an object", TEST_FACET_KEY),
1308 Some(test_facet_origin.clone()),
1309 ));
1310 }
1311 };
1312
1313 let restrict_test_type = self.features.has(&Feature::RestrictTestTypeInFacet);
1314
1315 if restrict_test_type {
1316 if test_facet_map.contains_key(TEST_TYPE_FACET_KEY) {
1317 return Err(Error::validate_context(
1318 format!(
1319 "'{}' is not allowed in facets. Refer to: \
1320 https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework#non-hermetic_tests",
1321 TEST_TYPE_FACET_KEY
1322 ),
1323 Some(test_facet_origin.clone()),
1324 ));
1325 }
1326 }
1327 }
1328
1329 if enable_allow_non_hermetic_packages {
1330 let allow_non_hermetic_packages = self.features.has(&Feature::AllowNonHermeticPackages);
1331
1332 let has_deprecated_facet = test_facet_spanned
1333 .and_then(|s| s.value.as_object())
1334 .map_or(false, |m| m.contains_key(TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY));
1335
1336 if allow_non_hermetic_packages && !has_deprecated_facet {
1337 return Err(Error::validate(format!(
1338 "Remove restricted_feature '{}' as manifest does not contain facet '{}'",
1339 Feature::AllowNonHermeticPackages,
1340 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
1341 )));
1342 }
1343
1344 if has_deprecated_facet && !allow_non_hermetic_packages {
1345 let origin = test_facet_spanned.map(|s| s.origin.clone());
1346
1347 return Err(Error::validate_context(
1348 format!(
1349 "restricted_feature '{}' should be present with facet '{}'",
1350 Feature::AllowNonHermeticPackages,
1351 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
1352 ),
1353 origin,
1354 ));
1355 }
1356 }
1357
1358 Ok(())
1359 }
1360
1361 fn validate_config(&self) -> Result<(), Error> {
1362 let fields = self.document.config.as_ref();
1363
1364 let optional_use_keys: BTreeMap<ConfigKey, ContextSpanned<ConfigValueType>> = self
1365 .document
1366 .r#use
1367 .iter()
1368 .flatten()
1369 .filter_map(|u_spanned| {
1370 let u = &u_spanned.value;
1371 if u.config.is_none() {
1372 return None;
1373 }
1374
1375 if u.availability.as_ref().map(|s| s.value) != Some(Availability::Optional) {
1376 return None;
1377 }
1378
1379 if u.config_default.is_some() {
1380 return None;
1381 }
1382
1383 let key =
1384 ConfigKey(u.key.as_ref().expect("key should be set").value.clone().into());
1385 let value_type =
1386 use_config_to_value_type_context(u).expect("config type should be valid");
1387
1388 Some((key, ContextSpanned { value: value_type, origin: u_spanned.origin.clone() }))
1389 })
1390 .collect();
1391
1392 let Some(fields_map) = fields else {
1393 if !optional_use_keys.is_empty() {
1394 return Err(Error::validate(
1395 "Optionally using a config capability without a default requires a matching 'config' section.",
1396 ));
1397 }
1398 return Ok(());
1399 };
1400
1401 if fields_map.is_empty() {
1402 return Err(Error::validate("'config' section is empty"));
1403 }
1404
1405 for (key, use_spanned) in optional_use_keys {
1406 match fields_map.get(&key) {
1407 None => {
1408 return Err(Error::validate_context(
1409 format!("'config' section must contain key for optional use '{}'", key),
1410 Some(use_spanned.origin),
1411 ));
1412 }
1413 Some(config_spanned) => {
1414 if config_spanned.value != use_spanned.value {
1415 return Err(Error::validate_contexts(
1416 format!("Use and config block differ on type for key '{}'", key),
1417 vec![use_spanned.origin, config_spanned.origin.clone()],
1418 ));
1419 }
1420 }
1421 }
1422 }
1423
1424 Ok(())
1425 }
1426
1427 fn validate_directory_rights(
1430 &self,
1431 rights_clause: &Rights,
1432 origin: Option<&Arc<PathBuf>>,
1433 ) -> Result<(), Error> {
1434 let mut rights = HashSet::new();
1435 for right_token in rights_clause.0.iter() {
1436 for right in right_token.expand() {
1437 if !rights.insert(right) {
1438 return Err(Error::validate_context(
1439 format!("\"{}\" is duplicated in the rights clause.", right_token),
1440 origin.cloned(),
1441 ));
1442 }
1443 }
1444 }
1445 Ok(())
1446 }
1447
1448 fn from_self(&self, from_one_or_many: &OneOrMany<OfferFromRef>) -> bool {
1449 for from_ref in from_one_or_many.iter() {
1450 match from_ref {
1451 OfferFromRef::Self_ => return true,
1452 _ => {}
1453 }
1454 }
1455 false
1456 }
1457
1458 fn validate_from_clause<T>(
1467 &self,
1468 verb: &str,
1469 cap: &T,
1470 source_availability: &Option<SourceAvailability>,
1471 availability: &Option<Availability>,
1472 origin: Arc<PathBuf>,
1473 ) -> Result<(), Error>
1474 where
1475 T: ContextCapabilityClause + FromClauseContext,
1476 {
1477 let from = cap.from_();
1478 if cap.service().is_none() && from.value.is_many() {
1479 return Err(Error::validate_context(
1480 format!(
1481 "\"{}\" capabilities cannot have multiple \"from\" clauses",
1482 cap.capability_type(None).unwrap()
1483 ),
1484 Some(origin),
1485 ));
1486 }
1487
1488 let from_val = &from.value;
1489
1490 if from_val.is_many() {
1491 ensure_no_duplicate_values(from_val.iter())?;
1492 }
1493
1494 let reference_description = format!("\"{}\" source", verb);
1495 for from_clause in from_val {
1496 let ref_validity_res = if cap.protocol().is_some() {
1499 self.validate_component_child_or_capability_ref(
1500 &reference_description,
1501 &from_clause,
1502 Some(&origin),
1503 )
1504 } else if cap.service().is_some() {
1505 self.validate_component_child_or_collection_ref(
1507 &reference_description,
1508 &from_clause,
1509 Some(&origin),
1510 )
1511 } else {
1512 self.validate_component_child_ref(
1513 &reference_description,
1514 &from_clause,
1515 Some(&origin),
1516 )
1517 };
1518
1519 match ref_validity_res {
1520 Ok(()) if *from_clause == AnyRef::Void => {
1521 if availability != &Some(Availability::Optional) {
1523 return Err(Error::validate_context(
1524 format!(
1525 "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"{}\", from: \"{}\"",
1526 cap.names()
1527 .iter()
1528 .map(|n| n.value.as_str())
1529 .collect::<Vec<_>>()
1530 .join(", "),
1531 cap.from_().value,
1532 ),
1533 Some(origin),
1534 ));
1535 }
1536 }
1537 Ok(()) => {
1538 }
1540 Err(_) if source_availability == &Some(SourceAvailability::Unknown) => {
1541 if availability != &Some(Availability::Optional) && availability != &None {
1543 return Err(Error::validate_context(
1544 format!(
1545 "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"{}\", from: \"{}\"",
1546 cap.names()
1547 .iter()
1548 .map(|n| n.value.as_str())
1549 .collect::<Vec<_>>()
1550 .join(", "),
1551 cap.from_().value,
1552 ),
1553 Some(origin),
1554 ));
1555 }
1556 }
1557 Err(e) => {
1558 return Err(e);
1560 }
1561 }
1562 }
1563 Ok(())
1564 }
1565
1566 fn validate_component_child_ref(
1573 &self,
1574 reference_description: &str,
1575 component_ref: &AnyRef<'_>,
1576 origin: Option<&Arc<PathBuf>>,
1577 ) -> Result<(), Error> {
1578 match component_ref {
1579 AnyRef::Named(name) => {
1580 if !self.all_children.contains(*name) {
1582 return Err(Error::validate_context(
1583 format!(
1584 "{} \"{}\" does not appear in \"children\"",
1585 reference_description, component_ref
1586 ),
1587 origin.cloned(),
1588 ));
1589 }
1590 Ok(())
1591 }
1592 _ => Ok(()),
1594 }
1595 }
1596
1597 fn validate_component_child_or_collection_ref(
1604 &self,
1605 reference_description: &str,
1606 component_ref: &AnyRef<'_>,
1607 origin: Option<&Arc<PathBuf>>,
1608 ) -> Result<(), Error> {
1609 match component_ref {
1610 AnyRef::Named(name) => {
1611 if !self.all_children.contains(*name) && !self.all_collections.contains(*name) {
1613 return Err(Error::validate_context(
1614 format!(
1615 "{} \"{}\" does not appear in \"children\" or \"collections\"",
1616 reference_description, component_ref
1617 ),
1618 origin.cloned(),
1619 ));
1620 }
1621 Ok(())
1622 }
1623 _ => Ok(()),
1625 }
1626 }
1627
1628 fn validate_component_capability_ref(
1635 &self,
1636 reference_description: &str,
1637 capability_ref: &AnyRef<'_>,
1638 origin: Option<&Arc<PathBuf>>,
1639 ) -> Result<(), Error> {
1640 match capability_ref {
1641 AnyRef::Named(name) => {
1642 if !self.all_capability_names.contains(*name) {
1643 return Err(Error::validate_context(
1644 format!(
1645 "{} \"{}\" does not appear in \"capabilities\"",
1646 reference_description, capability_ref
1647 ),
1648 origin.cloned(),
1649 ));
1650 }
1651 Ok(())
1652 }
1653 _ => Ok(()),
1654 }
1655 }
1656
1657 fn validate_component_child_or_capability_ref(
1664 &self,
1665 reference_description: &str,
1666 ref_: &AnyRef<'_>,
1667 origin: Option<&Arc<PathBuf>>,
1668 ) -> Result<(), Error> {
1669 if self.validate_component_child_ref(reference_description, ref_, origin).is_err()
1670 && self.validate_component_capability_ref(reference_description, ref_, origin).is_err()
1671 {
1672 return Err(Error::validate_context(
1673 format!(
1674 "{} \"{}\" does not appear in \"children\" or \"capabilities\"",
1675 reference_description, ref_
1676 ),
1677 origin.cloned(),
1678 ));
1679 }
1680 Ok(())
1681 }
1682
1683 fn protocol_from_self_checker<'b>(
1684 &'b self,
1685 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1686 ) -> RouteFromSelfCheckerV2<'b> {
1687 RouteFromSelfCheckerV2 {
1688 capability_name: input.protocol().map(|spanned| {
1689 spanned.map(|one_or_many| match one_or_many {
1690 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1691 OneOrMany::Many(names) => {
1692 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1693 }
1694 })
1695 }),
1696 from: input.from_(),
1697 container: &self.all_protocols,
1698 all_dictionaries: &self.all_dictionaries,
1699 typename: "protocol",
1700 }
1701 }
1702
1703 fn resolver_from_self_checker<'b>(
1704 &'b self,
1705 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1706 ) -> RouteFromSelfCheckerV2<'b> {
1707 RouteFromSelfCheckerV2 {
1708 capability_name: input.resolver().map(|spanned| {
1709 spanned.map(|one_or_many| match one_or_many {
1710 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1711 OneOrMany::Many(names) => {
1712 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1713 }
1714 })
1715 }),
1716 from: input.from_(),
1717 container: &self.all_resolvers,
1718 all_dictionaries: &self.all_dictionaries,
1719 typename: "resolver",
1720 }
1721 }
1722
1723 fn runner_from_self_checker<'b>(
1724 &'b self,
1725 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1726 ) -> RouteFromSelfCheckerV2<'b> {
1727 RouteFromSelfCheckerV2 {
1728 capability_name: input.runner().map(|spanned| {
1729 spanned.map(|one_or_many| match one_or_many {
1730 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1731 OneOrMany::Many(names) => {
1732 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1733 }
1734 })
1735 }),
1736 from: input.from_(),
1737 container: &self.all_runners,
1738 all_dictionaries: &self.all_dictionaries,
1739 typename: "runner",
1740 }
1741 }
1742
1743 fn service_from_self_checker<'b>(
1744 &'b self,
1745 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1746 ) -> RouteFromSelfCheckerV2<'b> {
1747 RouteFromSelfCheckerV2 {
1748 capability_name: input.service().map(|spanned| {
1749 spanned.map(|one_or_many| match one_or_many {
1750 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1751 OneOrMany::Many(names) => {
1752 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1753 }
1754 })
1755 }),
1756 from: input.from_(),
1757 container: &self.all_services,
1758 all_dictionaries: &self.all_dictionaries,
1759 typename: "service",
1760 }
1761 }
1762
1763 fn config_from_self_checker<'b>(
1764 &'b self,
1765 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1766 ) -> RouteFromSelfCheckerV2<'b> {
1767 RouteFromSelfCheckerV2 {
1768 capability_name: input.config().map(|spanned| {
1769 spanned.map(|one_or_many| match one_or_many {
1770 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1771 OneOrMany::Many(names) => {
1772 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1773 }
1774 })
1775 }),
1776 from: input.from_(),
1777 container: &self.all_configs,
1778 all_dictionaries: &self.all_dictionaries,
1779 typename: "config",
1780 }
1781 }
1782
1783 fn dictionary_from_self_checker<'b>(
1784 &'b self,
1785 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1786 ) -> RouteFromSelfCheckerV2<'b> {
1787 RouteFromSelfCheckerV2 {
1788 capability_name: input.dictionary().map(|spanned| {
1789 spanned.map(|one_or_many| match one_or_many {
1790 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1791 OneOrMany::Many(names) => {
1792 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1793 }
1794 })
1795 }),
1796 from: input.from_(),
1797 container: &self.all_dictionaries,
1798 all_dictionaries: &self.all_dictionaries,
1799 typename: "dictionary",
1800 }
1801 }
1802
1803 fn directory_from_self_checker<'b>(
1804 &'b self,
1805 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1806 ) -> RouteFromSelfCheckerV2<'b> {
1807 RouteFromSelfCheckerV2 {
1808 capability_name: input.directory().map(|spanned| {
1809 spanned.map(|one_or_many| match one_or_many {
1810 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1811 OneOrMany::Many(names) => {
1812 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1813 }
1814 })
1815 }),
1816 from: input.from_(),
1817 container: &self.all_directories,
1818 all_dictionaries: &self.all_dictionaries,
1819 typename: "directory",
1820 }
1821 }
1822
1823 fn storage_from_self_checker<'b>(
1824 &'b self,
1825 input: &'b (impl ContextCapabilityClause + FromClauseContext),
1826 ) -> RouteFromSelfCheckerV2<'b> {
1827 RouteFromSelfCheckerV2 {
1828 capability_name: input.storage().map(|spanned| {
1829 spanned.map(|one_or_many| match one_or_many {
1830 OneOrMany::One(name) => OneOrMany::One(AnyRef::from(name)),
1831 OneOrMany::Many(names) => {
1832 OneOrMany::Many(names.iter().cloned().map(AnyRef::from).collect())
1833 }
1834 })
1835 }),
1836 from: input.from_(),
1837 container: &self.all_storages,
1838 all_dictionaries: &self.all_dictionaries,
1839 typename: "storage",
1840 }
1841 }
1842}
1843
1844struct ValidationContext<'a> {
1845 document: &'a Document,
1846 features: &'a FeatureSet,
1847 capability_requirements: &'a CapabilityRequirements<'a>,
1848 all_children: HashMap<&'a BorrowedName, &'a Child>,
1849 all_collections: HashSet<&'a BorrowedName>,
1850 all_storages: HashMap<&'a BorrowedName, &'a CapabilityFromRef>,
1851 all_services: HashSet<&'a BorrowedName>,
1852 all_protocols: HashSet<&'a BorrowedName>,
1853 all_directories: HashSet<&'a BorrowedName>,
1854 all_runners: HashSet<&'a BorrowedName>,
1855 all_resolvers: HashSet<&'a BorrowedName>,
1856 all_dictionaries: HashMap<&'a BorrowedName, &'a Capability>,
1857 all_configs: HashSet<&'a BorrowedName>,
1858 all_environment_names: HashSet<&'a BorrowedName>,
1859 all_capability_names: HashSet<&'a BorrowedName>,
1860}
1861
1862const TEST_FACET_KEY: &'static str = "fuchsia.test";
1864
1865const TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: &'static str = "deprecated-allowed-packages";
1867
1868const TEST_TYPE_FACET_KEY: &'static str = "type";
1870
1871impl<'a> ValidationContext<'a> {
1872 fn new(
1873 document: &'a Document,
1874 features: &'a FeatureSet,
1875 capability_requirements: &'a CapabilityRequirements<'a>,
1876 ) -> Self {
1877 ValidationContext {
1878 document,
1879 features,
1880 capability_requirements,
1881 all_children: HashMap::new(),
1882 all_collections: HashSet::new(),
1883 all_storages: HashMap::new(),
1884 all_services: HashSet::new(),
1885 all_protocols: HashSet::new(),
1886 all_directories: HashSet::new(),
1887 all_runners: HashSet::new(),
1888 all_resolvers: HashSet::new(),
1889 all_dictionaries: HashMap::new(),
1890 all_configs: HashSet::new(),
1891 all_environment_names: HashSet::new(),
1892 all_capability_names: HashSet::new(),
1893 }
1894 }
1895
1896 fn validate(&mut self) -> Result<(), Error> {
1897 let all_children_names =
1904 self.document.all_children_names().into_iter().zip(iter::repeat("children"));
1905 let all_collection_names =
1906 self.document.all_collection_names().into_iter().zip(iter::repeat("collections"));
1907 let all_storage_names =
1908 self.document.all_storage_names().into_iter().zip(iter::repeat("storage"));
1909 let all_runner_names =
1910 self.document.all_runner_names().into_iter().zip(iter::repeat("runners"));
1911 let all_resolver_names =
1912 self.document.all_resolver_names().into_iter().zip(iter::repeat("resolvers"));
1913 let all_environment_names =
1914 self.document.all_environment_names().into_iter().zip(iter::repeat("environments"));
1915 let all_dictionary_names =
1916 self.document.all_dictionary_names().into_iter().zip(iter::repeat("dictionaries"));
1917 ensure_no_duplicate_names(
1918 all_children_names
1919 .chain(all_collection_names)
1920 .chain(all_storage_names)
1921 .chain(all_runner_names)
1922 .chain(all_resolver_names)
1923 .chain(all_environment_names)
1924 .chain(all_dictionary_names),
1925 )?;
1926
1927 if let Some(children) = &self.document.children {
1929 self.all_children = children.iter().map(|c| (c.name.as_ref(), c)).collect();
1930 }
1931 self.all_collections = self.document.all_collection_names().into_iter().collect();
1932 self.all_storages = self.document.all_storage_with_sources();
1933 self.all_services = self.document.all_service_names().into_iter().collect();
1934 self.all_protocols = self.document.all_protocol_names().into_iter().collect();
1935 self.all_directories = self.document.all_directory_names().into_iter().collect();
1936 self.all_runners = self.document.all_runner_names().into_iter().collect();
1937 self.all_resolvers = self.document.all_resolver_names().into_iter().collect();
1938 self.all_dictionaries = self.document.all_dictionaries().into_iter().collect();
1939 self.all_configs = self.document.all_config_names().into_iter().collect();
1940 self.all_environment_names = self.document.all_environment_names().into_iter().collect();
1941 self.all_capability_names = self.document.all_capability_names();
1942
1943 if let Some(children) = &self.document.children {
1945 for child in children {
1946 self.validate_child(&child)?;
1947 }
1948 }
1949
1950 if let Some(collections) = &self.document.collections {
1952 for collection in collections {
1953 self.validate_collection(&collection)?;
1954 }
1955 }
1956
1957 if let Some(capabilities) = self.document.capabilities.as_ref() {
1959 let mut used_ids = HashMap::new();
1960 for capability in capabilities {
1961 self.validate_capability(capability, &mut used_ids)?;
1962 }
1963 }
1964
1965 let mut uses_runner = false;
1967 if let Some(uses) = self.document.r#use.as_ref() {
1968 let mut used_ids = HashMap::new();
1969 for use_ in uses.iter() {
1970 self.validate_use(&use_, &mut used_ids)?;
1971 if use_.runner.is_some() {
1972 uses_runner = true;
1973 }
1974 }
1975 }
1976
1977 if let Some(exposes) = self.document.expose.as_ref() {
1979 let mut used_ids = HashMap::new();
1980 let mut exposed_to_framework_ids = HashMap::new();
1981 for expose in exposes.iter() {
1982 self.validate_expose(&expose, &mut used_ids, &mut exposed_to_framework_ids)?;
1983 }
1984 }
1985
1986 if let Some(offers) = self.document.offer.as_ref() {
1988 let mut used_ids = HashMap::new();
1989
1990 let mut duplicate_check: HashSet<CapabilityId<'a>> = HashSet::new();
1991 let mut problem_protocols = Vec::new();
1992 let mut problem_dictionaries = Vec::new();
1993
1994 offers
1995 .iter()
1996 .filter(|o| matches!(o.to, OneOrMany::One(OfferToRef::All)))
1997 .try_for_each(|offer| -> Result<(), Error> {
1998 if offer.protocol.is_some() {
1999 for cap_id in CapabilityId::from_offer_expose(offer)? {
2000 if !duplicate_check.insert(cap_id.clone()) {
2001 problem_protocols.push(cap_id);
2002 }
2003 }
2004 }
2005
2006 if offer.dictionary.is_some() {
2007 for cap_id in CapabilityId::from_offer_expose(offer)? {
2008 if !duplicate_check.insert(cap_id.clone()) {
2009 problem_dictionaries.push(cap_id);
2010 }
2011 }
2012 }
2013 Ok(())
2014 })?;
2015
2016 if !problem_protocols.is_empty() {
2017 return Err(Error::validate(format!(
2018 r#"{} {:?} offered to "all" multiple times"#,
2019 "Protocol(s)",
2020 problem_protocols.iter().map(|p| format!("{p}")).collect::<Vec<_>>()
2021 )));
2022 }
2023
2024 if !problem_dictionaries.is_empty() {
2025 return Err(Error::validate(format!(
2026 r#"{} {:?} offered to "all" multiple times"#,
2027 "Dictionary(s)",
2028 problem_dictionaries.iter().map(|p| format!("{p}")).collect::<Vec<_>>()
2029 )));
2030 }
2031
2032 let offered_to_all = offers
2033 .iter()
2034 .filter(|o| matches!(o.to, OneOrMany::One(OfferToRef::All)))
2035 .filter(|o| o.protocol.is_some() || o.dictionary.is_some())
2036 .collect::<Vec<&Offer>>();
2037
2038 for offer in offers.iter() {
2039 self.validate_offer(&offer, &mut used_ids, &offered_to_all)?;
2040 }
2041 }
2042
2043 if uses_runner {
2044 self.validate_runner_not_specified(self.document.program.as_ref())?;
2047 } else {
2048 self.validate_runner_specified(self.document.program.as_ref())?;
2051 }
2052
2053 if let Some(environments) = &self.document.environments {
2055 for env in environments {
2056 self.validate_environment(&env)?;
2057 }
2058 }
2059
2060 self.validate_config(&self.document.config)?;
2062
2063 self.validate_required_offer_decls()?;
2065
2066 self.validate_required_use_decls()?;
2068
2069 self.validate_facets()?;
2070
2071 Ok(())
2072 }
2073
2074 fn get_test_facet(&self) -> Option<&serde_json::Value> {
2075 match &self.document.facets {
2076 Some(m) => m.get(TEST_FACET_KEY),
2077 None => None,
2078 }
2079 }
2080
2081 fn validate_facets(&self) -> Result<(), Error> {
2082 let test_facet_map = {
2083 let test_facet = self.get_test_facet();
2084 match &test_facet {
2085 None => None,
2086 Some(serde_json::Value::Object(m)) => Some(m),
2087 Some(facet) => {
2088 return Err(Error::validate(format!(
2089 "'{TEST_FACET_KEY}' is not an object: {facet:?}"
2090 )));
2091 }
2092 }
2093 };
2094
2095 let restrict_test_type = self.features.has(&Feature::RestrictTestTypeInFacet);
2096 let enable_allow_non_hermetic_packages =
2097 self.features.has(&Feature::EnableAllowNonHermeticPackagesFeature);
2098
2099 if restrict_test_type {
2100 let test_type = test_facet_map.map(|m| m.get(TEST_TYPE_FACET_KEY)).flatten();
2101 if test_type.is_some() {
2102 return Err(Error::validate(format!(
2103 "'{}' is not allowed in facets. Refer \
2104https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#non-hermetic_tests \
2105to run your test in the correct test realm.",
2106 TEST_TYPE_FACET_KEY
2107 )));
2108 }
2109 }
2110
2111 if enable_allow_non_hermetic_packages {
2112 let allow_non_hermetic_packages = self.features.has(&Feature::AllowNonHermeticPackages);
2113 let deprecated_allowed_packages = test_facet_map
2114 .map_or(false, |m| m.contains_key(TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY));
2115 if deprecated_allowed_packages && !allow_non_hermetic_packages {
2116 return Err(Error::validate(format!(
2117 "restricted_feature '{}' should be present with facet '{}'",
2118 Feature::AllowNonHermeticPackages,
2119 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
2120 )));
2121 }
2122 if allow_non_hermetic_packages && !deprecated_allowed_packages {
2123 return Err(Error::validate(format!(
2124 "Remove restricted_feature '{}' as manifest does not contain facet '{}'",
2125 Feature::AllowNonHermeticPackages,
2126 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY
2127 )));
2128 }
2129 }
2130 Ok(())
2131 }
2132
2133 fn validate_child(&mut self, child: &'a Child) -> Result<(), Error> {
2134 if let Some(resource) = child.url.resource() {
2135 if resource.ends_with(".cml") {
2136 return Err(Error::validate(format!(
2137 "child URL ends in .cml instead of .cm, \
2138which is almost certainly a mistake: {}",
2139 child.url
2140 )));
2141 }
2142 }
2143
2144 Ok(())
2145 }
2146
2147 fn validate_collection(&mut self, collection: &'a Collection) -> Result<(), Error> {
2148 if collection.allow_long_names.is_some() {
2149 self.features.check(Feature::AllowLongNames)?;
2150 }
2151 Ok(())
2152 }
2153
2154 fn validate_capability(
2155 &mut self,
2156 capability: &'a Capability,
2157 used_ids: &mut HashMap<String, CapabilityId<'a>>,
2158 ) -> Result<(), Error> {
2159 if capability.directory.is_some() && capability.path.is_none() {
2160 return Err(Error::validate("\"path\" should be present with \"directory\""));
2161 }
2162 if capability.directory.is_some() && capability.rights.is_none() {
2163 return Err(Error::validate("\"rights\" should be present with \"directory\""));
2164 }
2165 if capability.storage.as_ref().is_some() {
2166 if capability.from.is_none() {
2167 return Err(Error::validate("\"from\" should be present with \"storage\""));
2168 }
2169 if capability.path.is_some() {
2170 return Err(Error::validate(
2171 "\"path\" cannot be present with \"storage\", use \"backing_dir\"",
2172 ));
2173 }
2174 if capability.backing_dir.is_none() {
2175 return Err(Error::validate("\"backing_dir\" should be present with \"storage\""));
2176 }
2177 if capability.storage_id.is_none() {
2178 return Err(Error::validate("\"storage_id\" should be present with \"storage\""));
2179 }
2180 }
2181 if capability.runner.is_some() && capability.from.is_some() {
2182 return Err(Error::validate("\"from\" should not be present with \"runner\""));
2183 }
2184 if capability.runner.is_some() && capability.path.is_none() {
2185 return Err(Error::validate("\"path\" should be present with \"runner\""));
2186 }
2187 if capability.resolver.is_some() && capability.from.is_some() {
2188 return Err(Error::validate("\"from\" should not be present with \"resolver\""));
2189 }
2190 if capability.resolver.is_some() && capability.path.is_none() {
2191 return Err(Error::validate("\"path\" should be present with \"resolver\""));
2192 }
2193
2194 if capability.dictionary.as_ref().is_some() && capability.path.is_some() {
2195 self.features.check(Feature::DynamicDictionaries)?;
2196 }
2197 if capability.delivery.is_some() {
2198 self.features.check(Feature::DeliveryType)?;
2199 }
2200 if let Some(from) = capability.from.as_ref() {
2201 self.validate_component_child_ref("\"capabilities\" source", &AnyRef::from(from))?;
2202 }
2203
2204 let capability_ids = CapabilityId::from_capability(capability)?;
2206 for capability_id in capability_ids {
2207 if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
2208 return Err(Error::validate(format!(
2209 "\"{}\" is a duplicate \"capability\" name",
2210 capability_id,
2211 )));
2212 }
2213 }
2214
2215 Ok(())
2216 }
2217
2218 fn validate_use(
2219 &mut self,
2220 use_: &'a Use,
2221 used_ids: &mut HashMap<String, CapabilityId<'a>>,
2222 ) -> Result<(), Error> {
2223 use_.capability_type()?;
2224 for checker in [
2225 self.service_from_self_checker(use_),
2226 self.protocol_from_self_checker(use_),
2227 self.directory_from_self_checker(use_),
2228 self.config_from_self_checker(use_),
2229 ] {
2230 checker.validate("used")?;
2231 }
2232
2233 if use_.from == Some(UseFromRef::Debug) && use_.protocol.is_none() {
2234 return Err(Error::validate("only \"protocol\" supports source from \"debug\""));
2235 }
2236 if use_.event_stream.is_some() && use_.availability.is_some() {
2237 return Err(Error::validate("\"availability\" cannot be used with \"event_stream\""));
2238 }
2239 if use_.event_stream.is_none() && use_.filter.is_some() {
2240 return Err(Error::validate("\"filter\" can only be used with \"event_stream\""));
2241 }
2242 if use_.storage.is_some() && use_.from.is_some() {
2243 return Err(Error::validate("\"from\" cannot be used with \"storage\""));
2244 }
2245 if use_.runner.is_some() && use_.availability.is_some() {
2246 return Err(Error::validate("\"availability\" cannot be used with \"runner\""));
2247 }
2248 if use_.from == Some(UseFromRef::Self_) && use_.event_stream.is_some() {
2249 return Err(Error::validate("\"from: self\" cannot be used with \"event_stream\""));
2250 }
2251 if use_.from == Some(UseFromRef::Self_) && use_.runner.is_some() {
2252 return Err(Error::validate("\"from: self\" cannot be used with \"runner\""));
2253 }
2254 if use_.availability == Some(Availability::SameAsTarget) {
2255 return Err(Error::validate(
2256 "\"availability: same_as_target\" cannot be used with use declarations",
2257 ));
2258 }
2259 if use_.dictionary.is_some() {
2260 self.features.check(Feature::UseDictionaries)?;
2261 }
2262 if let Some(UseFromRef::Dictionary(_)) = use_.from.as_ref() {
2263 if use_.storage.is_some() {
2264 return Err(Error::validate(
2265 "Dictionaries do not support \"storage\" capabilities",
2266 ));
2267 }
2268 if use_.event_stream.is_some() {
2269 return Err(Error::validate(
2270 "Dictionaries do not support \"event_stream\" capabilities",
2271 ));
2272 }
2273 }
2274 if let Some(config) = use_.config.as_ref() {
2275 if use_.key == None {
2276 return Err(Error::validate(format!("Config '{}' missing field 'key'", config)));
2277 }
2278 let _ = use_config_to_value_type(use_)?;
2279 let availability = use_.availability.unwrap_or(Availability::Required);
2280 if availability == Availability::Required && use_.config_default.is_some() {
2281 return Err(Error::validate(format!(
2282 "Config '{}' is required and has a default value",
2283 config
2284 )));
2285 }
2286 }
2287 if use_.numbered_handle.as_ref().is_some() {
2288 if use_.protocol.is_some() {
2289 if use_.path.is_some() {
2290 return Err(Error::validate(format!(
2291 "`path` and `numbered_handle` are incompatible"
2292 )));
2293 }
2294 } else {
2295 return Err(Error::validate(format!(
2296 "`numbered_handle` is only supported for `use protocol`"
2297 )));
2298 }
2299 }
2300
2301 let capability_ids = CapabilityId::from_use(use_)?;
2303 for capability_id in capability_ids {
2304 if let Some(conflicting_capability_id) =
2305 used_ids.insert(capability_id.to_string(), capability_id.clone())
2306 {
2307 if let (CapabilityId::UsedDictionary(_), CapabilityId::UsedDictionary(_)) =
2308 (&capability_id, &conflicting_capability_id)
2309 {
2310 } else {
2313 return Err(Error::validate(format!(
2314 "\"{}\" is a duplicate \"use\" target {}",
2315 capability_id,
2316 capability_id.type_str()
2317 )));
2318 }
2319 }
2320 let dir = capability_id.get_dir_path();
2321
2322 let pkg_path = cm_types::NamespacePath::new("/pkg").unwrap();
2324 if let Some(ref dir) = dir {
2325 if dir.has_prefix(&pkg_path) {
2326 return Err(Error::validate(format!(
2327 "{} \"{}\" conflicts with the protected path \"/pkg\", please use this capability with a different path",
2328 capability_id.type_str(),
2329 capability_id,
2330 )));
2331 }
2332 }
2333
2334 for (_, used_id) in used_ids.iter() {
2337 if capability_id == *used_id {
2338 continue;
2339 }
2340 let Some(ref path_b) = capability_id.get_target_path() else {
2341 continue;
2342 };
2343 let Some(path_a) = used_id.get_target_path() else {
2344 continue;
2345 };
2346 #[derive(Debug, Clone, Copy)]
2347 enum NodeType {
2348 Service,
2349 Directory,
2350 #[allow(unused)]
2353 Dictionary,
2354 }
2355 fn capability_id_to_type(id: &CapabilityId<'_>) -> Option<NodeType> {
2356 match id {
2357 CapabilityId::UsedConfiguration(_) => None,
2358 #[cfg(fuchsia_api_level_at_least = "30")]
2359 CapabilityId::UsedDictionary(_) => Some(NodeType::Dictionary),
2360 CapabilityId::UsedDirectory(_) => Some(NodeType::Directory),
2361 CapabilityId::UsedEventStream(_) => Some(NodeType::Service),
2362 CapabilityId::UsedProtocol(_) => Some(NodeType::Service),
2363 #[cfg(fuchsia_api_level_at_least = "HEAD")]
2364 CapabilityId::UsedRunner(_) => None,
2365 CapabilityId::UsedService(_) => Some(NodeType::Directory),
2366 CapabilityId::UsedStorage(_) => Some(NodeType::Directory),
2367 _ => None,
2368 }
2369 }
2370 let Some(type_a) = capability_id_to_type(&used_id) else {
2371 continue;
2372 };
2373 let Some(type_b) = capability_id_to_type(&capability_id) else {
2374 continue;
2375 };
2376 let mut conflicts = false;
2377 match (type_a, type_b) {
2378 (NodeType::Service, NodeType::Service)
2379 | (NodeType::Directory, NodeType::Service)
2380 | (NodeType::Service, NodeType::Directory)
2381 | (NodeType::Directory, NodeType::Directory) => {
2382 if path_a.has_prefix(&path_b) || path_b.has_prefix(&path_a) {
2383 conflicts = true;
2384 }
2385 }
2386 (NodeType::Dictionary, NodeType::Service)
2387 | (NodeType::Dictionary, NodeType::Directory) => {
2388 if path_a.has_prefix(&path_b) {
2389 conflicts = true;
2390 }
2391 }
2392 (NodeType::Service, NodeType::Dictionary)
2393 | (NodeType::Directory, NodeType::Dictionary) => {
2394 if path_b.has_prefix(&path_a) {
2395 conflicts = true;
2396 }
2397 }
2398 (NodeType::Dictionary, NodeType::Dictionary) => {
2399 }
2401 }
2402 if conflicts {
2403 return Err(Error::validate(format!(
2404 "{} \"{}\" is a prefix of \"use\" target {} \"{}\"",
2405 used_id.type_str(),
2406 used_id,
2407 capability_id.type_str(),
2408 capability_id,
2409 )));
2410 }
2411 }
2412 }
2413
2414 if let Some(_) = use_.directory.as_ref() {
2415 match &use_.rights {
2417 Some(rights) => self.validate_directory_rights(&rights)?,
2418 None => {
2419 return Err(Error::validate(
2420 "This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer.",
2421 ));
2422 }
2423 };
2424 }
2425
2426 match (&use_.from, &use_.dependency) {
2427 (Some(UseFromRef::Named(name)), _) if use_.service.is_some() => {
2428 self.validate_component_child_or_collection_ref(
2429 "\"use\" source",
2430 &AnyRef::Named(name),
2431 )?;
2432 }
2433 (Some(UseFromRef::Named(name)), _) => {
2434 self.validate_component_child_or_capability_ref(
2435 "\"use\" source",
2436 &AnyRef::Named(name),
2437 )?;
2438 }
2439 (
2440 Some(UseFromRef::Dictionary(DictionaryRef {
2441 path: _,
2442 root: RootDictionaryRef::Named(name),
2443 })),
2444 _,
2445 ) => {
2446 self.validate_component_child_or_capability_ref(
2447 "\"use\" source",
2448 &AnyRef::Named(name),
2449 )?;
2450 }
2451 (_, Some(DependencyType::Weak)) => {
2452 return Err(Error::validate(format!(
2453 "Only `use` from children can have dependency: \"weak\""
2454 )));
2455 }
2456 _ => {}
2457 }
2458 Ok(())
2459 }
2460
2461 fn validate_expose(
2462 &self,
2463 expose: &'a Expose,
2464 used_ids: &mut HashMap<String, CapabilityId<'a>>,
2465 exposed_to_framework_ids: &mut HashMap<String, CapabilityId<'a>>,
2466 ) -> Result<(), Error> {
2467 expose.capability_type()?;
2468 for checker in [
2469 self.service_from_self_checker(expose),
2470 self.protocol_from_self_checker(expose),
2471 self.directory_from_self_checker(expose),
2472 self.runner_from_self_checker(expose),
2473 self.resolver_from_self_checker(expose),
2474 self.dictionary_from_self_checker(expose),
2475 self.config_from_self_checker(expose),
2476 ] {
2477 checker.validate("exposed")?;
2478 }
2479
2480 if let Some(_) = expose.directory.as_ref() {
2482 if expose.from.iter().any(|r| *r == ExposeFromRef::Self_) || expose.rights.is_some() {
2483 if let Some(rights) = expose.rights.as_ref() {
2484 self.validate_directory_rights(&rights)?;
2485 }
2486 }
2487
2488 if expose.to == Some(ExposeToRef::Framework) {
2491 if expose.subdir.is_some() {
2492 return Err(Error::validate(
2493 "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead.",
2494 ));
2495 }
2496 }
2497 }
2498
2499 if let Some(event_stream) = &expose.event_stream {
2500 if event_stream.iter().len() > 1 && expose.r#as.is_some() {
2501 return Err(Error::validate(format!(
2502 "as cannot be used with multiple event streams"
2503 )));
2504 }
2505 if let Some(ExposeToRef::Framework) = &expose.to {
2506 return Err(Error::validate(format!("cannot expose an event_stream to framework")));
2507 }
2508 for from in expose.from.iter() {
2509 if from == &ExposeFromRef::Self_ {
2510 return Err(Error::validate(format!("Cannot expose event_streams from self")));
2511 }
2512 }
2513 if let Some(scopes) = &expose.scope {
2514 for scope in scopes {
2515 match scope {
2516 EventScope::Named(name) => {
2517 if !self.all_children.contains_key(&name.as_ref())
2518 && !self.all_collections.contains(&name.as_ref())
2519 {
2520 return Err(Error::validate(format!(
2521 "event_stream scope {} did not match a component or collection in this .cml file.",
2522 name.as_str()
2523 )));
2524 }
2525 }
2526 }
2527 }
2528 }
2529 }
2530
2531 for ref_ in expose.from.iter() {
2532 if let ExposeFromRef::Dictionary(d) = ref_ {
2533 if expose.event_stream.is_some() {
2534 return Err(Error::validate(
2535 "Dictionaries do not support \"event_stream\" capabilities",
2536 ));
2537 }
2538 match &d.root {
2539 RootDictionaryRef::Self_ | RootDictionaryRef::Named(_) => {}
2540 RootDictionaryRef::Parent => {
2541 return Err(Error::validate(
2542 "`expose` dictionary path must begin with `self` or `#<child-name>`",
2543 ));
2544 }
2545 }
2546 }
2547 }
2548
2549 let capability_ids = CapabilityId::from_offer_expose(expose)?;
2551 for capability_id in capability_ids {
2552 let mut ids = &mut *used_ids;
2553 if expose.to == Some(ExposeToRef::Framework) {
2554 ids = &mut *exposed_to_framework_ids;
2555 }
2556 if ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
2557 if let CapabilityId::Service(_) = capability_id {
2558 } else {
2560 return Err(Error::validate(format!(
2561 "\"{}\" is a duplicate \"expose\" target capability for \"{}\"",
2562 capability_id,
2563 expose.to.as_ref().unwrap_or(&ExposeToRef::Parent)
2564 )));
2565 }
2566 }
2567 }
2568
2569 self.validate_from_clause(
2572 "expose",
2573 expose,
2574 &expose.source_availability,
2575 &expose.availability,
2576 )?;
2577
2578 Ok(())
2579 }
2580
2581 fn validate_offer(
2582 &mut self,
2583 offer: &'a Offer,
2584 used_ids: &mut HashMap<Name, HashMap<String, CapabilityId<'a>>>,
2585 protocols_offered_to_all: &[&'a Offer],
2586 ) -> Result<(), Error> {
2587 offer.capability_type()?;
2588 for checker in [
2589 self.service_from_self_checker(offer),
2590 self.protocol_from_self_checker(offer),
2591 self.directory_from_self_checker(offer),
2592 self.storage_from_self_checker(offer),
2593 self.runner_from_self_checker(offer),
2594 self.resolver_from_self_checker(offer),
2595 self.dictionary_from_self_checker(offer),
2596 self.config_from_self_checker(offer),
2597 ] {
2598 checker.validate("offered")?;
2599 }
2600
2601 if let Some(stream) = offer.event_stream.as_ref() {
2602 if stream.iter().len() > 1 && offer.r#as.is_some() {
2603 return Err(Error::validate(format!("as cannot be used with multiple events")));
2604 }
2605 for from in &offer.from {
2606 match from {
2607 OfferFromRef::Self_ => {
2608 return Err(Error::validate(format!(
2609 "cannot offer an event_stream from self"
2610 )));
2611 }
2612 _ => {}
2613 }
2614 }
2615 }
2616
2617 if let Some(_) = offer.directory.as_ref() {
2619 if offer.from.iter().any(|r| *r == OfferFromRef::Self_) || offer.rights.is_some() {
2620 if let Some(rights) = offer.rights.as_ref() {
2621 self.validate_directory_rights(&rights)?;
2622 }
2623 }
2624 }
2625
2626 if let Some(storage) = offer.storage.as_ref() {
2627 for storage in storage {
2628 if offer.from.iter().any(|r| r.is_named()) {
2629 return Err(Error::validate(format!(
2630 "Storage \"{}\" is offered from a child, but storage capabilities cannot be exposed",
2631 storage
2632 )));
2633 }
2634 }
2635 }
2636
2637 for ref_ in offer.from.iter() {
2638 if let OfferFromRef::Dictionary(d) = ref_ {
2639 match &d.root {
2640 RootDictionaryRef::Self_
2641 | RootDictionaryRef::Named(_)
2642 | RootDictionaryRef::Parent => {}
2643 }
2644
2645 if offer.storage.is_some() {
2646 return Err(Error::validate(
2647 "Dictionaries do not support \"storage\" capabilities",
2648 ));
2649 }
2650 if offer.event_stream.is_some() {
2651 return Err(Error::validate(
2652 "Dictionaries do not support \"event_stream\" capabilities",
2653 ));
2654 }
2655 }
2656 }
2657
2658 if !offer_can_have_dependency_no_span(offer) && offer.dependency.is_some() {
2660 return Err(Error::validate(
2661 "Dependency can only be provided for protocol, directory, and service capabilities",
2662 ));
2663 }
2664
2665 let target_cap_ids = CapabilityId::from_offer_expose(offer)?;
2667 for to in &offer.to {
2668 let to_target = match to {
2670 OfferToRef::All => continue,
2671 OfferToRef::Named(to_target) => {
2672 for offer_to_all in protocols_offered_to_all {
2675 offer_to_all_would_duplicate(offer_to_all, offer, to_target)?;
2676 }
2677
2678 if offer.target_availability == Some(TargetAvailability::Unknown)
2681 || self.all_children.contains_key(&to_target.as_ref())
2682 || self.all_collections.contains(&to_target.as_ref())
2683 {
2684 } else {
2686 if let OneOrMany::One(from) = &offer.from {
2687 return Err(Error::validate(format!(
2688 "\"{to}\" is an \"offer\" target from \"{from}\" but \"{to}\" does \
2689 not appear in \"children\" or \"collections\"",
2690 )));
2691 } else {
2692 return Err(Error::validate(format!(
2693 "\"{to}\" is an \"offer\" target but \"{to}\" does not appear in \
2694 \"children\" or \"collections\"",
2695 )));
2696 }
2697 }
2698
2699 if let Some(storage) = offer.storage.as_ref() {
2701 for storage in storage {
2702 if let OneOrMany::One(OfferFromRef::Self_) = &offer.from {
2705 if let Some(CapabilityFromRef::Named(source)) =
2706 self.all_storages.get(&storage.as_ref())
2707 {
2708 if to_target == source {
2709 return Err(Error::validate(format!(
2710 "Storage offer target \"{}\" is same as source",
2711 to
2712 )));
2713 }
2714 }
2715 }
2716 }
2717 } else {
2718 for reference in offer.from.iter() {
2719 if offer_dependency_no_span(offer) == DependencyType::Weak {
2721 continue;
2722 }
2723 match reference {
2724 OfferFromRef::Named(name) if name == to_target => {
2725 return Err(Error::validate(format!(
2726 "Offer target \"{}\" is same as source",
2727 to
2728 )));
2729 }
2730 _ => {}
2731 }
2732 }
2733 }
2734 to_target
2735 }
2736 OfferToRef::OwnDictionary(to_target) => {
2737 if let Ok(capability_ids) = CapabilityId::from_offer_expose(offer) {
2738 for id in capability_ids {
2739 match &id {
2740 CapabilityId::Protocol(_)
2741 | CapabilityId::Dictionary(_)
2742 | CapabilityId::Directory(_)
2743 | CapabilityId::Runner(_)
2744 | CapabilityId::Resolver(_)
2745 | CapabilityId::Service(_)
2746 | CapabilityId::Configuration(_) => {}
2747 CapabilityId::Storage(_) | CapabilityId::EventStream(_) => {
2748 let type_name = id.type_str();
2749 return Err(Error::validate(format!(
2750 "\"offer\" to dictionary \"{to}\" for \"{type_name}\" but \
2751 dictionaries do not support this type yet."
2752 )));
2753 }
2754 CapabilityId::UsedService(_)
2755 | CapabilityId::UsedProtocol(_)
2756 | CapabilityId::UsedProtocolNumberedHandle(_)
2757 | CapabilityId::UsedDirectory(_)
2758 | CapabilityId::UsedStorage(_)
2759 | CapabilityId::UsedEventStream(_)
2760 | CapabilityId::UsedRunner(_)
2761 | CapabilityId::UsedConfiguration(_)
2762 | CapabilityId::UsedDictionary(_) => {
2763 unreachable!("this is not a use")
2764 }
2765 }
2766 }
2767 }
2768 match self.all_dictionaries.get(&to_target.as_ref()) {
2770 Some(d) => {
2771 if d.path.is_some() {
2772 return Err(Error::validate(format!(
2773 "\"offer\" has dictionary target \"{to}\" but \"{to_target}\" \
2774 sets \"path\". Therefore, it is a dynamic dictionary that \
2775 does not allow offers into it."
2776 )));
2777 }
2778 }
2779 None => {
2780 if offer.target_availability != Some(TargetAvailability::Unknown) {
2781 return Err(Error::validate(format!(
2782 "\"offer\" has dictionary target \"{to}\" but \"{to_target}\" \
2783 is not a dictionary capability defined by this component"
2784 )));
2785 }
2786 }
2787 }
2788 to_target
2789 }
2790 };
2791
2792 let ids_for_entity = used_ids.entry(to_target.clone()).or_insert(HashMap::new());
2794 for target_cap_id in &target_cap_ids {
2795 if ids_for_entity.insert(target_cap_id.to_string(), target_cap_id.clone()).is_some()
2796 {
2797 if let CapabilityId::Service(_) = target_cap_id {
2798 } else {
2800 return Err(Error::validate(format!(
2801 "\"{}\" is a duplicate \"offer\" target capability for \"{}\"",
2802 target_cap_id, to
2803 )));
2804 }
2805 }
2806 }
2807 }
2808
2809 self.validate_from_clause("offer", offer, &offer.source_availability, &offer.availability)?;
2812
2813 Ok(())
2814 }
2815
2816 fn validate_required_offer_decls(&self) -> Result<(), Error> {
2817 let children_stub = Vec::new();
2818 let children = self.document.children.as_ref().unwrap_or(&children_stub);
2819 let collections_stub = Vec::new();
2820 let collections = self.document.collections.as_ref().unwrap_or(&collections_stub);
2821 let offers_stub = Vec::new();
2822 let offers = self.document.offer.as_ref().unwrap_or(&offers_stub);
2823
2824 for required_offer in self.capability_requirements.must_offer {
2825 for child in children.iter() {
2830 if !offers
2831 .iter()
2832 .any(|offer| Self::has_required_offer(offer, &child.name, required_offer))
2833 {
2834 let capability_type = required_offer.offer_type();
2835 return Err(Error::validate(format!(
2836 r#"{capability_type} "{}" is not offered to child component "{}" but it is a required offer"#,
2837 required_offer.name(),
2838 child.name
2839 )));
2840 }
2841 }
2842
2843 for collection in collections.iter() {
2844 if !offers
2845 .iter()
2846 .any(|offer| Self::has_required_offer(offer, &collection.name, required_offer))
2847 {
2848 let capability_type = required_offer.offer_type();
2849 return Err(Error::validate(format!(
2850 r#"{capability_type} "{}" is not offered to collection "{}" but it is a required offer"#,
2851 required_offer.name(),
2852 collection.name
2853 )));
2854 }
2855 }
2856 }
2857
2858 Ok(())
2859 }
2860
2861 fn has_required_offer(
2862 offer: &Offer,
2863 target_name: &BorrowedName,
2864 required_offer: &OfferToAllCapability<'_>,
2865 ) -> bool {
2866 let names_this_collection = offer.to.iter().any(|target| match target {
2867 OfferToRef::Named(name) => **name == *target_name,
2868 OfferToRef::All => true,
2869 OfferToRef::OwnDictionary(_) => false,
2870 });
2871 let capability_names = match required_offer {
2872 OfferToAllCapability::Dictionary(_) => offer.dictionary.as_ref(),
2873 OfferToAllCapability::Protocol(_) => offer.protocol.as_ref(),
2874 };
2875 let names_this_capability = match capability_names.as_ref() {
2876 Some(OneOrMany::Many(names)) => {
2877 names.iter().any(|cap_name| cap_name.as_str() == required_offer.name())
2878 }
2879 Some(OneOrMany::One(name)) => {
2880 let cap_name = offer.r#as.as_ref().unwrap_or(name);
2881 cap_name.as_str() == required_offer.name()
2882 }
2883 None => false,
2884 };
2885 names_this_collection && names_this_capability
2886 }
2887
2888 fn validate_required_use_decls(&self) -> Result<(), Error> {
2889 let use_decls_stub = Vec::new();
2890 let use_decls = self.document.r#use.as_ref().unwrap_or(&use_decls_stub);
2891
2892 for required_usage in self.capability_requirements.must_use {
2893 if !use_decls.iter().any(|usage| match usage.protocol.as_ref() {
2894 None => false,
2895 Some(protocol) => protocol
2896 .iter()
2897 .any(|protocol_name| protocol_name.as_str() == required_usage.name()),
2898 }) {
2899 return Err(Error::validate(format!(
2900 r#"Protocol "{}" is not used by a component but is required by all"#,
2901 required_usage.name(),
2902 )));
2903 }
2904 }
2905
2906 Ok(())
2907 }
2908
2909 fn validate_from_clause<T>(
2918 &self,
2919 verb: &str,
2920 cap: &T,
2921 source_availability: &Option<SourceAvailability>,
2922 availability: &Option<Availability>,
2923 ) -> Result<(), Error>
2924 where
2925 T: CapabilityClause + FromClause,
2926 {
2927 let from = cap.from_();
2928 if cap.service().is_none() && from.is_many() {
2929 return Err(Error::validate(format!(
2930 "\"{}\" capabilities cannot have multiple \"from\" clauses",
2931 cap.capability_type().unwrap()
2932 )));
2933 }
2934
2935 if from.is_many() {
2936 ensure_no_duplicate_values(&cap.from_())?;
2937 }
2938
2939 let reference_description = format!("\"{}\" source", verb);
2940 for from_clause in from {
2941 let ref_validity_res = if cap.protocol().is_some() {
2944 self.validate_component_child_or_capability_ref(
2945 &reference_description,
2946 &from_clause,
2947 )
2948 } else if cap.service().is_some() {
2949 self.validate_component_child_or_collection_ref(
2951 &reference_description,
2952 &from_clause,
2953 )
2954 } else {
2955 self.validate_component_child_ref(&reference_description, &from_clause)
2956 };
2957
2958 match ref_validity_res {
2959 Ok(()) if from_clause == AnyRef::Void => {
2960 if availability != &Some(Availability::Optional) {
2962 return Err(Error::validate(format!(
2963 "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"{}\", from: \"{}\"",
2964 cap.names().iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", "),
2965 cap.from_(),
2966 )));
2967 }
2968 }
2969 Ok(()) => {
2970 }
2972 Err(_) if source_availability == &Some(SourceAvailability::Unknown) => {
2973 if availability != &Some(Availability::Optional) && availability != &None {
2975 return Err(Error::validate(format!(
2976 "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\", capabilities: \"{}\", from: \"{}\"",
2977 cap.names().iter().map(|n| n.as_str()).collect::<Vec<_>>().join(", "),
2978 cap.from_(),
2979 )));
2980 }
2981 }
2982 Err(e) => {
2983 return Err(e);
2985 }
2986 }
2987 }
2988 Ok(())
2989 }
2990
2991 fn validate_component_child_ref(
2998 &self,
2999 reference_description: &str,
3000 component_ref: &AnyRef<'_>,
3001 ) -> Result<(), Error> {
3002 match component_ref {
3003 AnyRef::Named(name) => {
3004 if !self.all_children.contains_key(name) {
3006 return Err(Error::validate(format!(
3007 "{} \"{}\" does not appear in \"children\"",
3008 reference_description, component_ref
3009 )));
3010 }
3011 Ok(())
3012 }
3013 _ => Ok(()),
3015 }
3016 }
3017
3018 fn validate_component_child_or_collection_ref(
3025 &self,
3026 reference_description: &str,
3027 component_ref: &AnyRef<'_>,
3028 ) -> Result<(), Error> {
3029 match component_ref {
3030 AnyRef::Named(name) => {
3031 if !self.all_children.contains_key(name) && !self.all_collections.contains(name) {
3033 return Err(Error::validate(format!(
3034 "{} \"{}\" does not appear in \"children\" or \"collections\"",
3035 reference_description, component_ref
3036 )));
3037 }
3038 Ok(())
3039 }
3040 _ => Ok(()),
3042 }
3043 }
3044
3045 fn validate_component_capability_ref(
3052 &self,
3053 reference_description: &str,
3054 capability_ref: &AnyRef<'_>,
3055 ) -> Result<(), Error> {
3056 match capability_ref {
3057 AnyRef::Named(name) => {
3058 if !self.all_capability_names.contains(name) {
3059 return Err(Error::validate(format!(
3060 "{} \"{}\" does not appear in \"capabilities\"",
3061 reference_description, capability_ref
3062 )));
3063 }
3064 Ok(())
3065 }
3066 _ => Ok(()),
3067 }
3068 }
3069
3070 fn validate_component_child_or_capability_ref(
3077 &self,
3078 reference_description: &str,
3079 ref_: &AnyRef<'_>,
3080 ) -> Result<(), Error> {
3081 if self.validate_component_child_ref(reference_description, ref_).is_err()
3082 && self.validate_component_capability_ref(reference_description, ref_).is_err()
3083 {
3084 return Err(Error::validate(format!(
3085 "{} \"{}\" does not appear in \"children\" or \"capabilities\"",
3086 reference_description, ref_
3087 )));
3088 }
3089 Ok(())
3090 }
3091
3092 fn validate_directory_rights(&self, rights_clause: &Rights) -> Result<(), Error> {
3095 let mut rights = HashSet::new();
3096 for right_token in rights_clause.0.iter() {
3097 for right in right_token.expand() {
3098 if !rights.insert(right) {
3099 return Err(Error::validate(format!(
3100 "\"{}\" is duplicated in the rights clause.",
3101 right_token
3102 )));
3103 }
3104 }
3105 }
3106 Ok(())
3107 }
3108
3109 fn validate_runner_specified(&self, program: Option<&Program>) -> Result<(), Error> {
3112 match program {
3113 Some(program) => match program.runner {
3114 Some(_) => Ok(()),
3115 None => {
3116 return Err(Error::validate(
3117 "Component has a `program` block defined, but doesn't specify a `runner`. \
3118 Components need to use a runner to actually execute code.",
3119 ));
3120 }
3121 },
3122 None => Ok(()),
3123 }
3124 }
3125
3126 fn validate_runner_not_specified(&self, program: Option<&Program>) -> Result<(), Error> {
3129 match program {
3130 Some(program) => match program.runner {
3131 Some(_) => {
3132 return Err(Error::validate(
3135 "Component has conflicting runners in `program` block and `use` block.",
3136 ));
3137 }
3138 None => Ok(()),
3139 },
3140 None => Ok(()),
3141 }
3142 }
3143
3144 fn validate_config(
3145 &self,
3146 fields: &Option<BTreeMap<ConfigKey, ConfigValueType>>,
3147 ) -> Result<(), Error> {
3148 let optional_use_keys: BTreeMap<ConfigKey, ConfigValueType> = self
3151 .document
3152 .r#use
3153 .iter()
3154 .flatten()
3155 .map(|u| {
3156 if u.config == None {
3157 return None;
3158 }
3159 if u.availability == Some(Availability::Required) || u.availability == None {
3160 return None;
3161 }
3162 if let Some(_) = u.config_default.as_ref() {
3163 return None;
3164 }
3165 let key = ConfigKey(u.key.clone().expect("key should be set").into());
3166 let value = use_config_to_value_type(u).expect("config type should be valid");
3167 Some((key, value))
3168 })
3169 .flatten()
3170 .collect();
3171
3172 let Some(fields) = fields else {
3173 if !optional_use_keys.is_empty() {
3174 return Err(Error::validate(
3175 "Optionally using a config capability without a default requires a matching 'config' section.",
3176 ));
3177 }
3178 return Ok(());
3179 };
3180
3181 if fields.is_empty() {
3182 return Err(Error::validate("'config' section is empty"));
3183 }
3184
3185 for (key, value) in optional_use_keys {
3186 if !fields.contains_key(&key) {
3187 return Err(Error::validate(format!(
3188 "'config' section must contain key for optional use '{}'",
3189 key
3190 )));
3191 }
3192 if fields.get(&key) != Some(&value) {
3193 return Err(Error::validate(format!(
3194 "Use and config block differ on type for key '{}'",
3195 key
3196 )));
3197 }
3198 }
3199
3200 Ok(())
3201 }
3202
3203 fn validate_environment(&mut self, environment: &'a Environment) -> Result<(), Error> {
3204 match &environment.extends {
3205 Some(EnvironmentExtends::None) => {
3206 if environment.stop_timeout_ms.is_none() {
3207 return Err(Error::validate(
3208 "'__stop_timeout_ms' must be provided if the environment does not extend \
3209 another environment",
3210 ));
3211 }
3212 }
3213 Some(EnvironmentExtends::Realm) | None => {}
3214 }
3215
3216 if let Some(runners) = &environment.runners {
3217 let mut used_names = HashMap::new();
3218 for registration in runners {
3219 let name = registration.r#as.as_ref().unwrap_or(®istration.runner);
3221 if let Some(previous_runner) = used_names.insert(name, ®istration.runner) {
3222 return Err(Error::validate(format!(
3223 "Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".",
3224 name, ®istration.runner, previous_runner
3225 )));
3226 }
3227
3228 if registration.from == RegistrationRef::Self_
3230 && !self.all_runners.contains(®istration.runner.as_ref())
3231 {
3232 return Err(Error::validate(format!(
3233 "Runner \"{}\" registered in environment is not in \"runners\"",
3234 ®istration.runner,
3235 )));
3236 }
3237
3238 self.validate_component_child_ref(
3239 &format!("\"{}\" runner source", ®istration.runner),
3240 &AnyRef::from(®istration.from),
3241 )?;
3242 }
3243 }
3244
3245 if let Some(resolvers) = &environment.resolvers {
3246 let mut used_schemes = HashMap::new();
3247 for registration in resolvers {
3248 if let Some(previous_resolver) =
3250 used_schemes.insert(®istration.scheme, ®istration.resolver)
3251 {
3252 return Err(Error::validate(format!(
3253 "scheme \"{}\" for resolver \"{}\" is already registered; \
3254 previously registered to resolver \"{}\".",
3255 ®istration.scheme, ®istration.resolver, previous_resolver
3256 )));
3257 }
3258
3259 self.validate_component_child_ref(
3260 &format!("\"{}\" resolver source", ®istration.resolver),
3261 &AnyRef::from(®istration.from),
3262 )?;
3263 }
3264 }
3265
3266 if let Some(debug_capabilities) = &environment.debug {
3267 for debug in debug_capabilities {
3268 self.protocol_from_self_checker(debug).validate("registered as debug")?;
3269 self.validate_from_clause("debug", debug, &None, &None)?;
3270 }
3271 }
3272 Ok(())
3273 }
3274
3275 fn service_from_self_checker<'b>(
3276 &'b self,
3277 input: &'b (impl CapabilityClause + FromClause),
3278 ) -> RouteFromSelfChecker<'b> {
3279 RouteFromSelfChecker {
3280 capability_name: input.service(),
3281 from: input.from_(),
3282 container: &self.all_services,
3283 all_dictionaries: &self.all_dictionaries,
3284 typename: "service",
3285 }
3286 }
3287
3288 fn protocol_from_self_checker<'b>(
3289 &'b self,
3290 input: &'b (impl CapabilityClause + FromClause),
3291 ) -> RouteFromSelfChecker<'b> {
3292 RouteFromSelfChecker {
3293 capability_name: input.protocol(),
3294 from: input.from_(),
3295 container: &self.all_protocols,
3296 all_dictionaries: &self.all_dictionaries,
3297 typename: "protocol",
3298 }
3299 }
3300
3301 fn directory_from_self_checker<'b>(
3302 &'b self,
3303 input: &'b (impl CapabilityClause + FromClause),
3304 ) -> RouteFromSelfChecker<'b> {
3305 RouteFromSelfChecker {
3306 capability_name: input.directory(),
3307 from: input.from_(),
3308 container: &self.all_directories,
3309 all_dictionaries: &self.all_dictionaries,
3310 typename: "directory",
3311 }
3312 }
3313
3314 fn storage_from_self_checker<'b>(
3315 &'b self,
3316 input: &'b (impl CapabilityClause + FromClause),
3317 ) -> RouteFromSelfChecker<'b> {
3318 RouteFromSelfChecker {
3319 capability_name: input.storage(),
3320 from: input.from_(),
3321 container: &self.all_storages,
3322 all_dictionaries: &self.all_dictionaries,
3323 typename: "storage",
3324 }
3325 }
3326
3327 fn runner_from_self_checker<'b>(
3328 &'b self,
3329 input: &'b (impl CapabilityClause + FromClause),
3330 ) -> RouteFromSelfChecker<'b> {
3331 RouteFromSelfChecker {
3332 capability_name: input.runner(),
3333 from: input.from_(),
3334 container: &self.all_runners,
3335 all_dictionaries: &self.all_dictionaries,
3336 typename: "runner",
3337 }
3338 }
3339
3340 fn resolver_from_self_checker<'b>(
3341 &'b self,
3342 input: &'b (impl CapabilityClause + FromClause),
3343 ) -> RouteFromSelfChecker<'b> {
3344 RouteFromSelfChecker {
3345 capability_name: input.resolver(),
3346 from: input.from_(),
3347 container: &self.all_resolvers,
3348 all_dictionaries: &self.all_dictionaries,
3349 typename: "resolver",
3350 }
3351 }
3352
3353 fn dictionary_from_self_checker<'b>(
3354 &'b self,
3355 input: &'b (impl CapabilityClause + FromClause),
3356 ) -> RouteFromSelfChecker<'b> {
3357 RouteFromSelfChecker {
3358 capability_name: input.dictionary(),
3359 from: input.from_(),
3360 container: &self.all_dictionaries,
3361 all_dictionaries: &self.all_dictionaries,
3362 typename: "dictionary",
3363 }
3364 }
3365
3366 fn config_from_self_checker<'b>(
3367 &'b self,
3368 input: &'b (impl CapabilityClause + FromClause),
3369 ) -> RouteFromSelfChecker<'b> {
3370 RouteFromSelfChecker {
3371 capability_name: input.config(),
3372 from: input.from_(),
3373 container: &self.all_configs,
3374 all_dictionaries: &self.all_dictionaries,
3375 typename: "config",
3376 }
3377 }
3378}
3379
3380struct RouteFromSelfChecker<'a> {
3382 capability_name: Option<OneOrMany<&'a BorrowedName>>,
3384
3385 from: OneOrMany<AnyRef<'a>>,
3387
3388 container: &'a dyn Container,
3390
3391 all_dictionaries: &'a HashMap<&'a BorrowedName, &'a Capability>,
3393
3394 typename: &'static str,
3396}
3397
3398impl<'a> RouteFromSelfChecker<'a> {
3399 fn validate(self, operand: &'static str) -> Result<(), Error> {
3400 let Self { capability_name, from, container, all_dictionaries, typename } = self;
3401 let Some(capability) = capability_name else {
3402 return Ok(());
3403 };
3404 for capability in capability {
3405 for from in &from {
3406 match from {
3407 AnyRef::Self_ if !container.contains(capability) => {
3408 return Err(Error::validate(format!(
3409 "{typename} \"{capability}\" is {operand} from self, so it \
3410 must be declared as a \"{typename}\" in \"capabilities\"",
3411 )));
3412 }
3413 AnyRef::Dictionary(DictionaryRef { root: RootDictionaryRef::Self_, path }) => {
3414 let first_segment = path.iter_segments().next().unwrap();
3415 if !all_dictionaries.contains_key(first_segment) {
3416 return Err(Error::validate(format!(
3417 "{typename} \"{capability}\" is {operand} from \"self/{path}\", so \
3418 \"{first_segment}\" must be declared as a \"dictionary\" in \"capabilities\"",
3419 )));
3420 }
3421 }
3422 _ => {}
3423 }
3424 }
3425 }
3426 Ok(())
3427 }
3428}
3429
3430struct RouteFromSelfCheckerV2<'a> {
3431 capability_name: Option<ContextSpanned<OneOrMany<AnyRef<'a>>>>,
3432
3433 from: ContextSpanned<OneOrMany<AnyRef<'a>>>,
3434
3435 container: &'a dyn Container,
3436
3437 all_dictionaries: &'a HashMap<Name, &'a ContextCapability>,
3438
3439 typename: &'static str,
3440}
3441
3442impl<'a> RouteFromSelfCheckerV2<'a> {
3443 fn validate(self, operand: &'static str) -> Result<(), Error> {
3444 let Self { capability_name, from, container, all_dictionaries, typename } = self;
3445
3446 let Some(capability_span) = capability_name else {
3447 return Ok(());
3448 };
3449
3450 for capability in capability_span.value.iter() {
3451 let AnyRef::Named(name_ref) = capability else {
3452 continue;
3453 };
3454
3455 let capability_name = name_ref.as_str();
3456
3457 for from_ref in from.value.iter() {
3458 match from_ref {
3459 AnyRef::Self_ if !container.contains(name_ref) => {
3460 return Err(Error::validate_context(
3461 format!(
3462 "{typename} \"{capability_name}\" is {operand} from self, so it \
3463 must be declared as a \"{typename}\" in \"capabilities\"",
3464 ),
3465 Some(capability_span.origin.clone()),
3466 ));
3467 }
3468
3469 AnyRef::Dictionary(DictionaryRef { root: RootDictionaryRef::Self_, path }) => {
3470 let first_segment = path.iter_segments().next().unwrap();
3471 if !all_dictionaries.contains_key(first_segment) {
3472 return Err(Error::validate_context(
3473 format!(
3474 "{typename} \"{capability_name}\" is {operand} from \"self/{path}\", so \
3475 \"{first_segment}\" must be declared as a \"dictionary\" in \"capabilities\"",
3476 ),
3477 Some(from.origin.clone()),
3478 ));
3479 }
3480 }
3481 _ => {}
3482 }
3483 }
3484 }
3485 Ok(())
3486 }
3487}
3488
3489trait Container {
3493 fn contains(&self, key: &BorrowedName) -> bool;
3494}
3495
3496impl<'a> Container for HashSet<&'a BorrowedName> {
3497 fn contains(&self, key: &BorrowedName) -> bool {
3498 self.contains(key)
3499 }
3500}
3501
3502impl Container for HashSet<Name> {
3503 fn contains(&self, key: &BorrowedName) -> bool {
3504 self.contains(key)
3505 }
3506}
3507
3508impl<'a, T> Container for HashMap<&'a BorrowedName, T> {
3509 fn contains(&self, key: &BorrowedName) -> bool {
3510 self.contains_key(key)
3511 }
3512}
3513
3514impl<T> Container for HashMap<Name, T> {
3515 fn contains(&self, key: &BorrowedName) -> bool {
3516 self.contains_key(key)
3517 }
3518}
3519
3520pub fn use_config_to_value_type(u: &Use) -> Result<ConfigValueType, Error> {
3523 let config = u.config.clone().expect("Only call use_config_to_value_type on a Config");
3524
3525 let Some(config_type) = u.config_type.as_ref() else {
3526 return Err(Error::validate(format!("Config '{}' is missing field 'type'", config)));
3527 };
3528
3529 let config_type = match config_type {
3530 ConfigType::Bool => ConfigValueType::Bool { mutability: None },
3531 ConfigType::Uint8 => ConfigValueType::Uint8 { mutability: None },
3532 ConfigType::Uint16 => ConfigValueType::Uint16 { mutability: None },
3533 ConfigType::Uint32 => ConfigValueType::Uint32 { mutability: None },
3534 ConfigType::Uint64 => ConfigValueType::Uint64 { mutability: None },
3535 ConfigType::Int8 => ConfigValueType::Int8 { mutability: None },
3536 ConfigType::Int16 => ConfigValueType::Int16 { mutability: None },
3537 ConfigType::Int32 => ConfigValueType::Int32 { mutability: None },
3538 ConfigType::Int64 => ConfigValueType::Int64 { mutability: None },
3539 ConfigType::String => {
3540 let Some(max_size) = u.config_max_size else {
3541 return Err(Error::validate(format!(
3542 "Config '{}' is type String but is missing field 'max_size'",
3543 config
3544 )));
3545 };
3546 ConfigValueType::String { max_size: max_size.into(), mutability: None }
3547 }
3548 ConfigType::Vector => {
3549 let Some(ref element) = u.config_element_type else {
3550 return Err(Error::validate(format!(
3551 "Config '{}' is type Vector but is missing field 'element'",
3552 config
3553 )));
3554 };
3555 let Some(max_count) = u.config_max_count else {
3556 return Err(Error::validate(format!(
3557 "Config '{}' is type Vector but is missing field 'max_count'",
3558 config
3559 )));
3560 };
3561 ConfigValueType::Vector {
3562 max_count: max_count.into(),
3563 element: element.clone(),
3564 mutability: None,
3565 }
3566 }
3567 };
3568 Ok(config_type)
3569}
3570
3571pub fn use_config_to_value_type_context(u: &ContextUse) -> Result<ConfigValueType, Error> {
3574 let config = u.config.clone().expect("Only call use_config_to_value_type on a Config");
3575
3576 let Some(config_type) = u.config_type.as_ref() else {
3577 return Err(Error::validate_context(
3578 format!("Config '{}' is missing field 'type'", config.value),
3579 Some(config.origin),
3580 ));
3581 };
3582
3583 let config_type = match config_type.value {
3584 ConfigType::Bool => ConfigValueType::Bool { mutability: None },
3585 ConfigType::Uint8 => ConfigValueType::Uint8 { mutability: None },
3586 ConfigType::Uint16 => ConfigValueType::Uint16 { mutability: None },
3587 ConfigType::Uint32 => ConfigValueType::Uint32 { mutability: None },
3588 ConfigType::Uint64 => ConfigValueType::Uint64 { mutability: None },
3589 ConfigType::Int8 => ConfigValueType::Int8 { mutability: None },
3590 ConfigType::Int16 => ConfigValueType::Int16 { mutability: None },
3591 ConfigType::Int32 => ConfigValueType::Int32 { mutability: None },
3592 ConfigType::Int64 => ConfigValueType::Int64 { mutability: None },
3593 ConfigType::String => {
3594 let Some(ref max_size) = u.config_max_size else {
3595 return Err(Error::validate_context(
3596 format!(
3597 "Config '{}' is type String but is missing field 'max_size'",
3598 config.value
3599 ),
3600 Some(config.origin),
3601 ));
3602 };
3603 ConfigValueType::String { max_size: max_size.value.into(), mutability: None }
3604 }
3605 ConfigType::Vector => {
3606 let Some(ref element) = u.config_element_type else {
3607 return Err(Error::validate_context(
3608 format!(
3609 "Config '{}' is type Vector but is missing field 'element'",
3610 config.value
3611 ),
3612 Some(config.origin),
3613 ));
3614 };
3615 let Some(ref max_count) = u.config_max_count else {
3616 return Err(Error::validate_context(
3617 format!(
3618 "Config '{}' is type Vector but is missing field 'max_count'",
3619 config.value
3620 ),
3621 Some(config.origin),
3622 ));
3623 };
3624 ConfigValueType::Vector {
3625 max_count: max_count.value.into(),
3626 element: element.value.clone(),
3627 mutability: None,
3628 }
3629 }
3630 };
3631 Ok(config_type)
3632}
3633
3634fn ensure_no_duplicate_names<'a, I>(values: I) -> Result<(), Error>
3637where
3638 I: Iterator<Item = (&'a BorrowedName, &'a str)>,
3639{
3640 let mut seen_keys = HashMap::new();
3641 for (key, name) in values {
3642 if let Some(preexisting_name) = seen_keys.insert(key, name) {
3643 return Err(Error::validate(format!(
3644 "identifier \"{}\" is defined twice, once in \"{}\" and once in \"{}\"",
3645 key, name, preexisting_name
3646 )));
3647 }
3648 }
3649 Ok(())
3650}
3651
3652fn ensure_no_duplicate_values<'a, I, V>(values: I) -> Result<(), Error>
3654where
3655 I: IntoIterator<Item = &'a V>,
3656 V: 'a + Hash + Eq + fmt::Display,
3657{
3658 let mut seen = HashSet::new();
3659 for value in values {
3660 if !seen.insert(value) {
3661 return Err(Error::validate(format!("Found duplicate value \"{}\" in array.", value)));
3662 }
3663 }
3664 Ok(())
3665}
3666
3667#[cfg(test)]
3668mod tests {
3669 use super::*;
3670 use crate::error::Location;
3671 use crate::types::offer::{
3672 offer_to_all_and_component_diff_capabilities_message,
3673 offer_to_all_and_component_diff_sources_message,
3674 };
3675 use assert_matches::assert_matches;
3676 use serde_json::json;
3677
3678 macro_rules! test_validate_cml {
3679 (
3680 $(
3681 $test_name:ident($input:expr, $($pattern:tt)+),
3682 )+
3683 ) => {
3684 $(
3685 #[test]
3686 fn $test_name() {
3687 let input = format!("{}", $input);
3688 let result = validate_for_test("test.cml", &input.as_bytes());
3689 assert_matches!(result, $($pattern)+);
3690 }
3691 )+
3692 }
3693 }
3694
3695 macro_rules! test_validate_cml_with_context {
3696 (
3697 $(
3698 $test_name:ident($input:expr, $($pattern:tt)+),
3699 )+
3700 ) => {
3701 $(
3702 #[test]
3703 fn $test_name() {
3704 let input = format!("{}", $input);
3705 let result = validate_for_test_context("test.cml", &input.as_bytes());
3706 assert_matches!(result, $($pattern)+);
3707 }
3708 )+
3709 }
3710 }
3711
3712 macro_rules! test_validate_cml_with_feature {
3713 (
3714 $features:expr,
3715 {
3716 $(
3717 $test_name:ident($input:expr, $($pattern:tt)+),
3718 )+
3719 }
3720 ) => {
3721 $(
3722 #[test]
3723 fn $test_name() {
3724 let input = format!("{}", $input);
3725 let features = $features;
3726 let result = validate_with_features_for_test("test.cml", &input.as_bytes(), &features, &vec![], &vec![], &vec![]);
3727 assert_matches!(result, $($pattern)+);
3728 }
3729 )+
3730 }
3731 }
3732
3733 macro_rules! test_validate_cml_with_feature_context {
3734 (
3735 $features:expr,
3736 {
3737 $(
3738 $test_name:ident($input:expr, $($pattern:tt)+),
3739 )+
3740 }
3741 ) => {
3742 $(
3743 #[test]
3744 fn $test_name() {
3745 let input = format!("{}", $input);
3746 let features = $features;
3747 let result = validate_with_features_for_test_context("test.cml", &input.as_bytes(), &features, &vec![], &vec![], &vec![]);
3748 assert_matches!(result, $($pattern)+);
3749 }
3750 )+
3751 }
3752 }
3753
3754 fn validate_for_test(filename: &str, input: &[u8]) -> Result<(), Error> {
3755 validate_with_features_for_test(filename, input, &FeatureSet::empty(), &[], &[], &[])
3756 }
3757
3758 fn validate_for_test_context(filename: &str, input: &[u8]) -> Result<(), Error> {
3759 validate_with_features_for_test_context(
3760 filename,
3761 input,
3762 &FeatureSet::empty(),
3763 &[],
3764 &[],
3765 &[],
3766 )
3767 }
3768
3769 fn validate_with_features_for_test_context(
3770 filename: &str,
3771 input: &[u8],
3772 features: &FeatureSet,
3773 required_offers: &[String],
3774 required_uses: &[String],
3775 required_dictionary_offers: &[String],
3776 ) -> Result<(), Error> {
3777 let input = format!("{}", std::str::from_utf8(input).unwrap().to_string());
3778 let file = Path::new(filename);
3779 let document = crate::load_cml_with_context(&input, file)?;
3780 validate_cml_context(
3781 &document,
3782 &features,
3783 &CapabilityRequirements {
3784 must_offer: &required_offers
3785 .iter()
3786 .map(|value| OfferToAllCapability::Protocol(value))
3787 .chain(
3788 required_dictionary_offers
3789 .iter()
3790 .map(|value| OfferToAllCapability::Dictionary(value)),
3791 )
3792 .collect::<Vec<_>>(),
3793 must_use: &required_uses
3794 .iter()
3795 .map(|value| MustUseRequirement::Protocol(value))
3796 .collect::<Vec<_>>(),
3797 },
3798 )
3799 }
3800
3801 fn validate_with_features_for_test(
3802 filename: &str,
3803 input: &[u8],
3804 features: &FeatureSet,
3805 required_offers: &[String],
3806 required_uses: &[String],
3807 required_dictionary_offers: &[String],
3808 ) -> Result<(), Error> {
3809 let input = format!("{}", std::str::from_utf8(input).unwrap().to_string());
3810 let file = Path::new(filename);
3811 let document = crate::parse_one_document(&input, &file)?;
3812 validate_cml(
3813 &document,
3814 Some(&file),
3815 &features,
3816 &CapabilityRequirements {
3817 must_offer: &required_offers
3818 .iter()
3819 .map(|value| OfferToAllCapability::Protocol(value))
3820 .chain(
3821 required_dictionary_offers
3822 .iter()
3823 .map(|value| OfferToAllCapability::Dictionary(value)),
3824 )
3825 .collect::<Vec<_>>(),
3826 must_use: &required_uses
3827 .iter()
3828 .map(|value| MustUseRequirement::Protocol(value))
3829 .collect::<Vec<_>>(),
3830 },
3831 )
3832 }
3833
3834 fn unused_component_err_message(missing: &str) -> String {
3835 format!(r#"Protocol "{}" is not used by a component but is required by all"#, missing)
3836 }
3837
3838 #[test]
3839 fn must_use_protocol() {
3840 let input = r##"{
3841 children: [
3842 {
3843 name: "logger",
3844 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
3845 },
3846 {
3847 name: "something",
3848 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
3849 },
3850 ],
3851 }"##;
3852
3853 let result = validate_with_features_for_test(
3854 "test.cml",
3855 input.as_bytes(),
3856 &FeatureSet::empty(),
3857 &[],
3858 &vec!["fuchsia.logger.LogSink".into()],
3859 &[],
3860 );
3861
3862 assert_matches!(result,
3863 Err(Error::Validate { err, filename }) => {
3864 assert_eq!(err, unused_component_err_message("fuchsia.logger.LogSink"));
3865 assert!(filename.is_some(), "Expected there to be a filename in error message");
3866 }
3867 );
3868
3869 let input = r##"{
3870 children: [
3871 {
3872 name: "logger",
3873 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
3874 },
3875 ],
3876
3877 use: [
3878 {
3879 protocol: [ "fuchsia.component.Binder" ],
3880 from: "framework",
3881 }
3882 ],
3883 }"##;
3884
3885 let result = validate_with_features_for_test(
3886 "test.cml",
3887 input.as_bytes(),
3888 &FeatureSet::empty(),
3889 &[],
3890 &vec!["fuchsia.component.Binder".into()],
3891 &[],
3892 );
3893 assert_matches!(result, Ok(_));
3894 }
3895
3896 #[test]
3897 fn required_offer_to_all() {
3898 let input = r##"{
3899 children: [
3900 {
3901 name: "logger",
3902 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
3903 },
3904 {
3905 name: "something",
3906 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
3907 },
3908 ],
3909 collections: [
3910 {
3911 name: "coll",
3912 durability: "transient",
3913 },
3914 ],
3915 offer: [
3916 {
3917 protocol: "fuchsia.logger.LogSink",
3918 from: "parent",
3919 to: "all"
3920 },
3921 {
3922 protocol: "fuchsia.inspect.InspectSink",
3923 from: "parent",
3924 to: "all"
3925 },
3926 {
3927 protocol: "fuchsia.process.Launcher",
3928 from: "parent",
3929 to: "#something",
3930 },
3931 ]
3932 }"##;
3933 let result = validate_with_features_for_test(
3934 "test.cml",
3935 input.as_bytes(),
3936 &FeatureSet::empty(),
3937 &vec!["fuchsia.logger.LogSink".into(), "fuchsia.inspect.InspectSink".into()],
3938 &Vec::new(),
3939 &[],
3940 );
3941 assert_matches!(result, Ok(_));
3942 }
3943
3944 #[test]
3945 fn required_offer_to_all_manually() {
3946 let input = r##"{
3947 children: [
3948 {
3949 name: "logger",
3950 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
3951 },
3952 {
3953 name: "something",
3954 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
3955 },
3956 ],
3957 collections: [
3958 {
3959 name: "coll",
3960 durability: "transient",
3961 },
3962 ],
3963 offer: [
3964 {
3965 protocol: "fuchsia.logger.LogSink",
3966 from: "#something",
3967 to: "#logger"
3968 },
3969 {
3970 protocol: "fuchsia.logger.LogSink",
3971 from: "parent",
3972 to: "#something"
3973 },
3974 {
3975 protocol: "fuchsia.logger.LogSink",
3976 from: "parent",
3977 to: "#coll",
3978 },
3979 ]
3980 }"##;
3981 let result = validate_with_features_for_test(
3982 "test.cml",
3983 input.as_bytes(),
3984 &FeatureSet::empty(),
3985 &vec!["fuchsia.logger.LogSink".into()],
3986 &[],
3987 &[],
3988 );
3989 assert_matches!(result, Ok(_));
3990
3991 let input = r##"{
3992 children: [
3993 {
3994 name: "logger",
3995 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
3996 },
3997 {
3998 name: "something",
3999 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4000 },
4001 {
4002 name: "something_v2",
4003 url: "fuchsia-pkg://fuchsia.com/something_v2#meta/something_v2.cm",
4004 },
4005 ],
4006 collections: [
4007 {
4008 name: "coll",
4009 durability: "transient",
4010 },
4011 ],
4012 offer: [
4013 {
4014 protocol: "fuchsia.logger.LogSink",
4015 from: "parent",
4016 to: ["#logger", "#something", "#something_v2", "#coll"],
4017 },
4018 ]
4019 }"##;
4020 let result = validate_with_features_for_test(
4021 "test.cml",
4022 input.as_bytes(),
4023 &FeatureSet::empty(),
4024 &vec!["fuchsia.logger.LogSink".into()],
4025 &[],
4026 &[],
4027 );
4028 assert_matches!(result, Ok(_));
4029 }
4030
4031 #[test]
4032 fn offer_to_all_mixed_with_array_syntax() {
4033 let input = r##"{
4034 "children": [
4035 {
4036 "name": "something",
4037 "url": "fuchsia-pkg://fuchsia.com/something/stable#meta/something.cm",
4038 },
4039 ],
4040 "offer": [
4041 {
4042 "protocol": ["fuchsia.logger.LogSink", "fuchsia.inspect.InspectSink",],
4043 "from": "parent",
4044 "to": "#something",
4045 },
4046 {
4047 "protocol": "fuchsia.logger.LogSink",
4048 "from": "parent",
4049 "to": "all",
4050 },
4051 ],
4052 }"##;
4053
4054 let result = validate_with_features_for_test(
4055 "test.cml",
4056 input.as_bytes(),
4057 &FeatureSet::empty(),
4058 &vec!["fuchsia.logger.LogSink".into()],
4059 &Vec::new(),
4060 &[],
4061 );
4062
4063 assert_matches!(result, Ok(_));
4064
4065 let input = r##"{
4066
4067 "children": [
4068 {
4069 "name": "something",
4070 "url": "fuchsia-pkg://fuchsia.com/something/stable#meta/something.cm",
4071 },
4072 ],
4073 "offer": [
4074 {
4075 "protocol": ["fuchsia.logger.LogSink", "fuchsia.inspect.InspectSink",],
4076 "from": "parent",
4077 "to": "all",
4078 },
4079 {
4080 "protocol": "fuchsia.logger.LogSink",
4081 "from": "parent",
4082 "to": "#something",
4083 },
4084 ],
4085 }"##;
4086
4087 let result = validate_with_features_for_test(
4088 "test.cml",
4089 input.as_bytes(),
4090 &FeatureSet::empty(),
4091 &vec!["fuchsia.logger.LogSink".into()],
4092 &Vec::new(),
4093 &[],
4094 );
4095
4096 assert_matches!(result, Ok(_));
4097 }
4098
4099 #[test]
4100 fn offer_to_all_and_manual() {
4101 let input = r##"{
4102 children: [
4103 {
4104 name: "logger",
4105 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4106 },
4107 {
4108 name: "something",
4109 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4110 },
4111 ],
4112 offer: [
4113 {
4114 protocol: "fuchsia.logger.LogSink",
4115 from: "parent",
4116 to: "all"
4117 },
4118 {
4119 protocol: "fuchsia.logger.LogSink",
4120 from: "parent",
4121 to: "#something"
4122 },
4123 ]
4124 }"##;
4125
4126 let result = validate_with_features_for_test(
4127 "test.cml",
4128 input.as_bytes(),
4129 &FeatureSet::empty(),
4130 &vec!["fuchsia.logger.LogSink".into()],
4131 &Vec::new(),
4132 &[],
4133 );
4134
4135 assert_matches!(result, Ok(_));
4137
4138 let input = r##"{
4139 children: [
4140 {
4141 name: "logger",
4142 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4143 },
4144 {
4145 name: "something",
4146 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4147 },
4148 ],
4149 offer: [
4150 {
4151 protocol: "fuchsia.logger.LogSink",
4152 from: "parent",
4153 to: "all"
4154 },
4155 {
4156 protocol: "fuchsia.logger.FakLog",
4157 from: "parent",
4158 as: "fuchsia.logger.LogSink",
4159 to: "#something"
4160 },
4161 ]
4162 }"##;
4163
4164 let result = validate_with_features_for_test(
4165 "test.cml",
4166 input.as_bytes(),
4167 &FeatureSet::empty(),
4168 &vec!["fuchsia.logger.LogSink".into()],
4169 &Vec::new(),
4170 &[],
4171 );
4172
4173 assert_matches!(result,
4175 Err(Error::Validate { err, filename }) => {
4176 assert_eq!(
4177 err,
4178 offer_to_all_and_component_diff_capabilities_message([OfferToAllCapability::Protocol("fuchsia.logger.LogSink")].into_iter(), "something"),
4179 );
4180 assert!(filename.is_some(), "Expected there to be a filename in error message");
4181 }
4182 );
4183
4184 let input = r##"{
4185 children: [
4186 {
4187 name: "logger",
4188 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4189 },
4190 {
4191 name: "something",
4192 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4193 },
4194 ],
4195 offer: [
4196 {
4197 protocol: "fuchsia.logger.LogSink",
4198 from: "parent",
4199 to: "all"
4200 },
4201 {
4202 protocol: "fuchsia.logger.LogSink",
4203 from: "framework",
4204 to: "#something"
4205 },
4206 ]
4207 }"##;
4208
4209 let result = validate_with_features_for_test(
4210 "test.cml",
4211 input.as_bytes(),
4212 &FeatureSet::empty(),
4213 &vec!["fuchsia.logger.LogSink".into()],
4214 &Vec::new(),
4215 &[],
4216 );
4217
4218 assert_matches!(result,
4220 Err(Error::Validate { err, filename }) => {
4221 assert_eq!(
4222 err,
4223 offer_to_all_and_component_diff_sources_message([OfferToAllCapability::Protocol("fuchsia.logger.LogSink")].into_iter(), "something"),
4224 );
4225 assert!(filename.is_some(), "Expected there to be a filename in error message");
4226 }
4227 );
4228 }
4229
4230 #[test]
4231 fn offer_to_all_and_manual_for_dictionary() {
4232 let input = r##"{
4233 children: [
4234 {
4235 name: "logger",
4236 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4237 },
4238 {
4239 name: "something",
4240 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4241 },
4242 ],
4243 offer: [
4244 {
4245 dictionary: "diagnostics",
4246 from: "parent",
4247 to: "all"
4248 },
4249 {
4250 dictionary: "diagnostics",
4251 from: "parent",
4252 to: "#something"
4253 },
4254 ]
4255 }"##;
4256
4257 let result = validate_with_features_for_test(
4258 "test.cml",
4259 input.as_bytes(),
4260 &FeatureSet::empty(),
4261 &vec![],
4262 &Vec::new(),
4263 &["diagnostics".into()],
4264 );
4265
4266 assert_matches!(result, Ok(_));
4268
4269 let input = r##"{
4270 children: [
4271 {
4272 name: "logger",
4273 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4274 },
4275 {
4276 name: "something",
4277 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4278 },
4279 ],
4280 offer: [
4281 {
4282 dictionary: "diagnostics",
4283 from: "parent",
4284 to: "all"
4285 },
4286 {
4287 dictionary: "FakDictionary",
4288 from: "parent",
4289 as: "diagnostics",
4290 to: "#something"
4291 },
4292 ]
4293 }"##;
4294
4295 let result = validate_with_features_for_test(
4296 "test.cml",
4297 input.as_bytes(),
4298 &FeatureSet::empty(),
4299 &vec![],
4300 &Vec::new(),
4301 &["diagnostics".into()],
4302 );
4303
4304 assert_matches!(result,
4306 Err(Error::Validate { err, filename }) => {
4307 assert_eq!(
4308 err,
4309 offer_to_all_and_component_diff_capabilities_message([OfferToAllCapability::Dictionary("diagnostics")].into_iter(), "something"),
4310 );
4311 assert!(filename.is_some(), "Expected there to be a filename in error message");
4312 }
4313 );
4314
4315 let input = r##"{
4316 children: [
4317 {
4318 name: "logger",
4319 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4320 },
4321 {
4322 name: "something",
4323 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4324 },
4325 ],
4326 offer: [
4327 {
4328 dictionary: "diagnostics",
4329 from: "parent",
4330 to: "all"
4331 },
4332 {
4333 dictionary: "diagnostics",
4334 from: "framework",
4335 to: "#something"
4336 },
4337 ]
4338 }"##;
4339
4340 let result = validate_with_features_for_test(
4341 "test.cml",
4342 input.as_bytes(),
4343 &FeatureSet::empty(),
4344 &vec![],
4345 &Vec::new(),
4346 &["diagnostics".into()],
4347 );
4348
4349 assert_matches!(result,
4351 Err(Error::Validate { err, filename }) => {
4352 assert_eq!(
4353 err,
4354 offer_to_all_and_component_diff_sources_message([OfferToAllCapability::Dictionary("diagnostics")].into_iter(), "something"),
4355 );
4356 assert!(filename.is_some(), "Expected there to be a filename in error message");
4357 }
4358 );
4359 }
4360
4361 fn offer_to_all_diff_sources_message(protocols: &[&str]) -> String {
4362 format!(r#"Protocol(s) {:?} offered to "all" multiple times"#, protocols)
4363 }
4364
4365 #[test]
4366 fn offer_to_all_from_diff_sources() {
4367 let input = r##"{
4368 "children": [
4369 {
4370 "name": "logger",
4371 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
4372 },
4373 {
4374 "name": "something",
4375 "url": "fuchsia-pkg://fuchsia.com/something#meta/something.cm"
4376 }
4377 ],
4378 "offer": [
4379 {
4380 "protocol": "fuchsia.logger.LogSink",
4381 "from": "parent",
4382 "to": "all"
4383 },
4384 {
4385 "protocol": "fuchsia.logger.LogSink",
4386 "from": "framework",
4387 "to": "all"
4388 }
4389 ]
4390 }"##;
4391
4392 let result = validate_with_features_for_test_context(
4393 "test.cml",
4394 input.as_bytes(),
4395 &FeatureSet::empty(),
4396 &vec!["fuchsia.logger.LogSink".into()],
4397 &Vec::new(),
4398 &[],
4399 );
4400
4401 assert_matches!(result,
4402 Err(Error::ValidateContexts { err, .. }) => {
4403 assert_eq!(
4404 err,
4405 offer_to_all_diff_sources_message(&["fuchsia.logger.LogSink"]),
4406 );
4407 }
4408 );
4409 }
4410
4411 #[test]
4412 fn offer_to_all_from_diff_sources_no_span() {
4413 let input = r##"{
4414 children: [
4415 {
4416 name: "logger",
4417 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4418 },
4419 {
4420 name: "something",
4421 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4422 },
4423 ],
4424 offer: [
4425 {
4426 protocol: "fuchsia.logger.LogSink",
4427 from: "parent",
4428 to: "all"
4429 },
4430 {
4431 protocol: "fuchsia.logger.LogSink",
4432 from: "framework",
4433 to: "all"
4434 },
4435 ]
4436 }"##;
4437
4438 let result = validate_with_features_for_test(
4439 "test.cml",
4440 input.as_bytes(),
4441 &FeatureSet::empty(),
4442 &vec!["fuchsia.logger.LogSink".into()],
4443 &Vec::new(),
4444 &[],
4445 );
4446
4447 assert_matches!(result,
4448 Err(Error::Validate { err, filename }) => {
4449 assert_eq!(
4450 err,
4451 offer_to_all_diff_sources_message(&["fuchsia.logger.LogSink"]),
4452 );
4453 assert!(filename.is_some(), "Expected there to be a filename in error message");
4454 }
4455 );
4456 }
4457
4458 #[test]
4459 fn offer_to_all_with_aliases() {
4460 let input = r##"{
4461 children: [
4462 {
4463 name: "logger",
4464 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4465 },
4466 {
4467 name: "something",
4468 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4469 },
4470 ],
4471 offer: [
4472 {
4473 protocol: "fuchsia.logger.LogSink",
4474 from: "parent",
4475 to: "all"
4476 },
4477 {
4478 protocol: "fuchsia.logger.LogSink",
4479 from: "framework",
4480 to: "all",
4481 as: "OtherLogSink",
4482 },
4483 {
4484 protocol: "fuchsia.logger.LogSink",
4485 from: "framework",
4486 to: "#something",
4487 as: "OtherOtherLogSink",
4488 },
4489 {
4490 protocol: "fuchsia.logger.LogSink",
4491 from: "parent",
4492 to: "#something",
4493 as: "fuchsia.logger.LogSink",
4494 },
4495 ]
4496 }"##;
4497
4498 let result = validate_with_features_for_test(
4499 "test.cml",
4500 input.as_bytes(),
4501 &FeatureSet::empty(),
4502 &["fuchsia.logger.LogSink".into()],
4503 &[],
4504 &[],
4505 );
4506
4507 assert_matches!(result, Ok(_));
4508 }
4509
4510 #[test]
4511 fn required_dict_offers_accept_aliases() {
4512 let input = r##"{
4513 capabilities: [
4514 {
4515 dictionary: "test-diagnostics",
4516 }
4517 ],
4518 children: [
4519 {
4520 name: "something",
4521 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4522 },
4523 ],
4524 offer: [
4525 {
4526 dictionary: "test-diagnostics",
4527 from: "self",
4528 to: "#something",
4529 as: "diagnostics",
4530 }
4531 ]
4532 }"##;
4533
4534 let result = validate_with_features_for_test(
4535 "test.cml",
4536 input.as_bytes(),
4537 &FeatureSet::empty(),
4538 &[],
4539 &[],
4540 &["diagnostics".into()],
4541 );
4542
4543 assert_matches!(result, Ok(_));
4544 }
4545
4546 fn fail_to_make_required_offer(
4547 protocol: &str,
4548 child_or_collection: &str,
4549 component: &str,
4550 ) -> String {
4551 format!(
4552 r#"Protocol "{}" is not offered to {} "{}" but it is a required offer"#,
4553 protocol, child_or_collection, component
4554 )
4555 }
4556
4557 fn fail_to_make_required_offer_dictionary(
4558 dictionary: &str,
4559 child_or_collection: &str,
4560 component: &str,
4561 ) -> String {
4562 format!(
4563 r#"Dictionary "{}" is not offered to {} "{}" but it is a required offer"#,
4564 dictionary, child_or_collection, component
4565 )
4566 }
4567
4568 #[test]
4569 fn fail_to_offer_to_all_when_required() {
4570 let input = r##"{
4571 children: [
4572 {
4573 name: "logger",
4574 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4575 },
4576 {
4577 name: "something",
4578 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4579 },
4580 ],
4581 offer: [
4582 {
4583 protocol: "fuchsia.logger.LogSink",
4584 from: "parent",
4585 to: "#logger"
4586 },
4587 {
4588 protocol: "fuchsia.logger.LegacyLog",
4589 from: "parent",
4590 to: "#something"
4591 },
4592 ]
4593 }"##;
4594 let result = validate_with_features_for_test(
4595 "test.cml",
4596 input.as_bytes(),
4597 &FeatureSet::empty(),
4598 &vec!["fuchsia.logger.LogSink".into()],
4599 &[],
4600 &[],
4601 );
4602
4603 assert_matches!(result,
4604 Err(Error::Validate { err, filename }) => {
4605 assert_eq!(
4606 err,
4607 fail_to_make_required_offer(
4608 "fuchsia.logger.LogSink",
4609 "child component",
4610 "something",
4611 ),
4612 );
4613 assert!(filename.is_some(), "Expected there to be a filename in error message");
4614 }
4615 );
4616
4617 let input = r##"{
4618 children: [
4619 {
4620 name: "logger",
4621 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4622 },
4623 ],
4624 collections: [
4625 {
4626 name: "coll",
4627 durability: "transient",
4628 },
4629 ],
4630 offer: [
4631 {
4632 protocol: "fuchsia.logger.LogSink",
4633 from: "parent",
4634 to: "#logger"
4635 },
4636 ]
4637 }"##;
4638 let result = validate_with_features_for_test(
4639 "test.cml",
4640 input.as_bytes(),
4641 &FeatureSet::empty(),
4642 &vec!["fuchsia.logger.LogSink".into()],
4643 &[],
4644 &[],
4645 );
4646
4647 assert_matches!(result,
4648 Err(Error::Validate { err, filename }) => {
4649 assert_eq!(
4650 err,
4651 fail_to_make_required_offer("fuchsia.logger.LogSink", "collection", "coll"),
4652 );
4653 assert!(filename.is_some(), "Expected there to be a filename in error message");
4654 }
4655 );
4656 }
4657
4658 #[test]
4659 fn fail_to_offer_dictionary_to_all_when_required() {
4660 let input = r##"{
4661 children: [
4662 {
4663 name: "logger",
4664 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4665 },
4666 {
4667 name: "something",
4668 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4669 },
4670 ],
4671 offer: [
4672 {
4673 protocol: "fuchsia.logger.LogSink",
4674 from: "parent",
4675 to: "all"
4676 },
4677 {
4678 dictionary: "diagnostics",
4679 from: "parent",
4680 to: "#logger"
4681 },
4682 {
4683 protocol: "fuchsia.logger.LegacyLog",
4684 from: "parent",
4685 to: "#something"
4686 },
4687 ]
4688 }"##;
4689 let result = validate_with_features_for_test(
4690 "test.cml",
4691 input.as_bytes(),
4692 &FeatureSet::empty(),
4693 &vec![],
4694 &[],
4695 &["diagnostics".to_string()],
4696 );
4697
4698 assert_matches!(result,
4699 Err(Error::Validate { err, filename }) => {
4700 assert_eq!(
4701 err,
4702 fail_to_make_required_offer_dictionary(
4703 "diagnostics",
4704 "child component",
4705 "something",
4706 ),
4707 );
4708 assert!(filename.is_some(), "Expected there to be a filename in error message");
4709 }
4710 );
4711
4712 let input = r##"{
4713 children: [
4714 {
4715 name: "logger",
4716 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4717 },
4718 ],
4719 collections: [
4720 {
4721 name: "coll",
4722 durability: "transient",
4723 },
4724 ],
4725 offer: [
4726 {
4727 protocol: "fuchsia.logger.LogSink",
4728 from: "parent",
4729 to: "all"
4730 },
4731 {
4732 protocol: "diagnostics",
4733 from: "parent",
4734 to: "all"
4735 },
4736 {
4737 dictionary: "diagnostics",
4738 from: "parent",
4739 to: "#logger"
4740 },
4741 ]
4742 }"##;
4743 let result = validate_with_features_for_test(
4744 "test.cml",
4745 input.as_bytes(),
4746 &FeatureSet::empty(),
4747 &vec!["fuchsia.logger.LogSink".into()],
4748 &[],
4749 &["diagnostics".to_string()],
4750 );
4751 assert_matches!(result,
4752 Err(Error::Validate { err, filename }) => {
4753 assert_eq!(
4754 err,
4755 fail_to_make_required_offer_dictionary("diagnostics", "collection", "coll"),
4756 );
4757 assert!(filename.is_some(), "Expected there to be a filename in error message");
4758 }
4759 );
4760 }
4761
4762 #[test]
4763 fn fail_to_offer_dictionary_to_all_when_required_even_if_protocol_called_diagnostics_offered() {
4764 let input = r##"{
4765 children: [
4766 {
4767 name: "logger",
4768 url: "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
4769 },
4770 {
4771 name: "something",
4772 url: "fuchsia-pkg://fuchsia.com/something#meta/something.cm",
4773 },
4774 ],
4775 offer: [
4776 {
4777 protocol: "fuchsia.logger.LogSink",
4778 from: "parent",
4779 to: "all"
4780 },
4781 {
4782 protocol: "diagnostics",
4783 from: "parent",
4784 to: "all"
4785 },
4786 {
4787 protocol: "fuchsia.logger.LegacyLog",
4788 from: "parent",
4789 to: "#something"
4790 },
4791 ]
4792 }"##;
4793 let result = validate_with_features_for_test(
4794 "test.cml",
4795 input.as_bytes(),
4796 &FeatureSet::empty(),
4797 &[],
4798 &[],
4799 &["diagnostics".to_string()],
4800 );
4801
4802 assert_matches!(result,
4803 Err(Error::Validate { err, filename }) => {
4804 assert_eq!(
4805 err,
4806 fail_to_make_required_offer_dictionary(
4807 "diagnostics",
4808 "child component",
4809 "logger",
4810 ),
4811 );
4812 assert!(filename.is_some(), "Expected there to be a filename in error message");
4813 }
4814 );
4815 }
4816
4817 #[test]
4818 fn test_validate_invalid_json_fails() {
4819 let result = validate_for_test("test.cml", b"{");
4820 let expected_err = r#" --> 1:2
4821 |
48221 | {
4823 | ^---
4824 |
4825 = expected identifier or string"#;
4826 assert_matches!(result, Err(Error::Parse { err, .. }) if &err == expected_err);
4827 }
4828
4829 #[test]
4830 fn test_cml_json5() {
4831 let input = r##"{
4832 "expose": [
4833 // Here are some services to expose.
4834 { "protocol": "fuchsia.logger.Log", "from": "#logger", },
4835 { "directory": "blobfs", "from": "#logger", "rights": ["rw*"]},
4836 ],
4837 "children": [
4838 {
4839 name: 'logger',
4840 'url': 'fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm',
4841 },
4842 ],
4843 }"##;
4844 let result = validate_for_test("test.cml", input.as_bytes());
4845 assert_matches!(result, Ok(()));
4846 }
4847
4848 #[test]
4849 fn test_cml_error_location() {
4850 let input = r##"{
4851 "use": [
4852 {
4853 "protocol": "foo",
4854 "from": "bad",
4855 },
4856 ],
4857}"##;
4858 let result = validate_for_test("test.cml", input.as_bytes());
4859 assert_matches!(
4860 result,
4861 Err(Error::Parse { err, location: Some(l), filename: Some(f) })
4862 if &err == "invalid value: string \"bad\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none" &&
4863 l == Location { line: 5, column: 21 } &&
4864 f.ends_with("test.cml")
4865 );
4866 }
4867
4868 test_validate_cml_with_context! {
4869 test_cml_empty_json(
4870 json!({}),
4871 Ok(())
4872 ),
4873
4874 test_cml_children_url_ends_in_cml(
4875 r##"{
4876 "children": [
4877 {
4878 "name": "logger",
4879 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cml"
4880 }
4881 ]
4882 }"##,
4883 Err(Error::ValidateContext { 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"),
4884
4885 test_cml_allow_long_names_without_feature(
4886 json!({
4887 "collections": [
4888 {
4889 "name": "foo",
4890 "durability": "transient",
4891 "allow_long_names": true
4892 },
4893 ],
4894 }),
4895 Err(Error::RestrictedFeature(s)) if s == "allow_long_names"
4896 ),
4897
4898 test_cml_directory_missing_path(
4899 r##"{
4900 "capabilities": [
4901 {
4902 "directory": "dir",
4903 "rights": ["connect"]
4904 }
4905 ]
4906 }"##,
4907 Err(Error::ValidateContext { err, ..}) if &err == "\"path\" should be present with \"directory\""
4908 ),
4909 test_cml_directory_missing_rights(
4910 r##"{
4911 "capabilities": [
4912 {
4913 "directory": "dir",
4914 "path": "/dir"
4915 }
4916 ]
4917 }"##,
4918 Err(Error::ValidateContext { err, .. }) if &err == "\"rights\" should be present with \"directory\""),
4919
4920 test_cml_storage_missing_from(
4921 r##"{
4922 "capabilities": [
4923 {
4924 "storage": "data-storage",
4925 "backing_dir": "minfs",
4926 "storage_id": "static_instance_id_or_moniker"
4927 }
4928 ]
4929 }"##,
4930 Err(Error::ValidateContext { err, .. }) if &err == "\"from\" should be present with \"storage\""),
4931
4932
4933 test_cml_storage_path(
4934 r##"{
4935 "capabilities": [ {
4936 "storage": "minfs",
4937 "from": "self",
4938 "path": "/minfs",
4939 "storage_id": "static_instance_id_or_moniker"
4940 } ]
4941 }"##,
4942 Err(Error::ValidateContext { err, .. }) if &err == "\"path\" cannot be present with \"storage\", use \"backing_dir\""),
4943
4944 test_cml_storage_missing_path_or_backing_dir(
4945 r##"{
4946 "capabilities": [ {
4947 "storage": "minfs",
4948 "from": "self",
4949 "storage_id": "static_instance_id_or_moniker"
4950 } ]
4951 }"##,
4952 Err(Error::ValidateContext { err, .. }) if &err == "\"backing_dir\" should be present with \"storage\""
4953 ),
4954
4955 test_cml_storage_missing_storage_id(
4956 r##"{
4957 "capabilities": [ {
4958 "storage": "minfs",
4959 "from": "self",
4960 "backing_dir": "storage"
4961 } ]
4962 }"##,
4963 Err(Error::ValidateContext{ err, .. }) if &err == "\"storage_id\" should be present with \"storage\""),
4964
4965 test_cml_capabilities_extraneous_resolver_from(
4966 r##"{
4967 "capabilities": [
4968 {
4969 "resolver": "pkg_resolver",
4970 "path": "/svc/fuchsia.component.resolution.Resolver",
4971 "from": "self"
4972 }
4973 ]
4974 }"##,
4975 Err(Error::ValidateContext { err, .. }) if &err == "\"from\" should not be present with \"resolver\""),
4976
4977 test_cml_resolver_missing_path(
4978 r##"{
4979 "capabilities": [
4980 {
4981 "resolver": "pkg_resolver"
4982 }
4983 ]
4984 }"##,
4985 Err(Error::ValidateContext { err, .. }) if &err == "\"path\" should be present with \"resolver\""),
4986
4987 test_cml_runner_missing_path(
4988 r##"{
4989 "capabilities": [
4990 {
4991 "runner": "runrun"
4992 }
4993 ]
4994 }"##,
4995 Err(Error::ValidateContext { err, .. }) if &err == "\"path\" should be present with \"runner\""
4996 ),
4997
4998 test_cml_runner_extraneous_from(
4999 r##"{
5000 "capabilities": [
5001 {
5002 "runner": "a",
5003 "path": "/example",
5004 "from": "self"
5005 }
5006 ]
5007 }"##,
5008 Err(Error::ValidateContext { err, .. }) if &err == "\"from\" should not be present with \"runner\""
5009 ),
5010
5011 test_cml_service_multi_invalid_path(
5012 r##"{
5013 "capabilities": [
5014 {
5015 "service": ["a", "b", "c"],
5016 "path": "/minfs"
5017 }
5018 ]
5019 }"##,
5020 Err(Error::ValidateContext{ err, .. }) if &err == "\"path\" can only be specified when one `service` is supplied."
5021 ),
5022
5023 test_cml_protocol_multi_invalid_path(
5024 r##"{
5025 "capabilities": [
5026 {
5027 "protocol": ["a", "b", "c"],
5028 "path": "/minfs"
5029 }
5030 ]
5031 }"##,
5032 Err(Error::ValidateContext { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."),
5033
5034 test_cml_use_bad_duplicate_target_names(
5035 r##"{
5036 "use": [
5037 { "protocol": "fuchsia.component.Realm" },
5038 { "protocol": "fuchsia.component.Realm" }
5039 ]
5040 }"##,
5041 Err(Error::ValidateContexts { err, .. }) if &err == "\"/svc/fuchsia.component.Realm\" is a duplicate \"use\" target protocol"),
5042
5043 test_cml_use_disallows_nested_dirs_directory(
5044 r##"{
5045 "use": [
5046 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
5047 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] }
5048 ]
5049 }"##,
5050 Err(Error::ValidateContexts { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target directory \"/foo/bar/baz\""
5051 ),
5052 test_cml_use_disallows_nested_dirs_storage(
5053 r##"{
5054 "use": [
5055 { "storage": "foobar", "path": "/foo/bar" },
5056 { "storage": "foobarbaz", "path": "/foo/bar/baz" }
5057 ]
5058 }"##,
5059 Err(Error::ValidateContexts { err, .. }) if &err == "storage \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
5060 ),
5061 test_cml_use_disallows_nested_dirs_directory_and_storage(
5062 r##"{
5063 "use": [
5064 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
5065 { "storage": "foobarbaz", "path": "/foo/bar/baz" }
5066 ]
5067 }"##,
5068 Err(Error::ValidateContexts { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
5069 ),
5070 test_cml_use_disallows_common_prefixes_service(
5071 r##"{
5072 "use": [
5073 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
5074 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia" }
5075 ]
5076 }"##,
5077 Err(Error::ValidateContexts { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia\""
5078 ),
5079 test_cml_use_disallows_common_prefixes_protocol(
5080 r##"{
5081 "use": [
5082 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
5083 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia.2" }
5084 ]
5085 }"##,
5086 Err(Error::ValidateContexts { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia.2\""
5087 ),
5088
5089
5090 test_cml_use_invalid_from_with_service(
5091 json!({
5092 "use": [ { "service": "foo", "from": "debug" } ]
5093 }),
5094 Err(Error::ValidateContext { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
5095 ),
5096
5097 test_cml_use_runner_debug_ref(
5098 r##"{
5099 "use": [
5100 {
5101 "runner": "elf",
5102 "from": "debug"
5103 }
5104 ]
5105 }"##,
5106 Err(Error::ValidateContext { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
5107 ),
5108
5109 test_cml_availability_not_supported_for_event_streams(
5110 r##"{
5111 "use": [
5112 {
5113 "event_stream": ["destroyed"],
5114 "from": "parent",
5115 "availability": "optional"
5116 }
5117 ]
5118 }"##,
5119 Err(Error::ValidateContext { err, .. }) if &err == "\"availability\" cannot be used with \"event_stream\""
5120 ),
5121
5122 test_cml_use_disallows_filter_on_non_events(
5123 json!({
5124 "use": [
5125 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ], "filter": {"path": "/diagnostics"} },
5126 ],
5127 }),
5128 Err(Error::ValidateContext { err, .. }) if &err == "\"filter\" can only be used with \"event_stream\""
5129 ),
5130
5131 test_cml_use_from_with_storage(
5132 json!({
5133 "use": [ { "storage": "cache", "from": "parent" } ]
5134 }),
5135 Err(Error::ValidateContext { err, .. }) if &err == "\"from\" cannot be used with \"storage\""
5136 ),
5137
5138 test_cml_availability_not_supported_for_runner(
5139 r##"{
5140 "use": [
5141 {
5142 "runner": "destroyed",
5143 "from": "parent",
5144 "availability": "optional"
5145 }
5146 ]
5147 }"##,
5148 Err(Error::ValidateContext { err, .. }) if &err == "\"availability\" cannot be used with \"runner\""
5149 ),
5150
5151 test_cml_use_event_stream_self_ref(
5152 r##"{
5153 "use": [
5154 {
5155 "event_stream": ["started"],
5156 "path": "/svc/my_stream",
5157 "from": "self"
5158 }
5159 ]
5160 }"##,
5161 Err(Error::ValidateContext { err, .. }) if &err == "\"from: self\" cannot be used with \"event_stream\""
5162 ),
5163
5164 test_cml_use_runner_self_ref(
5165 r##"{
5166 "use": [
5167 {
5168 "runner": "elf",
5169 "from": "self"
5170 }
5171 ]
5172 }"##,
5173 Err(Error::ValidateContext { err, .. }) if &err == "\"from: self\" cannot be used with \"runner\""
5174 ),
5175
5176 test_cml_use_invalid_availability(
5177 r##"{
5178 "use": [
5179 {
5180 "protocol": "fuchsia.examples.Echo",
5181 "availability": "same_as_target"
5182 }
5183 ]
5184 }"##,
5185 Err(Error::ValidateContext { err, .. }) if &err == "\"availability: same_as_target\" cannot be used with use declarations"
5186 ),
5187
5188 test_cml_use_config_bad_string(
5189 r##"{
5190 "use": [
5191 {
5192 "config": "fuchsia.config.MyConfig",
5193 "key": "my_config",
5194 "type": "string"
5195 }
5196 ]
5197 }"##,
5198 Err(Error::ValidateContext { err, .. })
5199 if &err == "Config 'fuchsia.config.MyConfig' is type String but is missing field 'max_size'"
5200 ),
5201
5202 test_config_required_with_default(
5203 r##"{"use": [
5204 {
5205 "config": "fuchsia.config.MyConfig",
5206 "key": "my_config",
5207 "type": "bool",
5208 "default": "true"
5209 }
5210 ]}"##,
5211 Err(Error::ValidateContext {err, ..})
5212 if &err == "Config 'fuchsia.config.MyConfig' is required and has a default value"
5213 ),
5214
5215 test_cml_use_numbered_handle_and_path(
5216 json!({
5217 "use": [
5218 {
5219 "protocol": "foo",
5220 "path": "/svc/foo",
5221 "numbered_handle": 0xab
5222 }
5223 ]
5224 }),
5225 Err(Error::ValidateContext { err, .. }) if &err == "`path` and `numbered_handle` are incompatible"
5226 ),
5227
5228 test_cml_use_numbered_handle_not_protocol(
5229 json!({
5230 "use": [
5231 {
5232 "runner": "foo",
5233 "numbered_handle": 0xab
5234 }
5235 ]
5236 }),
5237 Err(Error::ValidateContext { err, .. }) if &err == "`numbered_handle` is only supported for `use protocol`"
5238 ),
5239
5240 test_cml_expose_invalid_subdir_to_framework(
5241 r##"{
5242 "capabilities": [
5243 {
5244 "directory": "foo",
5245 "rights": ["r*"],
5246 "path": "/foo"
5247 }
5248 ],
5249 "expose": [
5250 {
5251 "directory": "foo",
5252 "from": "self",
5253 "to": "framework",
5254 "subdir": "blob"
5255 }
5256 ],
5257 "children": [
5258 {
5259 "name": "child",
5260 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm"
5261 }
5262 ]
5263 }"##,
5264 Err(Error::ValidateContext { err, .. }) if &err == "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
5265 ),
5266 test_cml_expose_event_stream_multiple_as(
5267 r##"{
5268 "expose": [
5269 {
5270 "event_stream": ["started", "stopped"],
5271 "from" : "framework",
5272 "as": "something"
5273 }
5274 ]
5275 }"##,
5276 Err(Error::ValidateContext { err, .. }) if &err == "as cannot be used with multiple event streams"
5277 ),
5278
5279 test_cml_expose_event_stream_to_framework(
5280 r##"{
5281 "expose": [
5282 {
5283 "event_stream": ["started", "stopped"],
5284 "from" : "self",
5285 "to": "framework"
5286 }
5287 ]
5288 }"##,
5289 Err(Error::ValidateContext { err, .. }) if &err == "cannot expose an event_stream to framework"
5290 ),
5291
5292 test_cml_expose_event_stream_from_self(
5293 json!({
5294 "expose": [
5295 { "event_stream": ["started", "stopped"], "from" : "self" },
5296 ]
5297 }),
5298 Err(Error::ValidateContext { err, .. }) if &err == "Cannot expose event_streams from self"
5299 ),
5300
5301 test_cml_offer_event_stream_from_self(
5302 json!({
5303 "offer": [
5304 { "event_stream": ["started", "stopped"], "from" : "self", "to": "#self" },
5305 ]
5306 }),
5307 Err(Error::ValidateContext { err, .. }) if &err == "cannot offer an event_stream from self"
5308 ),
5309
5310 test_cml_rights_alias_star_expansion_collision(
5311 r##"{
5312 "use": [
5313 {
5314 "directory": "mydir",
5315 "path": "/mydir",
5316 "rights": ["w*", "x*"]
5317 }
5318 ]
5319 }"##,
5320 Err(Error::ValidateContext { err, .. }) if &err == "\"x*\" is duplicated in the rights clause."
5321 ),
5322
5323 test_cml_rights_alias_star_expansion_with_longform_collision(
5324 r##"{
5325 "use": [
5326 {
5327 "directory": "mydir",
5328 "path": "/mydir",
5329 "rights": ["r*", "read_bytes"]
5330 }
5331 ]
5332 }"##,
5333 Err(Error::ValidateContext { err, ..}) if &err == "\"read_bytes\" is duplicated in the rights clause."
5334 ),
5335
5336 test_cml_rights_use_invalid(
5337 json!({
5338 "use": [
5339 { "directory": "mydir", "path": "/mydir" },
5340 ]
5341 }),
5342 Err(Error::ValidateContexts { err, .. }) if &err == "This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer."
5343 ),
5344 test_cml_use_missing_props(
5345 json!({
5346 "use": [ { "path": "/svc/fuchsia.logger.Log" } ]
5347 }),
5348 Err(Error::ValidateContext { err, .. }) if &err == "`use` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"event_stream\", \"runner\", \"config\", \"dictionary\""
5349 ),
5350
5351
5352 test_cml_use_two_types_bad(
5353 r##"{"use": [
5354 {
5355 "protocol": "fuchsia.protocol.MyProtocol",
5356 "service": "fuchsia.service.MyService"
5357 }
5358 ]
5359 }"##,
5360 Err(Error::ValidateContext {err, ..})
5361 if &err == "use declaration has multiple capability types defined: [\"service\", \"protocol\"]"
5362 ),
5363 test_cml_expose_two_types_bad(
5364 r##"{"expose": [
5365 {
5366 "protocol": "fuchsia.protocol.MyProtocol",
5367 "service": "fuchsia.service.MyService",
5368 "from" : "self"
5369 }
5370 ]
5371 }"##,
5372 Err(Error::ValidateContext {err, ..})
5373 if &err == "expose declaration has multiple capability types defined: [\"service\", \"protocol\"]"
5374 ),
5375
5376 test_cml_expose_from_self(
5377 json!({
5378 "expose": [
5379 {
5380 "protocol": "foo_protocol",
5381 "from": "self",
5382 },
5383 {
5384 "protocol": [ "bar_protocol", "baz_protocol" ],
5385 "from": "self",
5386 },
5387 {
5388 "directory": "foo_directory",
5389 "from": "self",
5390 },
5391 {
5392 "runner": "foo_runner",
5393 "from": "self",
5394 },
5395 {
5396 "resolver": "foo_resolver",
5397 "from": "self",
5398 },
5399 ],
5400 "capabilities": [
5401 {
5402 "protocol": "foo_protocol",
5403 },
5404 {
5405 "protocol": "bar_protocol",
5406 },
5407 {
5408 "protocol": "baz_protocol",
5409 },
5410 {
5411 "directory": "foo_directory",
5412 "path": "/dir",
5413 "rights": [ "r*" ],
5414 },
5415 {
5416 "runner": "foo_runner",
5417 "path": "/svc/runner",
5418 },
5419 {
5420 "resolver": "foo_resolver",
5421 "path": "/svc/resolver",
5422 },
5423 ]
5424 }),
5425 Ok(())
5426 ),
5427 test_cml_expose_protocol_from_self_missing(
5428 json!({
5429 "expose": [
5430 {
5431 "protocol": "pkg_protocol",
5432 "from": "self",
5433 },
5434 ],
5435 }),
5436 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"pkg_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
5437 ),
5438 test_cml_expose_protocol_from_self_missing_multiple(
5439 json!({
5440 "expose": [
5441 {
5442 "protocol": [ "foo_protocol", "bar_protocol" ],
5443 "from": "self",
5444 },
5445 ],
5446 }),
5447 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
5448 ),
5449 test_cml_expose_directory_from_self_missing(
5450 json!({
5451 "expose": [
5452 {
5453 "directory": "pkg_directory",
5454 "from": "self",
5455 },
5456 ],
5457 }),
5458 Err(Error::ValidateContext { err, .. }) if &err == "directory \"pkg_directory\" is exposed from self, so it must be declared as a \"directory\" in \"capabilities\""
5459 ),
5460 test_cml_expose_service_from_self_missing(
5461 json!({
5462 "expose": [
5463 {
5464 "service": "pkg_service",
5465 "from": "self",
5466 },
5467 ],
5468 }),
5469 Err(Error::ValidateContext { err, .. }) if &err == "service \"pkg_service\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\""
5470 ),
5471 test_cml_expose_runner_from_self_missing(
5472 json!({
5473 "expose": [
5474 {
5475 "runner": "dart",
5476 "from": "self",
5477 },
5478 ],
5479 }),
5480 Err(Error::ValidateContext { err, .. }) if &err == "runner \"dart\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\""
5481 ),
5482 test_cml_expose_resolver_from_self_missing(
5483 json!({
5484 "expose": [
5485 {
5486 "resolver": "pkg_resolver",
5487 "from": "self",
5488 },
5489 ],
5490 }),
5491 Err(Error::ValidateContext { err, .. }) if &err == "resolver \"pkg_resolver\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\""
5492 ),
5493 test_cml_expose_from_self_missing_dictionary(
5494 json!({
5495 "expose": [
5496 {
5497 "protocol": "foo_protocol",
5498 "from": "self/dict/inner",
5499 },
5500 ],
5501 }),
5502 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is exposed from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
5503 ),
5504
5505 test_cml_expose_from_dictionary_parent(
5506 json!({
5507 "expose": [
5508 {
5509 "protocol": "pkg_protocol",
5510 "from": "parent/a",
5511 },
5512 ],
5513 }),
5514 Err(Error::ValidateContext { err, .. }) if &err == "`expose` dictionary path must begin with `self` or `#<child-name>`"
5515 ),
5516 test_cml_expose_protocol_from_collection_invalid(
5517 json!({
5518 "collections": [ {
5519 "name": "coll",
5520 "durability": "transient",
5521 } ],
5522 "expose": [
5523 { "protocol": "fuchsia.logger.Log", "from": "#coll" },
5524 ]
5525 }),
5526 Err(Error::ValidateContext { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\" or \"capabilities\""
5527 ),
5528 test_cml_expose_directory_from_collection_invalid(
5529 json!({
5530 "collections": [ {
5531 "name": "coll",
5532 "durability": "transient",
5533 } ],
5534 "expose": [
5535 { "directory": "temp", "from": "#coll" },
5536 ]
5537 }),
5538 Err(Error::ValidateContext { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
5539 ),
5540 test_cml_expose_runner_from_collection_invalid(
5541 json!({
5542 "collections": [ {
5543 "name": "coll",
5544 "durability": "transient",
5545 } ],
5546 "expose": [
5547 { "runner": "elf", "from": "#coll" },
5548 ]
5549 }),
5550 Err(Error::ValidateContext { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
5551 ),
5552 test_cml_expose_resolver_from_collection_invalid(
5553 json!({
5554 "collections": [ {
5555 "name": "coll",
5556 "durability": "transient",
5557 } ],
5558 "expose": [
5559 { "resolver": "base", "from": "#coll" },
5560 ]
5561 }),
5562 Err(Error::ValidateContext { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
5563 ),
5564
5565 test_cml_offer_from_self(
5566 json!({
5567 "offer": [
5568 {
5569 "protocol": "foo_protocol",
5570 "from": "self",
5571 "to": [ "#modular" ],
5572 },
5573 {
5574 "protocol": [ "bar_protocol", "baz_protocol" ],
5575 "from": "self",
5576 "to": [ "#modular" ],
5577 },
5578 {
5579 "directory": "foo_directory",
5580 "from": "self",
5581 "to": [ "#modular" ],
5582 },
5583 {
5584 "runner": "foo_runner",
5585 "from": "self",
5586 "to": [ "#modular" ],
5587 },
5588 {
5589 "resolver": "foo_resolver",
5590 "from": "self",
5591 "to": [ "#modular" ],
5592 },
5593 ],
5594 "children": [
5595 {
5596 "name": "modular",
5597 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5598 },
5599 ],
5600 "capabilities": [
5601 {
5602 "protocol": "foo_protocol",
5603 },
5604 {
5605 "protocol": "bar_protocol",
5606 },
5607 {
5608 "protocol": "baz_protocol",
5609 },
5610 {
5611 "directory": "foo_directory",
5612 "path": "/dir",
5613 "rights": [ "r*" ],
5614 },
5615 {
5616 "runner": "foo_runner",
5617 "path": "/svc/fuchsia.sys2.ComponentRunner",
5618 },
5619 {
5620 "resolver": "foo_resolver",
5621 "path": "/svc/fuchsia.component.resolution.Resolver",
5622 },
5623 ]
5624 }),
5625 Ok(())
5626 ),
5627 test_cml_offer_service_from_self_missing(
5628 json!({
5629 "offer": [
5630 {
5631 "service": "pkg_service",
5632 "from": "self",
5633 "to": [ "#modular" ],
5634 },
5635 ],
5636 "children": [
5637 {
5638 "name": "modular",
5639 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5640 },
5641 ],
5642 }),
5643 Err(Error::ValidateContext { err, .. }) if &err == "service \"pkg_service\" is offered from self, so it must be declared as a \"service\" in \"capabilities\""
5644 ),
5645 test_cml_offer_protocol_from_self_missing(
5646 json!({
5647 "offer": [
5648 {
5649 "protocol": "pkg_protocol",
5650 "from": "self",
5651 "to": [ "#modular" ],
5652 },
5653 ],
5654 "children": [
5655 {
5656 "name": "modular",
5657 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5658 },
5659 ],
5660 }),
5661 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"pkg_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
5662 ),
5663 test_cml_offer_protocol_from_self_missing_multiple(
5664 json!({
5665 "offer": [
5666 {
5667 "protocol": [ "foo_protocol", "bar_protocol" ],
5668 "from": "self",
5669 "to": [ "#modular" ],
5670 },
5671 ],
5672 "children": [
5673 {
5674 "name": "modular",
5675 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5676 },
5677 ],
5678 }),
5679 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
5680 ),
5681 test_cml_offer_directory_from_self_missing(
5682 json!({
5683 "offer": [
5684 {
5685 "directory": "pkg_directory",
5686 "from": "self",
5687 "to": [ "#modular" ],
5688 },
5689 ],
5690 "children": [
5691 {
5692 "name": "modular",
5693 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5694 },
5695 ],
5696 }),
5697 Err(Error::ValidateContext { err, .. }) if &err == "directory \"pkg_directory\" is offered from self, so it must be declared as a \"directory\" in \"capabilities\""
5698 ),
5699 test_cml_offer_runner_from_self_missing(
5700 json!({
5701 "offer": [
5702 {
5703 "runner": "dart",
5704 "from": "self",
5705 "to": [ "#modular" ],
5706 },
5707 ],
5708 "children": [
5709 {
5710 "name": "modular",
5711 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5712 },
5713 ],
5714 }),
5715 Err(Error::ValidateContext { err, .. }) if &err == "runner \"dart\" is offered from self, so it must be declared as a \"runner\" in \"capabilities\""
5716 ),
5717 test_cml_offer_resolver_from_self_missing(
5718 json!({
5719 "offer": [
5720 {
5721 "resolver": "pkg_resolver",
5722 "from": "self",
5723 "to": [ "#modular" ],
5724 },
5725 ],
5726 "children": [
5727 {
5728 "name": "modular",
5729 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5730 },
5731 ],
5732 }),
5733 Err(Error::ValidateContext { err, .. }) if &err == "resolver \"pkg_resolver\" is offered from self, so it must be declared as a \"resolver\" in \"capabilities\""
5734 ),
5735 test_cml_offer_storage_from_self_missing(
5736 json!({
5737 "offer": [
5738 {
5739 "storage": "cache",
5740 "from": "self",
5741 "to": [ "#echo_server" ],
5742 },
5743 ],
5744 "children": [
5745 {
5746 "name": "echo_server",
5747 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
5748 },
5749 ],
5750 }),
5751 Err(Error::ValidateContext { err, .. }) if &err == "storage \"cache\" is offered from self, so it must be declared as a \"storage\" in \"capabilities\""
5752 ),
5753 test_cml_offer_from_self_missing_dictionary(
5754 json!({
5755 "offer": [
5756 {
5757 "protocol": "foo_protocol",
5758 "from": "self/dict/inner",
5759 "to": [ "#modular" ],
5760 },
5761 ],
5762 "children": [
5763 {
5764 "name": "modular",
5765 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5766 },
5767 ],
5768 }),
5769 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is offered from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
5770 ),
5771
5772 test_cml_storage_offer_from_child(
5773 r##"{
5774 "offer": [
5775 {
5776 "storage": "cache",
5777 "from": "#storage_provider",
5778 "to": [ "#echo_server" ]
5779 }
5780 ],
5781 "children": [
5782 {
5783 "name": "echo_server",
5784 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm"
5785 },
5786 {
5787 "name": "storage_provider",
5788 "url": "fuchsia-pkg://fuchsia.com/storage_provider#meta/storage_provider.cm"
5789 }
5790 ]
5791 }"##,
5792 Err(Error::ValidateContexts { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
5793 ),
5794
5795 test_cml_offer_storage_from_collection_invalid(
5796 r##"{
5797 "collections": [ {
5798 "name": "coll",
5799 "durability": "transient"
5800 } ],
5801 "children": [ {
5802 "name": "echo_server",
5803 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
5804 } ],
5805 "offer": [
5806 { "storage": "cache", "from": "#coll", "to": [ "#echo_server" ] }
5807 ]
5808 }"##,
5809 Err(Error::ValidateContexts { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
5810 ),
5811
5812 test_cml_children_bad_environment(
5813 json!({
5814 "children": [
5815 {
5816 "name": "logger",
5817 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5818 "environment": "parent",
5819 }
5820 ]
5821 }),
5822 Err(Error::Parse { err, .. }) if err.starts_with("invalid value: string \"parent\", expected \"#<environment-name>\"")
5823 ),
5824 test_cml_children_environment(
5825 json!({
5826 "children": [
5827 {
5828 "name": "logger",
5829 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
5830 "environment": "#foo_env",
5831 }
5832 ],
5833 "environments": [
5834 {
5835 "name": "foo_env",
5836 }
5837 ]
5838 }),
5839 Ok(())
5840 ),
5841 test_cml_collections_bad_environment(
5842 json!({
5843 "collections": [
5844 {
5845 "name": "tests",
5846 "durability": "transient",
5847 "environment": "parent",
5848 }
5849 ]
5850 }),
5851 Err(Error::Parse { err, .. }) if err.starts_with("invalid value: string \"parent\", expected \"#<environment-name>\"")
5852 ),
5853 test_cml_collections_environment(
5854 json!({
5855 "collections": [
5856 {
5857 "name": "tests",
5858 "durability": "transient",
5859 "environment": "#foo_env",
5860 }
5861 ],
5862 "environments": [
5863 {
5864 "name": "foo_env",
5865 }
5866 ]
5867 }),
5868 Ok(())
5869 ),
5870
5871 test_cml_environment_timeout(
5872 json!({
5873 "environments": [
5874 {
5875 "name": "foo_env",
5876 "__stop_timeout_ms": 10000,
5877 }
5878 ]
5879 }),
5880 Ok(())
5881 ),
5882 test_cml_environment_bad_timeout(
5883 json!({
5884 "environments": [
5885 {
5886 "name": "foo_env",
5887 "__stop_timeout_ms": -3,
5888 }
5889 ]
5890 }),
5891 Err(Error::Parse { err, .. }) if err.starts_with("invalid value: integer `-3`, expected an unsigned 32-bit integer")
5892 ),
5893
5894 test_cml_environment_debug(
5895 json!({
5896 "capabilities": [
5897 {
5898 "protocol": "fuchsia.logger.Log2",
5899 },
5900 ],
5901 "environments": [
5902 {
5903 "name": "foo_env",
5904 "extends": "realm",
5905 "debug": [
5906 {
5907 "protocol": "fuchsia.module.Module",
5908 "from": "#modular",
5909 },
5910 {
5911 "protocol": "fuchsia.logger.OtherLog",
5912 "from": "parent",
5913 },
5914 {
5915 "protocol": "fuchsia.logger.Log2",
5916 "from": "self",
5917 },
5918 ]
5919 }
5920 ],
5921 "children": [
5922 {
5923 "name": "modular",
5924 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5925 },
5926 ],
5927 }),
5928 Ok(())
5929 ),
5930
5931 test_cml_environment_debug_missing_capability(
5932 json!({
5933 "environments": [
5934 {
5935 "name": "foo_env",
5936 "extends": "realm",
5937 "debug": [
5938 {
5939 "protocol": "fuchsia.module.Module",
5940 "from": "#modular",
5941 },
5942 {
5943 "protocol": "fuchsia.logger.OtherLog",
5944 "from": "parent",
5945 },
5946 {
5947 "protocol": "fuchsia.logger.Log2",
5948 "from": "self",
5949 },
5950 ]
5951 }
5952 ],
5953 "children": [
5954 {
5955 "name": "modular",
5956 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5957 },
5958 ],
5959 }),
5960 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"fuchsia.logger.Log2\" is registered as debug from self, so it must be declared as a \"protocol\" in \"capabilities\""
5961 ),
5962
5963 test_cml_environment_invalid_from_child(
5964 json!({
5965 "capabilities": [
5966 {
5967 "protocol": "fuchsia.logger.Log2",
5968 },
5969 ],
5970 "environments": [
5971 {
5972 "name": "foo_env",
5973 "extends": "realm",
5974 "debug": [
5975 {
5976 "protocol": "fuchsia.module.Module",
5977 "from": "#missing",
5978 },
5979 {
5980 "protocol": "fuchsia.logger.OtherLog",
5981 "from": "parent",
5982 },
5983 {
5984 "protocol": "fuchsia.logger.Log2",
5985 "from": "self",
5986 },
5987 ]
5988 }
5989 ],
5990 "children": [
5991 {
5992 "name": "modular",
5993 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
5994 },
5995 ],
5996 }),
5997 Err(Error::ValidateContext { err, .. }) if &err == "\"debug\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
5998 ),
5999
6000 test_cml_facets(
6001 json!({
6002 "facets": {
6003 "metadata": {
6004 "title": "foo",
6005 "authors": [ "me", "you" ],
6006 "year": 2018
6007 }
6008 }
6009 }),
6010 Ok(())
6011 ),
6012 test_cml_facets_wrong_type(
6013 json!({
6014 "facets": 55
6015 }),
6016 Err(Error::Parse { err, .. }) if err.starts_with("invalid type: integer `55`, expected a map")
6017 ),
6018
6019 test_cml_use_from_self(
6020 json!({
6021 "use": [
6022 {
6023 "protocol": [ "bar_protocol", "baz_protocol" ],
6024 "from": "self",
6025 },
6026 {
6027 "directory": "foo_directory",
6028 "from": "self",
6029 "path": "/dir",
6030 "rights": [ "r*" ],
6031 },
6032 {
6033 "service": "foo_service",
6034 "from": "self",
6035 },
6036 {
6037 "config": "foo_config",
6038 "type": "bool",
6039 "key": "k",
6040 "from": "self",
6041 },
6042 ],
6043 "capabilities": [
6044 {
6045 "protocol": "bar_protocol",
6046 },
6047 {
6048 "protocol": "baz_protocol",
6049 },
6050 {
6051 "directory": "foo_directory",
6052 "path": "/dir",
6053 "rights": [ "r*" ],
6054 },
6055 {
6056 "service": "foo_service",
6057 },
6058 {
6059 "config": "foo_config",
6060 "type": "bool",
6061 },
6062 ]
6063 }),
6064 Ok(())
6065 ),
6066 test_cml_use_protocol_from_self_missing(
6067 json!({
6068 "use": [
6069 {
6070 "protocol": "foo_protocol",
6071 "from": "self",
6072 },
6073 ],
6074 }),
6075 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is used from self, so it must be declared as a \"protocol\" in \"capabilities\""
6076 ),
6077
6078 test_cml_use_directory_from_self_missing(
6079 json!({
6080 "use": [
6081 {
6082 "directory": "foo_directory",
6083 "from": "self",
6084 },
6085 ],
6086 }),
6087 Err(Error::ValidateContext { err, .. }) if &err == "directory \"foo_directory\" is used from self, so it must be declared as a \"directory\" in \"capabilities\""
6088 ),
6089 test_cml_use_service_from_self_missing(
6090 json!({
6091 "use": [
6092 {
6093 "service": "foo_service",
6094 "from": "self",
6095 },
6096 ],
6097 }),
6098 Err(Error::ValidateContext { err, .. }) if &err == "service \"foo_service\" is used from self, so it must be declared as a \"service\" in \"capabilities\""
6099 ),
6100 test_cml_use_config_from_self_missing(
6101 json!({
6102 "use": [
6103 {
6104 "config": "foo_config",
6105 "from": "self",
6106 },
6107 ],
6108 }),
6109 Err(Error::ValidateContext { err, .. }) if &err == "config \"foo_config\" is used from self, so it must be declared as a \"config\" in \"capabilities\""
6110 ),
6111 test_cml_use_from_self_missing_dictionary(
6112 json!({
6113 "use": [
6114 {
6115 "protocol": "foo_protocol",
6116 "from": "self/dict/inner",
6117 },
6118 ],
6119 }),
6120 Err(Error::ValidateContext { err, .. }) if &err == "protocol \"foo_protocol\" is used from \"self/dict/inner\", so \"dict\" must be declared as a \"dictionary\" in \"capabilities\""
6121 ),
6122 test_cml_use_event_stream_duplicate(
6123 json!({
6124 "use": [
6125 { "event_stream": ["started", "started"], "from" : "parent" },
6126 ]
6127 }),
6128 Err(Error::Parse { err, .. }) if err.starts_with("invalid value: array with duplicate element, expected a name or nonempty array of names, with unique elements")
6129 ),
6130 test_cml_use_event_stream_overlapping_path(
6131 json!({
6132 "use": [
6133 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
6134 {
6135 "event_stream": ["started"],
6136 "path": "/foo/bar/baz/er",
6137 "from": "parent",
6138 },
6139 ],
6140 }),
6141 Err(Error::ValidateContexts { err, .. }) if &err == "directory \"/foo/bar/baz\" is a prefix of \"use\" target event_stream \"/foo/bar/baz/er\""
6142 ),
6143 test_cml_use_event_stream_invalid_path(
6144 json!({
6145 "use": [
6146 {
6147 "event_stream": ["started"],
6148 "path": "my_stream",
6149 "from": "parent",
6150 },
6151 ],
6152 }),
6153 Err(Error::Parse { err, .. }) if err.starts_with("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")
6154 ),
6155
6156 test_cml_offer_bad_subdir(
6157 json!({
6158 "offer": [
6159 {
6160 "directory": "index",
6161 "subdir": "/",
6162 "from": "parent",
6163 "to": [ "#modular" ],
6164 },
6165 ],
6166 "children": [
6167 {
6168 "name": "modular",
6169 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
6170 }
6171 ]
6172 }),
6173 Err(Error::Parse { err, .. }) if err.starts_with("invalid value: string \"/\", expected a path with no leading `/` and non-empty segments")
6174 ),
6175
6176 }
6177
6178 test_validate_cml! {
6179 test_cml_empty_include(
6181 json!(
6182 {
6183 "include": [],
6184 }
6185 ),
6186 Ok(())
6187 ),
6188 test_cml_some_include(
6189 json!(
6190 {
6191 "include": [ "some.cml" ],
6192 }
6193 ),
6194 Ok(())
6195 ),
6196 test_cml_couple_of_include(
6197 json!(
6198 {
6199 "include": [ "some1.cml", "some2.cml" ],
6200 }
6201 ),
6202 Ok(())
6203 ),
6204
6205 test_cml_empty_json_no_span(
6207 json!({}),
6208 Ok(())
6209 ),
6210 test_cml_program(
6211 json!(
6212 {
6213 "program": {
6214 "runner": "elf",
6215 "binary": "bin/app",
6216 },
6217 }
6218 ),
6219 Ok(())
6220 ),
6221 test_cml_program_use_runner(
6222 json!(
6223 {
6224 "program": {
6225 "binary": "bin/app",
6226 },
6227 "use": [
6228 { "runner": "elf", "from": "parent" }
6229 ]
6230 }
6231 ),
6232 Ok(())
6233 ),
6234 test_cml_program_use_runner_conflict(
6235 json!(
6236 {
6237 "program": {
6238 "runner": "elf",
6239 "binary": "bin/app",
6240 },
6241 "use": [
6242 { "runner": "elf", "from": "parent" }
6243 ]
6244 }
6245 ),
6246 Err(Error::Validate { err, .. }) if &err ==
6247 "Component has conflicting runners in `program` block and `use` block."
6248 ),
6249 test_cml_program_no_runner(
6250 json!({"program": { "binary": "bin/app" }}),
6251 Err(Error::Validate { err, .. }) if &err ==
6252 "Component has a `program` block defined, but doesn't specify a `runner`. \
6253 Components need to use a runner to actually execute code."
6254 ),
6255
6256 test_cml_use(
6258 json!({
6259 "use": [
6260 { "protocol": "CoolFonts", "path": "/svc/MyFonts" },
6261 { "protocol": "CoolFonts2", "path": "/svc/MyFonts2", "from": "parent/dict" },
6262 { "protocol": "fuchsia.test.hub.HubReport", "from": "framework" },
6263 { "protocol": "fuchsia.sys2.StorageAdmin", "from": "#data-storage" },
6264 { "protocol": ["fuchsia.ui.scenic.Scenic", "fuchsia.logger.LogSink"] },
6265 {
6266 "directory": "assets",
6267 "path": "/data/assets",
6268 "rights": ["rw*"],
6269 },
6270 {
6271 "directory": "config",
6272 "from": "parent",
6273 "path": "/data/config",
6274 "rights": ["rx*"],
6275 "subdir": "fonts/all",
6276 },
6277 { "storage": "data", "path": "/example" },
6278 { "storage": "cache", "path": "/tmp" },
6279 {
6280 "event_stream": ["started", "stopped", "running"],
6281 "scope":["#test"],
6282 "path":"/svc/testpath",
6283 "from":"parent",
6284 },
6285 { "runner": "usain", "from": "parent" },
6286 ],
6287 "capabilities": [
6288 {
6289 "storage": "data-storage",
6290 "from": "parent",
6291 "backing_dir": "minfs",
6292 "storage_id": "static_instance_id_or_moniker",
6293 }
6294 ]
6295 }),
6296 Ok(())
6297 ),
6298 test_cml_expose_event_stream_multiple_as_no_span(
6299 json!({
6300 "expose": [
6301 {
6302 "event_stream": ["started", "stopped"],
6303 "from" : "framework",
6304 "as": "something"
6305 },
6306 ]
6307 }),
6308 Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple event streams"
6309 ),
6310 test_cml_offer_event_stream_capability_requested_not_from_framework(
6311 json!({
6312 "offer": [
6313 {
6314 "event_stream": ["capability_requested", "stopped"],
6315 "from" : "parent",
6316 "to": "#something"
6317 },
6318 ]
6319 }),
6320 Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target from \"parent\" but \"#something\" does not appear in \"children\" or \"collections\""
6321 ),
6322 test_cml_offer_event_stream_capability_requested_with_filter(
6323 json!({
6324 "offer": [
6325 {
6326 "event_stream": "capability_requested",
6327 "from" : "framework",
6328 "to": "#something",
6329 },
6330 ]
6331 }),
6332 Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target from \"framework\" but \"#something\" does not appear in \"children\" or \"collections\""
6333 ),
6334 test_cml_offer_event_stream_multiple_as(
6335 json!({
6336 "offer": [
6337 {
6338 "event_stream": ["started", "stopped"],
6339 "from" : "framework",
6340 "to": "#self",
6341 "as": "something"
6342 },
6343 ]
6344 }),
6345 Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple events"
6346 ),
6347 test_cml_expose_event_stream_from_self_no_span(
6348 json!({
6349 "expose": [
6350 { "event_stream": ["started", "stopped"], "from" : "self" },
6351 ]
6352 }),
6353 Err(Error::Validate { err, .. }) if &err == "Cannot expose event_streams from self"
6354 ),
6355 test_cml_offer_event_stream_from_self_no_span(
6356 json!({
6357 "offer": [
6358 { "event_stream": ["started", "stopped"], "from" : "self", "to": "#self" },
6359 ]
6360 }),
6361 Err(Error::Validate { err, .. }) if &err == "cannot offer an event_stream from self"
6362 ),
6363 test_cml_offer_event_stream_from_anything_else(
6364 json!({
6365 "offer": [
6366 {
6367 "event_stream": ["started", "stopped"],
6368 "from" : "framework",
6369 "to": "#self"
6370 },
6371 ]
6372 }),
6373 Err(Error::Validate { err, .. }) if &err == "\"#self\" is an \"offer\" target from \"framework\" but \"#self\" does not appear in \"children\" or \"collections\""
6374 ),
6375 test_cml_expose_event_stream_to_framework_no_span(
6376 json!({
6377 "expose": [
6378 {
6379 "event_stream": ["started", "stopped"],
6380 "from" : "self",
6381 "to": "framework"
6382 },
6383 ]
6384 }),
6385 Err(Error::Validate { err, .. }) if &err == "cannot expose an event_stream to framework"
6386 ),
6387 test_cml_expose_event_stream_scope_invalid_component(
6388 json!({
6389 "expose": [
6390 {
6391 "event_stream": ["started", "stopped"],
6392 "from" : "framework",
6393 "scope":["#invalid_component"]
6394 },
6395 ]
6396 }),
6397 Err(Error::Validate { err, .. }) if &err == "event_stream scope invalid_component did not match a component or collection in this .cml file."
6398 ),
6399
6400 test_cml_use_from_self_no_span(
6401 json!({
6402 "use": [
6403 {
6404 "protocol": [ "bar_protocol", "baz_protocol" ],
6405 "from": "self",
6406 },
6407 {
6408 "directory": "foo_directory",
6409 "from": "self",
6410 "path": "/dir",
6411 "rights": [ "r*" ],
6412 },
6413 {
6414 "service": "foo_service",
6415 "from": "self",
6416 },
6417 {
6418 "config": "foo_config",
6419 "type": "bool",
6420 "key": "k",
6421 "from": "self",
6422 },
6423 ],
6424 "capabilities": [
6425 {
6426 "protocol": "bar_protocol",
6427 },
6428 {
6429 "protocol": "baz_protocol",
6430 },
6431 {
6432 "directory": "foo_directory",
6433 "path": "/dir",
6434 "rights": [ "r*" ],
6435 },
6436 {
6437 "service": "foo_service",
6438 },
6439 {
6440 "config": "foo_config",
6441 "type": "bool",
6442 },
6443 ]
6444 }),
6445 Ok(())
6446 ),
6447 test_cml_use_protocol_from_self_missing_no_span(
6448 json!({
6449 "use": [
6450 {
6451 "protocol": "foo_protocol",
6452 "from": "self",
6453 },
6454 ],
6455 }),
6456 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is used from self, so it must be declared as a \"protocol\" in \"capabilities\""
6457 ),
6458 test_cml_use_numbered_handle_not_protocol_no_span(
6459 json!({
6460 "use": [
6461 {
6462 "runner": "foo",
6463 "numbered_handle": 0xab,
6464 },
6465 ],
6466 }),
6467 Err(Error::Validate { err, .. }) if &err == "`numbered_handle` is only supported for `use protocol`"
6468 ),
6469 test_cml_use_numbered_handle_and_path_no_span(
6470 json!({
6471 "use": [
6472 {
6473 "protocol": "foo",
6474 "path": "/svc/foo",
6475 "numbered_handle": 0xab,
6476 },
6477 ],
6478 }),
6479 Err(Error::Validate { err, .. }) if &err == "`path` and `numbered_handle` are incompatible"
6480 ),
6481 test_cml_use_directory_from_self_missing_no_span(
6482 json!({
6483 "use": [
6484 {
6485 "directory": "foo_directory",
6486 "from": "self",
6487 },
6488 ],
6489 }),
6490 Err(Error::Validate { err, .. }) if &err == "directory \"foo_directory\" is used from self, so it must be declared as a \"directory\" in \"capabilities\""
6491 ),
6492 test_cml_use_service_from_self_missing_no_span(
6493 json!({
6494 "use": [
6495 {
6496 "service": "foo_service",
6497 "from": "self",
6498 },
6499 ],
6500 }),
6501 Err(Error::Validate { err, .. }) if &err == "service \"foo_service\" is used from self, so it must be declared as a \"service\" in \"capabilities\""
6502 ),
6503 test_cml_use_config_from_self_missing_no_span(
6504 json!({
6505 "use": [
6506 {
6507 "config": "foo_config",
6508 "from": "self",
6509 },
6510 ],
6511 }),
6512 Err(Error::Validate { err, .. }) if &err == "config \"foo_config\" is used from self, so it must be declared as a \"config\" in \"capabilities\""
6513 ),
6514 test_cml_use_from_self_missing_dictionary_no_span(
6515 json!({
6516 "use": [
6517 {
6518 "protocol": "foo_protocol",
6519 "from": "self/dict/inner",
6520 },
6521 ],
6522 }),
6523 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\""
6524 ),
6525 test_cml_use_event_stream_duplicate_no_span(
6526 json!({
6527 "use": [
6528 { "event_stream": ["started", "started"], "from" : "parent" },
6529 ]
6530 }),
6531 Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a name or nonempty array of names, with unique elements"
6532 ),
6533 test_cml_use_event_stream_overlapping_path_no_span(
6534 json!({
6535 "use": [
6536 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
6537 {
6538 "event_stream": ["started"],
6539 "path": "/foo/bar/baz/er",
6540 "from": "parent",
6541 },
6542 ],
6543 }),
6544 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar/baz\" is a prefix of \"use\" target event_stream \"/foo/bar/baz/er\""
6545 ),
6546 test_cml_use_event_stream_invalid_path_no_span(
6547 json!({
6548 "use": [
6549 {
6550 "event_stream": ["started"],
6551 "path": "my_stream",
6552 "from": "parent",
6553 },
6554 ],
6555 }),
6556 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"
6557 ),
6558 test_cml_use_event_stream_self_ref_no_span(
6559 json!({
6560 "use": [
6561 {
6562 "event_stream": ["started"],
6563 "path": "/svc/my_stream",
6564 "from": "self",
6565 },
6566 ],
6567 }),
6568 Err(Error::Validate { err, .. }) if &err == "\"from: self\" cannot be used with \"event_stream\""
6569 ),
6570 test_cml_use_runner_debug_ref_no_span(
6571 json!({
6572 "use": [
6573 {
6574 "runner": "elf",
6575 "from": "debug",
6576 },
6577 ],
6578 }),
6579 Err(Error::Validate { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
6580 ),
6581 test_cml_use_runner_self_ref_no_span(
6582 json!({
6583 "use": [
6584 {
6585 "runner": "elf",
6586 "from": "self",
6587 },
6588 ],
6589 }),
6590 Err(Error::Validate { err, .. }) if &err == "\"from: self\" cannot be used with \"runner\""
6591 ),
6592 test_cml_use_missing_props_no_span(
6593 json!({
6594 "use": [ { "path": "/svc/fuchsia.logger.Log" } ]
6595 }),
6596 Err(Error::Validate { err, .. }) if &err == "`use` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"event_stream\", \"runner\", \"config\", \"dictionary\""
6597 ),
6598 test_cml_use_from_with_storage_no_span(
6599 json!({
6600 "use": [ { "storage": "cache", "from": "parent" } ]
6601 }),
6602 Err(Error::Validate { err, .. }) if &err == "\"from\" cannot be used with \"storage\""
6603 ),
6604 test_cml_use_invalid_from(
6605 json!({
6606 "use": [
6607 { "protocol": "CoolFonts", "from": "bad" }
6608 ]
6609 }),
6610 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"
6611 ),
6612 test_cml_use_invalid_from_dictionary(
6613 json!({
6614 "use": [
6615 { "protocol": "CoolFonts", "from": "bad/dict" }
6616 ]
6617 }),
6618 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"
6619 ),
6620 test_cml_use_from_missing_capability(
6621 json!({
6622 "use": [
6623 { "protocol": "fuchsia.sys2.Admin", "from": "#mystorage" }
6624 ]
6625 }),
6626 Err(Error::Validate { err, .. }) if &err == "\"use\" source \"#mystorage\" does not appear in \"children\" or \"capabilities\""
6627 ),
6628 test_cml_use_bad_path(
6629 json!({
6630 "use": [
6631 {
6632 "protocol": ["CoolFonts", "FunkyFonts"],
6633 "path": "/MyFonts"
6634 }
6635 ]
6636 }),
6637 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."
6638 ),
6639 test_cml_use_bad_duplicate_target_names_no_span(
6640 json!({
6641 "use": [
6642 { "protocol": "fuchsia.component.Realm" },
6643 { "protocol": "fuchsia.component.Realm" },
6644 ],
6645 }),
6646 Err(Error::Validate { err, .. }) if &err == "\"/svc/fuchsia.component.Realm\" is a duplicate \"use\" target protocol"
6647 ),
6648 test_cml_use_empty_protocols(
6649 json!({
6650 "use": [
6651 {
6652 "protocol": [],
6653 },
6654 ],
6655 }),
6656 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
6657 ),
6658 test_cml_use_bad_subdir(
6659 json!({
6660 "use": [
6661 {
6662 "directory": "config",
6663 "path": "/config",
6664 "from": "parent",
6665 "rights": [ "r*" ],
6666 "subdir": "/",
6667 },
6668 ]
6669 }),
6670 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
6671 ),
6672 test_cml_use_resolver_fails(
6673 json!({
6674 "use": [
6675 {
6676 "resolver": "pkg_resolver",
6677 },
6678 ]
6679 }),
6680 Err(Error::Parse { err, .. }) if err.starts_with("unknown field `resolver`, expected one of")
6681 ),
6682
6683 test_cml_use_disallows_nested_dirs_directory_no_span(
6684 json!({
6685 "use": [
6686 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
6687 { "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
6688 ],
6689 }),
6690 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target directory \"/foo/bar/baz\""
6691 ),
6692 test_cml_use_disallows_nested_dirs_storage_no_span(
6693 json!({
6694 "use": [
6695 { "storage": "foobar", "path": "/foo/bar" },
6696 { "storage": "foobarbaz", "path": "/foo/bar/baz" },
6697 ],
6698 }),
6699 Err(Error::Validate { err, .. }) if &err == "storage \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
6700 ),
6701 test_cml_use_disallows_nested_dirs_directory_and_storage_no_span(
6702 json!({
6703 "use": [
6704 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
6705 { "storage": "foobarbaz", "path": "/foo/bar/baz" },
6706 ],
6707 }),
6708 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
6709 ),
6710 test_cml_use_disallows_common_prefixes_service_no_span(
6711 json!({
6712 "use": [
6713 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
6714 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia" },
6715 ],
6716 }),
6717 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia\""
6718 ),
6719 test_cml_use_disallows_common_prefixes_protocol_no_span(
6720 json!({
6721 "use": [
6722 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
6723 { "protocol": "fuchsia", "path": "/foo/bar/fuchsia.2" },
6724 ],
6725 }),
6726 Err(Error::Validate { err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia.2\""
6727 ),
6728 test_cml_use_disallows_pkg_conflicts_for_directories(
6729 json!({
6730 "use": [
6731 { "directory": "dir", "path": "/pkg/dir", "rights": [ "r*" ] },
6732 ],
6733 }),
6734 Err(Error::Validate { err, .. }) if &err == "directory \"/pkg/dir\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
6735 ),
6736 test_cml_use_disallows_pkg_conflicts_for_protocols(
6737 json!({
6738 "use": [
6739 { "protocol": "prot", "path": "/pkg/protocol" },
6740 ],
6741 }),
6742 Err(Error::Validate { err, .. }) if &err == "protocol \"/pkg/protocol\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
6743 ),
6744 test_cml_use_disallows_pkg_conflicts_for_storage(
6745 json!({
6746 "use": [
6747 { "storage": "store", "path": "/pkg/storage" },
6748 ],
6749 }),
6750 Err(Error::Validate { err, .. }) if &err == "storage \"/pkg/storage\" conflicts with the protected path \"/pkg\", please use this capability with a different path"
6751 ),
6752 test_cml_use_disallows_filter_on_non_events_no_span(
6753 json!({
6754 "use": [
6755 { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ], "filter": {"path": "/diagnostics"} },
6756 ],
6757 }),
6758 Err(Error::Validate { err, .. }) if &err == "\"filter\" can only be used with \"event_stream\""
6759 ),
6760 test_cml_availability_not_supported_for_event_streams_no_span(
6761 json!({
6762 "use": [
6763 {
6764 "event_stream": ["destroyed"],
6765 "from": "parent",
6766 "availability": "optional",
6767 }
6768 ]
6769 }),
6770 Err(Error::Validate { err, .. }) if &err == "\"availability\" cannot be used with \"event_stream\""
6771 ),
6772 test_cml_use_from_parent_weak(
6773 json!({
6774 "use": [
6775 {
6776 "protocol": "fuchsia.parent.Protocol",
6777 "from": "parent",
6778 "dependency": "weak",
6779 },
6780 ],
6781 }),
6782 Err(Error::Validate { err, .. }) if &err == "Only `use` from children can have dependency: \"weak\""
6783 ),
6784 test_cml_use_from_child_weak(
6785 json!({
6786 "children": [
6787 {
6788 "name": "logger",
6789 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6790 },
6791 ],
6792 "use": [
6793 {
6794 "protocol": "fuchsia.child.Protocol",
6795 "from": "#logger",
6796 "dependency": "weak",
6797 },
6798 ],
6799 }),
6800 Ok(())
6801 ),
6802 test_cml_use_from_child_dictionary_weak(
6803 json!({
6804 "children": [
6805 {
6806 "name": "logger",
6807 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6808 },
6809 ],
6810 "use": [
6811 {
6812 "protocol": "fuchsia.child.Protocol",
6813 "from": "#logger/dictionary",
6814 "dependency": "weak",
6815 },
6816 ],
6817 }),
6818 Ok(())
6819 ),
6820 test_cml_use_numbered_handle_from_same_protocol_ok(
6821 json!({
6822 "use": [
6823 {
6824 "protocol": "foo",
6825 },
6826 {
6827 "protocol": "foo",
6828 "numbered_handle": 0xab,
6829 },
6830 {
6831 "protocol": "foo",
6832 "numbered_handle": 0xac,
6833 },
6834 ],
6835 }),
6836 Ok(())
6837 ),
6838 test_cml_use_numbered_handle_not_a_number(
6839 json!({
6840 "use": [
6841 {
6842 "protocol": "foo",
6843 "numbered_handle": "0xab",
6844 },
6845 ],
6846 }),
6847 Err(Error::Parse { err, .. }) if &err == "error parsing number"
6848 ),
6849 test_cml_use_numbered_handle_out_of_range(
6850 json!({
6851 "use": [
6852 {
6853 "protocol": "foo",
6854 "numbered_handle": 256,
6855 },
6856 ],
6857 }),
6858 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `256`, expected a uint8 from zircon/processargs.h"
6859 ),
6860
6861 test_cml_expose(
6863 json!({
6864 "expose": [
6865 {
6866 "protocol": "A",
6867 "from": "self",
6868 },
6869 {
6870 "protocol": ["B", "C"],
6871 "from": "self",
6872 },
6873 {
6874 "protocol": "D",
6875 "from": "#mystorage",
6876 },
6877 {
6878 "directory": "blobfs",
6879 "from": "self",
6880 "rights": ["r*"],
6881 "subdir": "blob",
6882 },
6883 { "directory": "data", "from": "framework" },
6884 { "runner": "elf", "from": "#logger", },
6885 { "resolver": "pkg_resolver", "from": "#logger" },
6886 ],
6887 "capabilities": [
6888 { "protocol": ["A", "B", "C"] },
6889 {
6890 "directory": "blobfs",
6891 "path": "/blobfs",
6892 "rights": ["rw*"],
6893 },
6894 {
6895 "storage": "mystorage",
6896 "from": "self",
6897 "backing_dir": "blobfs",
6898 "storage_id": "static_instance_id_or_moniker",
6899 }
6900 ],
6901 "children": [
6902 {
6903 "name": "logger",
6904 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
6905 },
6906 ]
6907 }),
6908 Ok(())
6909 ),
6910 test_cml_expose_all_valid_chars(
6911 json!({
6912 "expose": [
6913 {
6914 "protocol": "fuchsia.logger.Log",
6915 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-.",
6916 },
6917 ],
6918 "children": [
6919 {
6920 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
6921 "url": "https://www.google.com/gmail"
6922 },
6923 ],
6924 }),
6925 Ok(())
6926 ),
6927 test_cml_expose_missing_props(
6928 json!({
6929 "expose": [ {} ]
6930 }),
6931 Err(Error::Parse { err, .. }) if &err == "missing field `from`"
6932 ),
6933 test_cml_expose_missing_from(
6934 json!({
6935 "expose": [
6936 { "protocol": "fuchsia.logger.Log", "from": "#missing" },
6937 ],
6938 }),
6939 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
6940 ),
6941 test_cml_expose_duplicate_target_names(
6942 json!({
6943 "capabilities": [
6944 { "protocol": "logger" },
6945 ],
6946 "expose": [
6947 { "protocol": "logger", "from": "self", "as": "thing" },
6948 { "directory": "thing", "from": "#child" , "rights": ["rx*"] },
6949 ],
6950 "children": [
6951 {
6952 "name": "child",
6953 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
6954 },
6955 ],
6956 }),
6957 Err(Error::Validate { err, .. }) if &err == "\"thing\" is a duplicate \"expose\" target capability for \"parent\""
6958 ),
6959 test_cml_expose_invalid_multiple_from(
6960 json!({
6961 "capabilities": [
6962 { "protocol": "fuchsia.logger.Log" },
6963 ],
6964 "expose": [
6965 {
6966 "protocol": "fuchsia.logger.Log",
6967 "from": [ "self", "#logger" ],
6968 },
6969 ],
6970 "children": [
6971 {
6972 "name": "logger",
6973 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
6974 },
6975 ]
6976 }),
6977 Err(Error::Validate { err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses"
6978 ),
6979 test_cml_expose_from_missing_named_source(
6980 json!({
6981 "expose": [
6982 {
6983 "protocol": "fuchsia.logger.Log",
6984 "from": "#does-not-exist",
6985 },
6986 ],
6987 }),
6988 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\""
6989 ),
6990 test_cml_expose_bad_from(
6991 json!({
6992 "expose": [ {
6993 "protocol": "fuchsia.logger.Log", "from": "parent"
6994 } ]
6995 }),
6996 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected one or an array of \"framework\", \"self\", \"#<child-name>\", or a dictionary path"
6997 ),
6998 test_cml_expose_bad_as(
7000 json!({
7001 "expose": [
7002 {
7003 "protocol": ["A", "B"],
7004 "from": "#echo_server",
7005 "as": "thing"
7006 },
7007 ],
7008 "children": [
7009 {
7010 "name": "echo_server",
7011 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7012 }
7013 ]
7014 }),
7015 Err(Error::Validate { err, .. }) if &err == "\"as\" can only be specified when one `protocol` is supplied."
7016 ),
7017 test_cml_expose_empty_protocols(
7018 json!({
7019 "expose": [
7020 {
7021 "protocol": [],
7022 "from": "#child",
7023 "as": "thing"
7024 },
7025 ],
7026 "children": [
7027 {
7028 "name": "child",
7029 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
7030 },
7031 ],
7032 }),
7033 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
7034 ),
7035 test_cml_expose_bad_subdir(
7036 json!({
7037 "expose": [
7038 {
7039 "directory": "blobfs",
7040 "from": "self",
7041 "rights": ["r*"],
7042 "subdir": "/",
7043 },
7044 ]
7045 }),
7046 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
7047 ),
7048 test_cml_expose_invalid_subdir_to_framework_no_span(
7049 json!({
7050 "capabilities": [
7051 {
7052 "directory": "foo",
7053 "rights": ["r*"],
7054 "path": "/foo",
7055 },
7056 ],
7057 "expose": [
7058 {
7059 "directory": "foo",
7060 "from": "self",
7061 "to": "framework",
7062 "subdir": "blob",
7063 },
7064 ],
7065 "children": [
7066 {
7067 "name": "child",
7068 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
7069 },
7070 ],
7071 }),
7072 Err(Error::Validate { err, .. }) if &err == "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
7073 ),
7074 test_cml_expose_from_self_no_span(
7075 json!({
7076 "expose": [
7077 {
7078 "protocol": "foo_protocol",
7079 "from": "self",
7080 },
7081 {
7082 "protocol": [ "bar_protocol", "baz_protocol" ],
7083 "from": "self",
7084 },
7085 {
7086 "directory": "foo_directory",
7087 "from": "self",
7088 },
7089 {
7090 "runner": "foo_runner",
7091 "from": "self",
7092 },
7093 {
7094 "resolver": "foo_resolver",
7095 "from": "self",
7096 },
7097 ],
7098 "capabilities": [
7099 {
7100 "protocol": "foo_protocol",
7101 },
7102 {
7103 "protocol": "bar_protocol",
7104 },
7105 {
7106 "protocol": "baz_protocol",
7107 },
7108 {
7109 "directory": "foo_directory",
7110 "path": "/dir",
7111 "rights": [ "r*" ],
7112 },
7113 {
7114 "runner": "foo_runner",
7115 "path": "/svc/runner",
7116 },
7117 {
7118 "resolver": "foo_resolver",
7119 "path": "/svc/resolver",
7120 },
7121 ]
7122 }),
7123 Ok(())
7124 ),
7125 test_cml_expose_protocol_from_self_missing_no_span(
7126 json!({
7127 "expose": [
7128 {
7129 "protocol": "pkg_protocol",
7130 "from": "self",
7131 },
7132 ],
7133 }),
7134 Err(Error::Validate { err, .. }) if &err == "protocol \"pkg_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
7135 ),
7136 test_cml_expose_protocol_from_self_missing_multiple_no_span(
7137 json!({
7138 "expose": [
7139 {
7140 "protocol": [ "foo_protocol", "bar_protocol" ],
7141 "from": "self",
7142 },
7143 ],
7144 }),
7145 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\""
7146 ),
7147 test_cml_expose_directory_from_self_missing_no_span(
7148 json!({
7149 "expose": [
7150 {
7151 "directory": "pkg_directory",
7152 "from": "self",
7153 },
7154 ],
7155 }),
7156 Err(Error::Validate { err, .. }) if &err == "directory \"pkg_directory\" is exposed from self, so it must be declared as a \"directory\" in \"capabilities\""
7157 ),
7158 test_cml_expose_service_from_self_missing_no_span(
7159 json!({
7160 "expose": [
7161 {
7162 "service": "pkg_service",
7163 "from": "self",
7164 },
7165 ],
7166 }),
7167 Err(Error::Validate { err, .. }) if &err == "service \"pkg_service\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\""
7168 ),
7169 test_cml_expose_runner_from_self_missing_no_span(
7170 json!({
7171 "expose": [
7172 {
7173 "runner": "dart",
7174 "from": "self",
7175 },
7176 ],
7177 }),
7178 Err(Error::Validate { err, .. }) if &err == "runner \"dart\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\""
7179 ),
7180 test_cml_expose_resolver_from_self_missing_no_span(
7181 json!({
7182 "expose": [
7183 {
7184 "resolver": "pkg_resolver",
7185 "from": "self",
7186 },
7187 ],
7188 }),
7189 Err(Error::Validate { err, .. }) if &err == "resolver \"pkg_resolver\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\""
7190 ),
7191 test_cml_expose_from_self_missing_dictionary_no_span(
7192 json!({
7193 "expose": [
7194 {
7195 "protocol": "foo_protocol",
7196 "from": "self/dict/inner",
7197 },
7198 ],
7199 }),
7200 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\""
7201 ),
7202 test_cml_expose_from_dictionary_invalid(
7203 json!({
7204 "expose": [
7205 {
7206 "protocol": "pkg_protocol",
7207 "from": "bad/a",
7208 },
7209 ],
7210 }),
7211 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"
7212 ),
7213 test_cml_expose_from_dictionary_parent_no_span(
7214 json!({
7215 "expose": [
7216 {
7217 "protocol": "pkg_protocol",
7218 "from": "parent/a",
7219 },
7220 ],
7221 }),
7222 Err(Error::Validate { err, .. }) if &err == "`expose` dictionary path must begin with `self` or `#<child-name>`"
7223 ),
7224 test_cml_expose_protocol_from_collection_invalid_no_span(
7225 json!({
7226 "collections": [ {
7227 "name": "coll",
7228 "durability": "transient",
7229 } ],
7230 "expose": [
7231 { "protocol": "fuchsia.logger.Log", "from": "#coll" },
7232 ]
7233 }),
7234 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\" or \"capabilities\""
7235 ),
7236 test_cml_expose_directory_from_collection_invalid_no_span(
7237 json!({
7238 "collections": [ {
7239 "name": "coll",
7240 "durability": "transient",
7241 } ],
7242 "expose": [
7243 { "directory": "temp", "from": "#coll" },
7244 ]
7245 }),
7246 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
7247 ),
7248 test_cml_expose_runner_from_collection_invalid_no_span(
7249 json!({
7250 "collections": [ {
7251 "name": "coll",
7252 "durability": "transient",
7253 } ],
7254 "expose": [
7255 { "runner": "elf", "from": "#coll" },
7256 ]
7257 }),
7258 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
7259 ),
7260 test_cml_expose_resolver_from_collection_invalid_no_span(
7261 json!({
7262 "collections": [ {
7263 "name": "coll",
7264 "durability": "transient",
7265 } ],
7266 "expose": [
7267 { "resolver": "base", "from": "#coll" },
7268 ]
7269 }),
7270 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\""
7271 ),
7272 test_cml_offer(
7274 json!({
7275 "offer": [
7276 {
7277 "protocol": "fuchsia.fonts.LegacyProvider",
7278 "from": "parent",
7279 "to": [ "#echo_server" ],
7280 "dependency": "weak"
7281 },
7282 {
7283 "protocol": "fuchsia.sys2.StorageAdmin",
7284 "from": "#data",
7285 "to": [ "#echo_server" ]
7286 },
7287 {
7288 "protocol": [
7289 "fuchsia.settings.Accessibility",
7290 "fuchsia.ui.scenic.Scenic"
7291 ],
7292 "from": "parent",
7293 "to": [ "#echo_server" ],
7294 "dependency": "strong"
7295 },
7296 {
7297 "directory": "assets",
7298 "from": "self",
7299 "to": [ "#echo_server" ],
7300 "rights": ["r*"]
7301 },
7302 {
7303 "directory": "index",
7304 "subdir": "files",
7305 "from": "parent",
7306 "to": [ "#modular" ],
7307 "dependency": "weak"
7308 },
7309 {
7310 "directory": "config",
7311 "from": "framework",
7312 "to": [ "#modular" ],
7313 "as": "config",
7314 "dependency": "strong"
7315 },
7316 {
7317 "storage": "data",
7318 "from": "self",
7319 "to": [ "#modular", "#logger" ]
7320 },
7321 {
7322 "runner": "elf",
7323 "from": "parent",
7324 "to": [ "#modular", "#logger" ]
7325 },
7326 {
7327 "resolver": "pkg_resolver",
7328 "from": "parent",
7329 "to": [ "#modular" ],
7330 },
7331 ],
7332 "children": [
7333 {
7334 "name": "logger",
7335 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
7336 },
7337 {
7338 "name": "echo_server",
7339 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7340 },
7341 ],
7342 "collections": [
7343 {
7344 "name": "modular",
7345 "durability": "transient",
7346 },
7347 ],
7348 "capabilities": [
7349 {
7350 "directory": "assets",
7351 "path": "/data/assets",
7352 "rights": [ "rw*" ],
7353 },
7354 {
7355 "storage": "data",
7356 "from": "parent",
7357 "backing_dir": "minfs",
7358 "storage_id": "static_instance_id_or_moniker",
7359 },
7360 ],
7361 }),
7362 Ok(())
7363 ),
7364 test_cml_offer_all_valid_chars(
7365 json!({
7366 "offer": [
7367 {
7368 "protocol": "fuchsia.logger.Log",
7369 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
7370 "to": [ "#abcdefghijklmnopqrstuvwxyz0123456789_-to" ],
7371 },
7372 ],
7373 "children": [
7374 {
7375 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
7376 "url": "https://www.google.com/gmail"
7377 },
7378 {
7379 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-to",
7380 "url": "https://www.google.com/gmail"
7381 },
7382 ],
7383 "capabilities": [
7384 {
7385 "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage",
7386 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
7387 "backing_dir": "example",
7388 "storage_id": "static_instance_id_or_moniker",
7389 }
7390 ]
7391 }),
7392 Ok(())
7393 ),
7394 test_cml_offer_singleton_to (
7395 json!({
7396 "offer": [
7397 {
7398 "protocol": "fuchsia.fonts.LegacyProvider",
7399 "from": "parent",
7400 "to": "#echo_server",
7401 "dependency": "weak"
7402 },
7403 ],
7404 "children": [
7405 {
7406 "name": "echo_server",
7407 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7408 },
7409 ],
7410 }),
7411 Ok(())
7412 ),
7413 test_cml_offer_missing_props(
7414 json!({
7415 "offer": [ {} ]
7416 }),
7417 Err(Error::Parse { err, .. }) if &err == "missing field `from`"
7418 ),
7419 test_cml_offer_missing_from(
7420 json!({
7421 "offer": [
7422 {
7423 "protocol": "fuchsia.logger.Log",
7424 "from": "#missing",
7425 "to": [ "#echo_server" ],
7426 },
7427 ],
7428 "children": [
7429 {
7430 "name": "echo_server",
7431 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
7432 },
7433 ],
7434 }),
7435 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
7436 ),
7437 test_cml_storage_offer_from_child_no_span(
7438 json!({
7439 "offer": [
7440 {
7441 "storage": "cache",
7442 "from": "#storage_provider",
7443 "to": [ "#echo_server" ],
7444 },
7445 ],
7446 "children": [
7447 {
7448 "name": "echo_server",
7449 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
7450 },
7451 {
7452 "name": "storage_provider",
7453 "url": "fuchsia-pkg://fuchsia.com/storage_provider#meta/storage_provider.cm",
7454 },
7455 ],
7456 }),
7457 Err(Error::Validate { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
7458 ),
7459 test_cml_offer_bad_from(
7460 json!({
7461 "offer": [ {
7462 "protocol": "fuchsia.logger.Log",
7463 "from": "#invalid@",
7464 "to": [ "#echo_server" ],
7465 } ]
7466 }),
7467 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"
7468 ),
7469 test_cml_offer_invalid_multiple_from(
7470 json!({
7471 "offer": [
7472 {
7473 "protocol": "fuchsia.logger.Log",
7474 "from": [ "parent", "#logger" ],
7475 "to": [ "#echo_server" ],
7476 },
7477 ],
7478 "children": [
7479 {
7480 "name": "logger",
7481 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
7482 },
7483 {
7484 "name": "echo_server",
7485 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7486 },
7487 ]
7488 }),
7489 Err(Error::Validate { err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses"
7490 ),
7491 test_cml_offer_from_missing_named_source(
7492 json!({
7493 "offer": [
7494 {
7495 "protocol": "fuchsia.logger.Log",
7496 "from": "#does-not-exist",
7497 "to": ["#echo_server" ],
7498 },
7499 ],
7500 "children": [
7501 {
7502 "name": "echo_server",
7503 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7504 },
7505 ]
7506 }),
7507 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\""
7508 ),
7509 test_cml_offer_protocol_from_collection_invalid(
7510 json!({
7511 "collections": [ {
7512 "name": "coll",
7513 "durability": "transient",
7514 } ],
7515 "children": [ {
7516 "name": "echo_server",
7517 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7518 } ],
7519 "offer": [
7520 { "protocol": "fuchsia.logger.Log", "from": "#coll", "to": [ "#echo_server" ] },
7521 ]
7522 }),
7523 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\" or \"capabilities\""
7524 ),
7525 test_cml_offer_directory_from_collection_invalid(
7526 json!({
7527 "collections": [ {
7528 "name": "coll",
7529 "durability": "transient",
7530 } ],
7531 "children": [ {
7532 "name": "echo_server",
7533 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7534 } ],
7535 "offer": [
7536 { "directory": "temp", "from": "#coll", "to": [ "#echo_server" ] },
7537 ]
7538 }),
7539 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
7540 ),
7541 test_cml_offer_storage_from_collection_invalid_no_span(
7542 json!({
7543 "collections": [ {
7544 "name": "coll",
7545 "durability": "transient",
7546 } ],
7547 "children": [ {
7548 "name": "echo_server",
7549 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7550 } ],
7551 "offer": [
7552 { "storage": "cache", "from": "#coll", "to": [ "#echo_server" ] },
7553 ]
7554 }),
7555 Err(Error::Validate { err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed"
7556 ),
7557 test_cml_offer_runner_from_collection_invalid(
7558 json!({
7559 "collections": [ {
7560 "name": "coll",
7561 "durability": "transient",
7562 } ],
7563 "children": [ {
7564 "name": "echo_server",
7565 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7566 } ],
7567 "offer": [
7568 { "runner": "elf", "from": "#coll", "to": [ "#echo_server" ] },
7569 ]
7570 }),
7571 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
7572 ),
7573 test_cml_offer_resolver_from_collection_invalid(
7574 json!({
7575 "collections": [ {
7576 "name": "coll",
7577 "durability": "transient",
7578 } ],
7579 "children": [ {
7580 "name": "echo_server",
7581 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
7582 } ],
7583 "offer": [
7584 { "resolver": "base", "from": "#coll", "to": [ "#echo_server" ] },
7585 ]
7586 }),
7587 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\""
7588 ),
7589 test_cml_offer_from_dictionary_invalid(
7590 json!({
7591 "offer": [
7592 {
7593 "protocol": "pkg_protocol",
7594 "from": "bad/a",
7595 "to": "#child",
7596 },
7597 ],
7598 "children": [
7599 {
7600 "name": "child",
7601 "url": "fuchsia-pkg://child",
7602 },
7603 ],
7604 }),
7605 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"
7606 ),
7607 test_cml_offer_to_non_dictionary(
7608 json!({
7609 "offer": [
7610 {
7611 "protocol": "p",
7612 "from": "parent",
7613 "to": "self/dict",
7614 },
7615 ],
7616 "capabilities": [
7617 {
7618 "protocol": "dict",
7619 },
7620 ],
7621 }),
7622 Err(Error::Validate { err, .. }) if &err == "\"offer\" has dictionary target \
7623 \"self/dict\" but \"dict\" is not a dictionary capability defined by \
7624 this component"
7625 ),
7626
7627 test_cml_offer_empty_targets(
7628 json!({
7629 "offer": [
7630 {
7631 "protocol": "fuchsia.logger.Log",
7632 "from": "#child",
7633 "to": []
7634 },
7635 ],
7636 "children": [
7637 {
7638 "name": "child",
7639 "url": "fuchsia-pkg://fuchsia.com/pkg#comp.cm",
7640 },
7641 ],
7642 }),
7643 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"
7644 ),
7645 test_cml_offer_duplicate_targets(
7646 json!({
7647 "offer": [ {
7648 "protocol": "fuchsia.logger.Log",
7649 "from": "#logger",
7650 "to": ["#a", "#a"]
7651 } ]
7652 }),
7653 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"
7654 ),
7655 test_cml_offer_target_missing_props(
7656 json!({
7657 "offer": [ {
7658 "protocol": "fuchsia.logger.Log",
7659 "from": "#logger",
7660 "as": "fuchsia.logger.SysLog",
7661 } ]
7662 }),
7663 Err(Error::Parse { err, .. }) if &err == "missing field `to`"
7664 ),
7665 test_cml_offer_target_missing_to(
7666 json!({
7667 "offer": [ {
7668 "protocol": "fuchsia.logger.Log",
7669 "from": "#logger",
7670 "to": [ "#missing" ],
7671 } ],
7672 "children": [ {
7673 "name": "logger",
7674 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
7675 } ]
7676 }),
7677 Err(Error::Validate { err, .. }) if &err == "\"#missing\" is an \"offer\" target from \"#logger\" but \"#missing\" does not appear in \"children\" or \"collections\""
7678 ),
7679 test_cml_offer_target_bad_to(
7680 json!({
7681 "offer": [ {
7682 "protocol": "fuchsia.logger.Log",
7683 "from": "#logger",
7684 "to": [ "self" ],
7685 "as": "fuchsia.logger.SysLog",
7686 } ]
7687 }),
7688 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"self\", expected \"#<child-name>\", \"#<collection-name>\", or \"self/<dictionary>\""
7689 ),
7690 test_cml_offer_empty_protocols(
7691 json!({
7692 "offer": [
7693 {
7694 "protocol": [],
7695 "from": "parent",
7696 "to": [ "#echo_server" ],
7697 "as": "thing"
7698 },
7699 ],
7700 }),
7701 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
7702 ),
7703 test_cml_offer_target_equals_from(
7704 json!({
7705 "children": [
7706 {
7707 "name": "child",
7708 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
7709 },
7710 ],
7711 "offer": [
7712 {
7713 "protocol": "fuchsia.example.Protocol",
7714 "from": "#child",
7715 "to": [ "#child" ],
7716 },
7717 ],
7718 }),
7719 Err(Error::Validate { err, .. }) if &err == "Offer target \"#child\" is same as source"
7720 ),
7721 test_cml_offer_target_equals_from_weak(
7722 json!({
7723 "children": [
7724 {
7725 "name": "child",
7726 "url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
7727 },
7728 ],
7729 "offer": [
7730 {
7731 "protocol": "fuchsia.example.Protocol",
7732 "from": "#child",
7733 "to": [ "#child" ],
7734 "dependency": "weak",
7735 },
7736 {
7737 "directory": "data",
7738 "from": "#child",
7739 "to": [ "#child" ],
7740 "dependency": "weak",
7741 },
7742 ],
7743 }),
7744 Ok(())
7745 ),
7746 test_cml_storage_offer_target_equals_from(
7747 json!({
7748 "offer": [ {
7749 "storage": "minfs",
7750 "from": "self",
7751 "to": [ "#logger" ],
7752 } ],
7753 "children": [ {
7754 "name": "logger",
7755 "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
7756 } ],
7757 "capabilities": [ {
7758 "storage": "minfs",
7759 "from": "#logger",
7760 "backing_dir": "minfs-dir",
7761 "storage_id": "static_instance_id_or_moniker",
7762 } ],
7763 }),
7764 Err(Error::Validate { err, .. }) if &err == "Storage offer target \"#logger\" is same as source"
7765 ),
7766 test_cml_offer_duplicate_target_names(
7767 json!({
7768 "offer": [
7769 {
7770 "protocol": "logger",
7771 "from": "parent",
7772 "to": [ "#echo_server" ],
7773 "as": "thing"
7774 },
7775 {
7776 "protocol": "logger",
7777 "from": "parent",
7778 "to": [ "#scenic" ],
7779 },
7780 {
7781 "directory": "thing",
7782 "from": "parent",
7783 "to": [ "#echo_server" ],
7784 }
7785 ],
7786 "children": [
7787 {
7788 "name": "scenic",
7789 "url": "fuchsia-pkg://fuchsia.com/scenic/stable#meta/scenic.cm"
7790 },
7791 {
7792 "name": "echo_server",
7793 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7794 },
7795 ],
7796 }),
7797 Err(Error::Validate { err, .. }) if &err == "\"thing\" is a duplicate \"offer\" target capability for \"#echo_server\""
7798 ),
7799 test_cml_offer_duplicate_storage_names(
7800 json!({
7801 "offer": [
7802 {
7803 "storage": "cache",
7804 "from": "parent",
7805 "to": [ "#echo_server" ]
7806 },
7807 {
7808 "storage": "cache",
7809 "from": "self",
7810 "to": [ "#echo_server" ]
7811 }
7812 ],
7813 "capabilities": [ {
7814 "storage": "cache",
7815 "from": "self",
7816 "backing_dir": "minfs",
7817 "storage_id": "static_instance_id_or_moniker",
7818 } ],
7819 "children": [ {
7820 "name": "echo_server",
7821 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7822 } ]
7823 }),
7824 Err(Error::Validate { err, .. }) if &err == "\"cache\" is a duplicate \"offer\" target capability for \"#echo_server\""
7825 ),
7826 test_cml_offer_bad_as(
7828 json!({
7829 "offer": [
7830 {
7831 "protocol": ["A", "B"],
7832 "from": "parent",
7833 "to": [ "#echo_server" ],
7834 "as": "thing"
7835 },
7836 ],
7837 "children": [
7838 {
7839 "name": "echo_server",
7840 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
7841 }
7842 ]
7843 }),
7844 Err(Error::Validate { err, .. }) if &err == "\"as\" can only be specified when one `protocol` is supplied."
7845 ),
7846 test_cml_offer_bad_subdir_no_span(
7847 json!({
7848 "offer": [
7849 {
7850 "directory": "index",
7851 "subdir": "/",
7852 "from": "parent",
7853 "to": [ "#modular" ],
7854 },
7855 ],
7856 "children": [
7857 {
7858 "name": "modular",
7859 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7860 }
7861 ]
7862 }),
7863 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
7864 ),
7865 test_cml_offer_from_self_no_span(
7866 json!({
7867 "offer": [
7868 {
7869 "protocol": "foo_protocol",
7870 "from": "self",
7871 "to": [ "#modular" ],
7872 },
7873 {
7874 "protocol": [ "bar_protocol", "baz_protocol" ],
7875 "from": "self",
7876 "to": [ "#modular" ],
7877 },
7878 {
7879 "directory": "foo_directory",
7880 "from": "self",
7881 "to": [ "#modular" ],
7882 },
7883 {
7884 "runner": "foo_runner",
7885 "from": "self",
7886 "to": [ "#modular" ],
7887 },
7888 {
7889 "resolver": "foo_resolver",
7890 "from": "self",
7891 "to": [ "#modular" ],
7892 },
7893 ],
7894 "children": [
7895 {
7896 "name": "modular",
7897 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7898 },
7899 ],
7900 "capabilities": [
7901 {
7902 "protocol": "foo_protocol",
7903 },
7904 {
7905 "protocol": "bar_protocol",
7906 },
7907 {
7908 "protocol": "baz_protocol",
7909 },
7910 {
7911 "directory": "foo_directory",
7912 "path": "/dir",
7913 "rights": [ "r*" ],
7914 },
7915 {
7916 "runner": "foo_runner",
7917 "path": "/svc/fuchsia.sys2.ComponentRunner",
7918 },
7919 {
7920 "resolver": "foo_resolver",
7921 "path": "/svc/fuchsia.component.resolution.Resolver",
7922 },
7923 ]
7924 }),
7925 Ok(())
7926 ),
7927 test_cml_offer_service_from_self_missing_no_span(
7928 json!({
7929 "offer": [
7930 {
7931 "service": "pkg_service",
7932 "from": "self",
7933 "to": [ "#modular" ],
7934 },
7935 ],
7936 "children": [
7937 {
7938 "name": "modular",
7939 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7940 },
7941 ],
7942 }),
7943 Err(Error::Validate { err, .. }) if &err == "service \"pkg_service\" is offered from self, so it must be declared as a \"service\" in \"capabilities\""
7944 ),
7945 test_cml_offer_protocol_from_self_missing_no_span(
7946 json!({
7947 "offer": [
7948 {
7949 "protocol": "pkg_protocol",
7950 "from": "self",
7951 "to": [ "#modular" ],
7952 },
7953 ],
7954 "children": [
7955 {
7956 "name": "modular",
7957 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7958 },
7959 ],
7960 }),
7961 Err(Error::Validate { err, .. }) if &err == "protocol \"pkg_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
7962 ),
7963 test_cml_offer_protocol_from_self_missing_multiple_no_span(
7964 json!({
7965 "offer": [
7966 {
7967 "protocol": [ "foo_protocol", "bar_protocol" ],
7968 "from": "self",
7969 "to": [ "#modular" ],
7970 },
7971 ],
7972 "children": [
7973 {
7974 "name": "modular",
7975 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7976 },
7977 ],
7978 }),
7979 Err(Error::Validate { err, .. }) if &err == "protocol \"foo_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\""
7980 ),
7981 test_cml_offer_directory_from_self_missing_no_span(
7982 json!({
7983 "offer": [
7984 {
7985 "directory": "pkg_directory",
7986 "from": "self",
7987 "to": [ "#modular" ],
7988 },
7989 ],
7990 "children": [
7991 {
7992 "name": "modular",
7993 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
7994 },
7995 ],
7996 }),
7997 Err(Error::Validate { err, .. }) if &err == "directory \"pkg_directory\" is offered from self, so it must be declared as a \"directory\" in \"capabilities\""
7998 ),
7999 test_cml_offer_runner_from_self_missing_no_span(
8000 json!({
8001 "offer": [
8002 {
8003 "runner": "dart",
8004 "from": "self",
8005 "to": [ "#modular" ],
8006 },
8007 ],
8008 "children": [
8009 {
8010 "name": "modular",
8011 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8012 },
8013 ],
8014 }),
8015 Err(Error::Validate { err, .. }) if &err == "runner \"dart\" is offered from self, so it must be declared as a \"runner\" in \"capabilities\""
8016 ),
8017 test_cml_offer_resolver_from_self_missing_no_span(
8018 json!({
8019 "offer": [
8020 {
8021 "resolver": "pkg_resolver",
8022 "from": "self",
8023 "to": [ "#modular" ],
8024 },
8025 ],
8026 "children": [
8027 {
8028 "name": "modular",
8029 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8030 },
8031 ],
8032 }),
8033 Err(Error::Validate { err, .. }) if &err == "resolver \"pkg_resolver\" is offered from self, so it must be declared as a \"resolver\" in \"capabilities\""
8034 ),
8035 test_cml_offer_storage_from_self_missing_no_span(
8036 json!({
8037 "offer": [
8038 {
8039 "storage": "cache",
8040 "from": "self",
8041 "to": [ "#echo_server" ],
8042 },
8043 ],
8044 "children": [
8045 {
8046 "name": "echo_server",
8047 "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm",
8048 },
8049 ],
8050 }),
8051 Err(Error::Validate { err, .. }) if &err == "storage \"cache\" is offered from self, so it must be declared as a \"storage\" in \"capabilities\""
8052 ),
8053 test_cml_offer_from_self_missing_dictionary_no_span(
8054 json!({
8055 "offer": [
8056 {
8057 "protocol": "foo_protocol",
8058 "from": "self/dict/inner",
8059 "to": [ "#modular" ],
8060 },
8061 ],
8062 "children": [
8063 {
8064 "name": "modular",
8065 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8066 },
8067 ],
8068 }),
8069 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\""
8070 ),
8071 test_cml_offer_dependency_on_wrong_type(
8072 json!({
8073 "offer": [ {
8074 "resolver": "fuchsia.logger.Log",
8075 "from": "parent",
8076 "to": [ "#echo_server" ],
8077 "dependency": "strong",
8078 } ],
8079 "children": [ {
8080 "name": "echo_server",
8081 "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm",
8082 } ],
8083 }),
8084 Err(Error::Validate { err, .. }) if err.starts_with("Dependency can only be provided for")
8085 ),
8086
8087 test_cml_children(
8089 json!({
8090 "children": [
8091 {
8092 "name": "logger",
8093 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
8094 "on_terminate": "reboot",
8095 },
8096 {
8097 "name": "gmail",
8098 "url": "https://www.google.com/gmail",
8099 "startup": "eager",
8100 },
8101 ]
8102 }),
8103 Ok(())
8104 ),
8105 test_cml_children_missing_props(
8106 json!({
8107 "children": [ {} ]
8108 }),
8109 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
8110 ),
8111 test_cml_children_duplicate_names(
8112 json!({
8113 "children": [
8114 {
8115 "name": "logger",
8116 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
8117 },
8118 {
8119 "name": "logger",
8120 "url": "fuchsia-pkg://fuchsia.com/logger/beta#meta/logger.cm"
8121 }
8122 ]
8123 }),
8124 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"children\" and once in \"children\""
8125 ),
8126 test_cml_children_url_ends_in_cml_no_span(
8127 json!({
8128 "children": [
8129 {
8130 "name": "logger",
8131 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cml"
8132 },
8133 ]
8134 }),
8135 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"
8136 ),
8137 test_cml_children_bad_startup(
8138 json!({
8139 "children": [
8140 {
8141 "name": "logger",
8142 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
8143 "startup": "zzz",
8144 },
8145 ],
8146 }),
8147 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `lazy` or `eager`"
8148 ),
8149 test_cml_children_bad_on_terminate(
8150 json!({
8151 "children": [
8152 {
8153 "name": "logger",
8154 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
8155 "on_terminate": "zzz",
8156 },
8157 ],
8158 }),
8159 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `none` or `reboot`"
8160 ),
8161
8162
8163 test_cml_children_bad_environment_no_span(
8164 json!({
8165 "children": [
8166 {
8167 "name": "logger",
8168 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
8169 "environment": "parent",
8170 }
8171 ]
8172 }),
8173 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\""
8174 ),
8175 test_cml_children_environment_no_span(
8176 json!({
8177 "children": [
8178 {
8179 "name": "logger",
8180 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
8181 "environment": "#foo_env",
8182 }
8183 ],
8184 "environments": [
8185 {
8186 "name": "foo_env",
8187 }
8188 ]
8189 }),
8190 Ok(())
8191 ),
8192 test_cml_collections_bad_environment_no_span(
8193 json!({
8194 "collections": [
8195 {
8196 "name": "tests",
8197 "durability": "transient",
8198 "environment": "parent",
8199 }
8200 ]
8201 }),
8202 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\""
8203 ),
8204 test_cml_collections_environment_no_span(
8205 json!({
8206 "collections": [
8207 {
8208 "name": "tests",
8209 "durability": "transient",
8210 "environment": "#foo_env",
8211 }
8212 ],
8213 "environments": [
8214 {
8215 "name": "foo_env",
8216 }
8217 ]
8218 }),
8219 Ok(())
8220 ),
8221
8222 test_cml_environment_timeout_no_span(
8223 json!({
8224 "environments": [
8225 {
8226 "name": "foo_env",
8227 "__stop_timeout_ms": 10000,
8228 }
8229 ]
8230 }),
8231 Ok(())
8232 ),
8233
8234 test_cml_environment_bad_timeout_no_span(
8235 json!({
8236 "environments": [
8237 {
8238 "name": "foo_env",
8239 "__stop_timeout_ms": -3,
8240 }
8241 ]
8242 }),
8243 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `-3`, expected an unsigned 32-bit integer"
8244 ),
8245 test_cml_environment_debug_no_span(
8246 json!({
8247 "capabilities": [
8248 {
8249 "protocol": "fuchsia.logger.Log2",
8250 },
8251 ],
8252 "environments": [
8253 {
8254 "name": "foo_env",
8255 "extends": "realm",
8256 "debug": [
8257 {
8258 "protocol": "fuchsia.module.Module",
8259 "from": "#modular",
8260 },
8261 {
8262 "protocol": "fuchsia.logger.OtherLog",
8263 "from": "parent",
8264 },
8265 {
8266 "protocol": "fuchsia.logger.Log2",
8267 "from": "self",
8268 },
8269 ]
8270 }
8271 ],
8272 "children": [
8273 {
8274 "name": "modular",
8275 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8276 },
8277 ],
8278 }),
8279 Ok(())
8280 ),
8281 test_cml_environment_debug_missing_capability_no_span(
8282 json!({
8283 "environments": [
8284 {
8285 "name": "foo_env",
8286 "extends": "realm",
8287 "debug": [
8288 {
8289 "protocol": "fuchsia.module.Module",
8290 "from": "#modular",
8291 },
8292 {
8293 "protocol": "fuchsia.logger.OtherLog",
8294 "from": "parent",
8295 },
8296 {
8297 "protocol": "fuchsia.logger.Log2",
8298 "from": "self",
8299 },
8300 ]
8301 }
8302 ],
8303 "children": [
8304 {
8305 "name": "modular",
8306 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8307 },
8308 ],
8309 }),
8310 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\""
8311 ),
8312 test_cml_environment_invalid_from_child_no_span(
8313 json!({
8314 "capabilities": [
8315 {
8316 "protocol": "fuchsia.logger.Log2",
8317 },
8318 ],
8319 "environments": [
8320 {
8321 "name": "foo_env",
8322 "extends": "realm",
8323 "debug": [
8324 {
8325 "protocol": "fuchsia.module.Module",
8326 "from": "#missing",
8327 },
8328 {
8329 "protocol": "fuchsia.logger.OtherLog",
8330 "from": "parent",
8331 },
8332 {
8333 "protocol": "fuchsia.logger.Log2",
8334 "from": "self",
8335 },
8336 ]
8337 }
8338 ],
8339 "children": [
8340 {
8341 "name": "modular",
8342 "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm"
8343 },
8344 ],
8345 }),
8346 Err(Error::Validate { err, .. }) if &err == "\"debug\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
8347 ),
8348
8349
8350 test_cml_collections(
8352 json!({
8353 "collections": [
8354 {
8355 "name": "test_single_run_coll",
8356 "durability": "single_run"
8357 },
8358 {
8359 "name": "test_transient_coll",
8360 "durability": "transient"
8361 },
8362 ]
8363 }),
8364 Ok(())
8365 ),
8366 test_cml_collections_missing_props(
8367 json!({
8368 "collections": [ {} ]
8369 }),
8370 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
8371 ),
8372 test_cml_collections_duplicate_names(
8373 json!({
8374 "collections": [
8375 {
8376 "name": "duplicate",
8377 "durability": "single_run"
8378 },
8379 {
8380 "name": "duplicate",
8381 "durability": "transient"
8382 }
8383 ]
8384 }),
8385 Err(Error::Validate { err, .. }) if &err == "identifier \"duplicate\" is defined twice, once in \"collections\" and once in \"collections\""
8386 ),
8387 test_cml_collections_bad_durability(
8388 json!({
8389 "collections": [
8390 {
8391 "name": "modular",
8392 "durability": "zzz",
8393 },
8394 ],
8395 }),
8396 Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `transient` or `single_run`"
8397 ),
8398
8399 test_cml_protocol(
8401 json!({
8402 "capabilities": [
8403 {
8404 "protocol": "a",
8405 "path": "/minfs",
8406 },
8407 {
8408 "protocol": "b",
8409 "path": "/data",
8410 },
8411 {
8412 "protocol": "c",
8413 },
8414 ],
8415 }),
8416 Ok(())
8417 ),
8418 test_cml_protocol_multi(
8419 json!({
8420 "capabilities": [
8421 {
8422 "protocol": ["a", "b", "c"],
8423 },
8424 ],
8425 }),
8426 Ok(())
8427 ),
8428 test_cml_protocol_multi_invalid_path_no_span(
8429 json!({
8430 "capabilities": [
8431 {
8432 "protocol": ["a", "b", "c"],
8433 "path": "/minfs",
8434 },
8435 ],
8436 }),
8437 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."
8438 ),
8439 test_cml_protocol_all_valid_chars(
8440 json!({
8441 "capabilities": [
8442 {
8443 "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
8444 },
8445 ],
8446 }),
8447 Ok(())
8448 ),
8449 test_cml_directory(
8450 json!({
8451 "capabilities": [
8452 {
8453 "directory": "a",
8454 "path": "/minfs",
8455 "rights": ["connect"],
8456 },
8457 {
8458 "directory": "b",
8459 "path": "/data",
8460 "rights": ["connect"],
8461 },
8462 ],
8463 }),
8464 Ok(())
8465 ),
8466 test_cml_directory_all_valid_chars(
8467 json!({
8468 "capabilities": [
8469 {
8470 "directory": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
8471 "path": "/data",
8472 "rights": ["connect"],
8473 },
8474 ],
8475 }),
8476 Ok(())
8477 ),
8478 test_cml_directory_missing_path_no_span(
8479 json!({
8480 "capabilities": [
8481 {
8482 "directory": "dir",
8483 "rights": ["connect"],
8484 },
8485 ]
8486 }),
8487 Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"directory\""
8488 ),
8489 test_cml_directory_missing_rights_no_span(
8490 json!({
8491 "capabilities": [
8492 {
8493 "directory": "dir",
8494 "path": "/dir",
8495 },
8496 ]
8497 }),
8498 Err(Error::Validate { err, .. }) if &err == "\"rights\" should be present with \"directory\""
8499 ),
8500 test_cml_storage(
8501 json!({
8502 "capabilities": [
8503 {
8504 "storage": "a",
8505 "from": "#minfs",
8506 "backing_dir": "minfs",
8507 "storage_id": "static_instance_id",
8508 },
8509 {
8510 "storage": "b",
8511 "from": "parent",
8512 "backing_dir": "data",
8513 "storage_id": "static_instance_id_or_moniker",
8514 },
8515 {
8516 "storage": "c",
8517 "from": "self",
8518 "backing_dir": "storage",
8519 "storage_id": "static_instance_id_or_moniker",
8520 },
8521 ],
8522 "children": [
8523 {
8524 "name": "minfs",
8525 "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm",
8526 },
8527 ],
8528 }),
8529 Ok(())
8530 ),
8531 test_cml_storage_all_valid_chars(
8532 json!({
8533 "capabilities": [
8534 {
8535 "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage",
8536 "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
8537 "backing_dir": "example",
8538 "storage_id": "static_instance_id_or_moniker",
8539 },
8540 ],
8541 "children": [
8542 {
8543 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
8544 "url": "https://www.google.com/gmail",
8545 },
8546 ],
8547 }),
8548 Ok(())
8549 ),
8550 test_cml_storage_invalid_from(
8551 json!({
8552 "capabilities": [ {
8553 "storage": "minfs",
8554 "from": "#missing",
8555 "backing_dir": "minfs",
8556 "storage_id": "static_instance_id_or_moniker",
8557 } ]
8558 }),
8559 Err(Error::Validate { err, .. }) if &err == "\"capabilities\" source \"#missing\" does not appear in \"children\""
8560 ),
8561 test_cml_storage_missing_path_or_backing_dir_no_span(
8562 json!({
8563 "capabilities": [ {
8564 "storage": "minfs",
8565 "from": "self",
8566 "storage_id": "static_instance_id_or_moniker",
8567 } ]
8568 }),
8569 Err(Error::Validate { err, .. }) if &err == "\"backing_dir\" should be present with \"storage\""
8570
8571 ),
8572 test_cml_storage_missing_storage_id_no_span(
8573 json!({
8574 "capabilities": [ {
8575 "storage": "minfs",
8576 "from": "self",
8577 "backing_dir": "storage",
8578 }, ]
8579 }),
8580 Err(Error::Validate { err, .. }) if &err == "\"storage_id\" should be present with \"storage\""
8581 ),
8582 test_cml_storage_path_no_span(
8583 json!({
8584 "capabilities": [ {
8585 "storage": "minfs",
8586 "from": "self",
8587 "path": "/minfs",
8588 "storage_id": "static_instance_id_or_moniker",
8589 } ]
8590 }),
8591 Err(Error::Validate { err, .. }) if &err == "\"path\" cannot be present with \"storage\", use \"backing_dir\""
8592 ),
8593 test_cml_runner(
8594 json!({
8595 "capabilities": [
8596 {
8597 "runner": "a",
8598 "path": "/minfs",
8599 },
8600 ],
8601 }),
8602 Ok(())
8603 ),
8604 test_cml_runner_all_valid_chars(
8605 json!({
8606 "children": [
8607 {
8608 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
8609 "url": "https://www.google.com/gmail"
8610 },
8611 ],
8612 "capabilities": [
8613 {
8614 "runner": "abcdefghijklmnopqrstuvwxyz0123456789_-runner",
8615 "path": "/example",
8616 },
8617 ]
8618 }),
8619 Ok(())
8620 ),
8621 test_cml_runner_extraneous_from_no_span(
8622 json!({
8623 "capabilities": [
8624 {
8625 "runner": "a",
8626 "path": "/example",
8627 "from": "self",
8628 },
8629 ]
8630 }),
8631 Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"runner\""
8632 ),
8633 test_cml_capability_missing_name_no_span(
8634 json!({
8635 "capabilities": [
8636 {
8637 "path": "/svc/fuchsia.component.resolution.Resolver",
8638 },
8639 ]
8640 }),
8641 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\""
8642 ),
8643 test_cml_resolver_missing_path_no_span(
8644 json!({
8645 "capabilities": [
8646 {
8647 "resolver": "pkg_resolver",
8648 },
8649 ]
8650 }),
8651 Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"resolver\""
8652 ),
8653 test_cml_capabilities_extraneous_from_no_span(
8654 json!({
8655 "capabilities": [
8656 {
8657 "resolver": "pkg_resolver",
8658 "path": "/svc/fuchsia.component.resolution.Resolver",
8659 "from": "self",
8660 },
8661 ]
8662 }),
8663 Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"resolver\""
8664 ),
8665 test_cml_capabilities_duplicates(
8666 json!({
8667 "capabilities": [
8668 {
8669 "runner": "pkg_resolver",
8670 "path": "/svc/fuchsia.component.resolution.Resolver",
8671 },
8672 {
8673 "resolver": "pkg_resolver",
8674 "path": "/svc/my-resolver",
8675 },
8676 ]
8677 }),
8678 Err(Error::Validate { err, .. }) if &err == "identifier \"pkg_resolver\" is defined twice, once in \"resolvers\" and once in \"runners\""
8679 ),
8680
8681 test_cml_environments(
8683 json!({
8684 "environments": [
8685 {
8686 "name": "my_env_a",
8687 },
8688 {
8689 "name": "my_env_b",
8690 "extends": "realm",
8691 },
8692 {
8693 "name": "my_env_c",
8694 "extends": "none",
8695 "__stop_timeout_ms": 8000,
8696 },
8697 ],
8698 }),
8699 Ok(())
8700 ),
8701
8702 test_invalid_cml_environment_no_stop_timeout(
8703 json!({
8704 "environments": [
8705 {
8706 "name": "my_env",
8707 "extends": "none",
8708 },
8709 ],
8710 }),
8711 Err(Error::Validate { err, .. }) if &err ==
8712 "'__stop_timeout_ms' must be provided if the environment does not extend \
8713 another environment"
8714 ),
8715
8716 test_cml_environment_invalid_extends(
8717 json!({
8718 "environments": [
8719 {
8720 "name": "my_env",
8721 "extends": "some_made_up_string",
8722 },
8723 ],
8724 }),
8725 Err(Error::Parse { err, .. }) if &err == "unknown variant `some_made_up_string`, expected `realm` or `none`"
8726 ),
8727 test_cml_environment_missing_props(
8728 json!({
8729 "environments": [ {} ]
8730 }),
8731 Err(Error::Parse { err, .. }) if &err == "missing field `name`"
8732 ),
8733
8734 test_cml_environment_with_runners(
8735 json!({
8736 "environments": [
8737 {
8738 "name": "my_env",
8739 "extends": "realm",
8740 "runners": [
8741 {
8742 "runner": "dart",
8743 "from": "parent",
8744 }
8745 ]
8746 }
8747 ],
8748 }),
8749 Ok(())
8750 ),
8751 test_cml_environment_with_runners_alias(
8752 json!({
8753 "environments": [
8754 {
8755 "name": "my_env",
8756 "extends": "realm",
8757 "runners": [
8758 {
8759 "runner": "dart",
8760 "from": "parent",
8761 "as": "my-dart",
8762 }
8763 ]
8764 }
8765 ],
8766 }),
8767 Ok(())
8768 ),
8769 test_cml_environment_with_runners_missing(
8770 json!({
8771 "environments": [
8772 {
8773 "name": "my_env",
8774 "extends": "realm",
8775 "runners": [
8776 {
8777 "runner": "dart",
8778 "from": "self",
8779 }
8780 ]
8781 }
8782 ],
8783 "capabilities": [
8784 {
8785 "runner": "dart",
8786 "path": "/svc/fuchsia.component.Runner",
8787 }
8788 ],
8789 }),
8790 Ok(())
8791 ),
8792 test_cml_environment_with_runners_bad_name(
8793 json!({
8794 "environments": [
8795 {
8796 "name": "my_env",
8797 "extends": "realm",
8798 "runners": [
8799 {
8800 "runner": "elf",
8801 "from": "parent",
8802 "as": "#elf",
8803 }
8804 ]
8805 }
8806 ],
8807 }),
8808 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"#elf\", expected a \
8809 name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]"
8810 ),
8811 test_cml_environment_with_runners_duplicate_name(
8812 json!({
8813 "environments": [
8814 {
8815 "name": "my_env",
8816 "extends": "realm",
8817 "runners": [
8818 {
8819 "runner": "dart",
8820 "from": "parent",
8821 },
8822 {
8823 "runner": "other-dart",
8824 "from": "parent",
8825 "as": "dart",
8826 }
8827 ]
8828 }
8829 ],
8830 }),
8831 Err(Error::Validate { err, .. }) if &err == "Duplicate runners registered under name \"dart\": \"other-dart\" and \"dart\"."
8832 ),
8833 test_cml_environment_with_runner_from_missing_child(
8834 json!({
8835 "environments": [
8836 {
8837 "name": "my_env",
8838 "extends": "realm",
8839 "runners": [
8840 {
8841 "runner": "elf",
8842 "from": "#missing_child",
8843 }
8844 ]
8845 }
8846 ]
8847 }),
8848 Err(Error::Validate { err, .. }) if &err == "\"elf\" runner source \"#missing_child\" does not appear in \"children\""
8849 ),
8850 test_cml_environment_with_resolvers(
8851 json!({
8852 "environments": [
8853 {
8854 "name": "my_env",
8855 "extends": "realm",
8856 "resolvers": [
8857 {
8858 "resolver": "pkg_resolver",
8859 "from": "parent",
8860 "scheme": "fuchsia-pkg",
8861 }
8862 ]
8863 }
8864 ],
8865 }),
8866 Ok(())
8867 ),
8868 test_cml_environment_with_resolvers_bad_scheme(
8869 json!({
8870 "environments": [
8871 {
8872 "name": "my_env",
8873 "extends": "realm",
8874 "resolvers": [
8875 {
8876 "resolver": "pkg_resolver",
8877 "from": "parent",
8878 "scheme": "9scheme",
8879 }
8880 ]
8881 }
8882 ],
8883 }),
8884 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"9scheme\", expected a valid URL scheme"
8885 ),
8886 test_cml_environment_with_resolvers_duplicate_scheme(
8887 json!({
8888 "environments": [
8889 {
8890 "name": "my_env",
8891 "extends": "realm",
8892 "resolvers": [
8893 {
8894 "resolver": "pkg_resolver",
8895 "from": "parent",
8896 "scheme": "fuchsia-pkg",
8897 },
8898 {
8899 "resolver": "base_resolver",
8900 "from": "parent",
8901 "scheme": "fuchsia-pkg",
8902 }
8903 ]
8904 }
8905 ],
8906 }),
8907 Err(Error::Validate { err, .. }) if &err == "scheme \"fuchsia-pkg\" for resolver \"base_resolver\" is already registered; previously registered to resolver \"pkg_resolver\"."
8908 ),
8909 test_cml_environment_with_resolver_from_missing_child(
8910 json!({
8911 "environments": [
8912 {
8913 "name": "my_env",
8914 "extends": "realm",
8915 "resolvers": [
8916 {
8917 "resolver": "pkg_resolver",
8918 "from": "#missing_child",
8919 "scheme": "fuchsia-pkg",
8920 }
8921 ]
8922 }
8923 ]
8924 }),
8925 Err(Error::Validate { err, .. }) if &err == "\"pkg_resolver\" resolver source \"#missing_child\" does not appear in \"children\""
8926 ),
8927
8928 test_cml_facets_no_span(
8930 json!({
8931 "facets": {
8932 "metadata": {
8933 "title": "foo",
8934 "authors": [ "me", "you" ],
8935 "year": 2018
8936 }
8937 }
8938 }),
8939 Ok(())
8940 ),
8941 test_cml_facets_wrong_type_no_span(
8942 json!({
8943 "facets": 55
8944 }),
8945 Err(Error::Parse { err, .. }) if &err == "invalid type: integer `55`, expected a map"
8946 ),
8947
8948 test_cml_rights_all(
8950 json!({
8951 "use": [
8952 {
8953 "directory": "mydir",
8954 "path": "/mydir",
8955 "rights": ["connect", "enumerate", "read_bytes", "write_bytes",
8956 "execute", "update_attributes", "get_attributes", "traverse",
8957 "modify_directory"],
8958 },
8959 ]
8960 }),
8961 Ok(())
8962 ),
8963 test_cml_rights_invalid(
8964 json!({
8965 "use": [
8966 {
8967 "directory": "mydir",
8968 "path": "/mydir",
8969 "rights": ["cAnnect", "enumerate"],
8970 },
8971 ]
8972 }),
8973 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*`"
8974 ),
8975 test_cml_rights_duplicate(
8976 json!({
8977 "use": [
8978 {
8979 "directory": "mydir",
8980 "path": "/mydir",
8981 "rights": ["connect", "connect"],
8982 },
8983 ]
8984 }),
8985 Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a nonempty array of rights, with unique elements"
8986 ),
8987 test_cml_rights_empty(
8988 json!({
8989 "use": [
8990 {
8991 "directory": "mydir",
8992 "path": "/mydir",
8993 "rights": [],
8994 },
8995 ]
8996 }),
8997 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a nonempty array of rights, with unique elements"
8998 ),
8999 test_cml_rights_alias_star_expansion(
9000 json!({
9001 "use": [
9002 {
9003 "directory": "mydir",
9004 "rights": ["r*"],
9005 "path": "/mydir",
9006 },
9007 ]
9008 }),
9009 Ok(())
9010 ),
9011 test_cml_rights_alias_star_expansion_with_longform(
9012 json!({
9013 "use": [
9014 {
9015 "directory": "mydir",
9016 "rights": ["w*", "read_bytes"],
9017 "path": "/mydir",
9018 },
9019 ]
9020 }),
9021 Ok(())
9022 ),
9023 test_cml_rights_alias_star_expansion_with_longform_collision_no_span(
9024 json!({
9025 "use": [
9026 {
9027 "directory": "mydir",
9028 "path": "/mydir",
9029 "rights": ["r*", "read_bytes"],
9030 },
9031 ]
9032 }),
9033 Err(Error::Validate { err, .. }) if &err == "\"read_bytes\" is duplicated in the rights clause."
9034 ),
9035 test_cml_rights_alias_star_expansion_collision_no_span(
9036 json!({
9037 "use": [
9038 {
9039 "directory": "mydir",
9040 "path": "/mydir",
9041 "rights": ["w*", "x*"],
9042 },
9043 ]
9044 }),
9045 Err(Error::Validate { err, .. }) if &err == "\"x*\" is duplicated in the rights clause."
9046 ),
9047 test_cml_rights_use_invalid_no_span(
9048 json!({
9049 "use": [
9050 { "directory": "mydir", "path": "/mydir" },
9051 ]
9052 }),
9053 Err(Error::Validate { err, .. }) if &err == "This use statement requires a `rights` field. Refer to: https://fuchsia.dev/go/components/directory#consumer."
9054 ),
9055
9056 test_cml_path(
9057 json!({
9058 "capabilities": [
9059 {
9060 "protocol": "foo",
9061 "path": "/foo/in.-_/Bar",
9062 },
9063 ]
9064 }),
9065 Ok(())
9066 ),
9067 test_cml_path_invalid_empty(
9068 json!({
9069 "capabilities": [
9070 { "protocol": "foo", "path": "" },
9071 ]
9072 }),
9073 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"
9074 ),
9075 test_cml_path_invalid_root(
9076 json!({
9077 "capabilities": [
9078 { "protocol": "foo", "path": "/" },
9079 ]
9080 }),
9081 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"
9082 ),
9083 test_cml_path_invalid_absolute_is_relative(
9084 json!({
9085 "capabilities": [
9086 { "protocol": "foo", "path": "foo/bar" },
9087 ]
9088 }),
9089 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"
9090 ),
9091 test_cml_path_invalid_trailing(
9092 json!({
9093 "capabilities": [
9094 { "protocol": "foo", "path":"/foo/bar/" },
9095 ]
9096 }),
9097 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"
9098 ),
9099 test_cml_path_too_long(
9100 json!({
9101 "capabilities": [
9102 { "protocol": "foo", "path": format!("/{}", "a".repeat(4095)) },
9103 ]
9104 }),
9105 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"
9106 ),
9107 test_cml_path_invalid_segment(
9108 json!({
9109 "capabilities": [
9110 { "protocol": "foo", "path": "/foo/../bar" },
9111 ]
9112 }),
9113 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"
9114 ),
9115 test_cml_relative_path(
9116 json!({
9117 "use": [
9118 {
9119 "directory": "foo",
9120 "path": "/foo",
9121 "rights": ["r*"],
9122 "subdir": "Baz/Bar",
9123 },
9124 ]
9125 }),
9126 Ok(())
9127 ),
9128 test_cml_relative_path_invalid_empty(
9129 json!({
9130 "use": [
9131 {
9132 "directory": "foo",
9133 "path": "/foo",
9134 "rights": ["r*"],
9135 "subdir": "",
9136 },
9137 ]
9138 }),
9139 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"
9140 ),
9141 test_cml_relative_path_invalid_root(
9142 json!({
9143 "use": [
9144 {
9145 "directory": "foo",
9146 "path": "/foo",
9147 "rights": ["r*"],
9148 "subdir": "/",
9149 },
9150 ]
9151 }),
9152 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
9153 ),
9154 test_cml_relative_path_invalid_absolute(
9155 json!({
9156 "use": [
9157 {
9158 "directory": "foo",
9159 "path": "/foo",
9160 "rights": ["r*"],
9161 "subdir": "/bar",
9162 },
9163 ]
9164 }),
9165 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bar\", expected a path with no leading `/` and non-empty segments"
9166 ),
9167 test_cml_relative_path_invalid_trailing(
9168 json!({
9169 "use": [
9170 {
9171 "directory": "foo",
9172 "path": "/foo",
9173 "rights": ["r*"],
9174 "subdir": "bar/",
9175 },
9176 ]
9177 }),
9178 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bar/\", expected a path with no leading `/` and non-empty segments"
9179 ),
9180 test_cml_relative_path_too_long(
9181 json!({
9182 "use": [
9183 {
9184 "directory": "foo",
9185 "path": "/foo",
9186 "rights": ["r*"],
9187 "subdir": format!("{}", "a".repeat(4096)),
9188 },
9189 ]
9190 }),
9191 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"
9192 ),
9193 test_cml_relative_ref_too_long(
9194 json!({
9195 "expose": [
9196 {
9197 "protocol": "fuchsia.logger.Log",
9198 "from": &format!("#{}", "a".repeat(256)),
9199 },
9200 ],
9201 "children": [
9202 {
9203 "name": "logger",
9204 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
9205 },
9206 ]
9207 }),
9208 Err(Error::Parse { err, .. }) if &err == "invalid length 257, expected one or an array of \"framework\", \"self\", \"#<child-name>\", or a dictionary path"
9209 ),
9210 test_cml_dictionary_ref_invalid_root(
9211 json!({
9212 "use": [
9213 {
9214 "protocol": "a",
9215 "from": "bad/a",
9216 },
9217 ],
9218 }),
9219 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"
9220 ),
9221 test_cml_dictionary_ref_invalid_path(
9222 json!({
9223 "use": [
9224 {
9225 "protocol": "a",
9226 "from": "parent//a",
9227 },
9228 ],
9229 }),
9230 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"
9231 ),
9232 test_cml_dictionary_ref_too_long(
9233 json!({
9234 "use": [
9235 {
9236 "protocol": "a",
9237 "from": format!("parent/{}", "a".repeat(4089)),
9238 },
9239 ],
9240 }),
9241 Err(Error::Parse { err, .. }) if &err == "invalid length 4096, expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", \"#<collection-name>\", dictionary path, or none"
9242 ),
9243 test_cml_capability_name(
9244 json!({
9245 "use": [
9246 {
9247 "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
9248 },
9249 ]
9250 }),
9251 Ok(())
9252 ),
9253 test_cml_capability_name_invalid(
9254 json!({
9255 "use": [
9256 {
9257 "protocol": "/bad",
9258 },
9259 ]
9260 }),
9261 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a name or nonempty array of names, with unique elements"
9262 ),
9263 test_cml_child_name(
9264 json!({
9265 "children": [
9266 {
9267 "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
9268 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
9269 },
9270 ]
9271 }),
9272 Ok(())
9273 ),
9274 test_cml_child_name_invalid(
9275 json!({
9276 "children": [
9277 {
9278 "name": "/bad",
9279 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
9280 },
9281 ]
9282 }),
9283 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a \
9284 name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]"
9285 ),
9286 test_cml_child_name_too_long(
9287 json!({
9288 "children": [
9289 {
9290 "name": "a".repeat(256),
9291 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
9292 }
9293 ]
9294 }),
9295 Err(Error::Parse { err, .. }) if &err == "invalid length 256, expected a non-empty name no more than 255 characters in length"
9296 ),
9297 test_cml_url(
9298 json!({
9299 "children": [
9300 {
9301 "name": "logger",
9302 "url": "my+awesome-scheme.2://abc123!@$%.com",
9303 },
9304 ]
9305 }),
9306 Ok(())
9307 ),
9308 test_cml_url_host_pound_invalid(
9309 json!({
9310 "children": [
9311 {
9312 "name": "logger",
9313 "url": "my+awesome-scheme.2://abc123!@#$%.com",
9314 },
9315 ]
9316 }),
9317 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"my+awesome-scheme.2://abc123!@#$%.com\", expected a valid URL"
9318 ),
9319 test_cml_url_invalid(
9320 json!({
9321 "children": [
9322 {
9323 "name": "logger",
9324 "url": "fuchsia-pkg",
9325 },
9326 ]
9327 }),
9328 Err(Error::Parse { err, .. }) if &err == "invalid value: string \"fuchsia-pkg\", expected a valid URL"
9329 ),
9330 test_cml_url_too_long(
9331 json!({
9332 "children": [
9333 {
9334 "name": "logger",
9335 "url": &format!("fuchsia-pkg://{}", "a".repeat(4083)),
9336 },
9337 ]
9338 }),
9339 Err(Error::Parse { err, .. }) if &err == "invalid length 4097, expected a non-empty URL no more than 4096 characters in length"
9340 ),
9341 test_cml_duplicate_identifiers_children_collection(
9342 json!({
9343 "children": [
9344 {
9345 "name": "logger",
9346 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
9347 }
9348 ],
9349 "collections": [
9350 {
9351 "name": "logger",
9352 "durability": "transient"
9353 }
9354 ]
9355 }),
9356 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"collections\" and once in \"children\""
9357 ),
9358 test_cml_duplicate_identifiers_children_storage(
9359 json!({
9360 "children": [
9361 {
9362 "name": "logger",
9363 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
9364 }
9365 ],
9366 "capabilities": [
9367 {
9368 "storage": "logger",
9369 "path": "/logs",
9370 "from": "parent"
9371 }
9372 ]
9373 }),
9374 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"children\""
9375 ),
9376 test_cml_duplicate_identifiers_collection_storage(
9377 json!({
9378 "collections": [
9379 {
9380 "name": "logger",
9381 "durability": "transient"
9382 }
9383 ],
9384 "capabilities": [
9385 {
9386 "storage": "logger",
9387 "path": "/logs",
9388 "from": "parent"
9389 }
9390 ]
9391 }),
9392 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"collections\""
9393 ),
9394 test_cml_duplicate_identifiers_children_runners(
9395 json!({
9396 "children": [
9397 {
9398 "name": "logger",
9399 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
9400 }
9401 ],
9402 "capabilities": [
9403 {
9404 "runner": "logger",
9405 "from": "parent"
9406 }
9407 ]
9408 }),
9409 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"runners\" and once in \"children\""
9410 ),
9411 test_cml_duplicate_identifiers_environments(
9412 json!({
9413 "children": [
9414 {
9415 "name": "logger",
9416 "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
9417 }
9418 ],
9419 "environments": [
9420 {
9421 "name": "logger",
9422 }
9423 ]
9424 }),
9425 Err(Error::Validate { err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"environments\" and once in \"children\""
9426 ),
9427
9428 test_deny_unknown_fields(
9430 json!(
9431 {
9432 "program": {
9433 "runner": "elf",
9434 "binary": "bin/app",
9435 },
9436 "unknown_field": {},
9437 }
9438 ),
9439 Err(Error::Parse { err, .. }) if err.starts_with("unknown field `unknown_field`, expected one of ")
9440 ),
9441 test_offer_source_availability_unknown(
9442 json!({
9443 "children": [
9444 {
9445 "name": "foo",
9446 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9447 },
9448 ],
9449 "offer": [
9450 {
9451 "protocol": "fuchsia.examples.Echo",
9452 "from": "#bar",
9453 "to": "#foo",
9454 "availability": "optional",
9455 "source_availability": "unknown",
9456 },
9457 ],
9458 }),
9459 Ok(())
9460 ),
9461 test_offer_source_availability_required(
9462 json!({
9463 "children": [
9464 {
9465 "name": "foo",
9466 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9467 },
9468 ],
9469 "offer": [
9470 {
9471 "protocol": "fuchsia.examples.Echo",
9472 "from": "#bar",
9473 "to": "#foo",
9474 "source_availability": "required",
9475 },
9476 ],
9477 }),
9478 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
9479 ),
9480 test_offer_source_availability_omitted(
9481 json!({
9482 "children": [
9483 {
9484 "name": "foo",
9485 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9486 },
9487 ],
9488 "offer": [
9489 {
9490 "protocol": "fuchsia.examples.Echo",
9491 "from": "#bar",
9492 "to": "#foo",
9493 },
9494 ],
9495 }),
9496 Err(Error::Validate { err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
9497 ),
9498 test_cml_use_invalid_availability_no_span(
9499 json!({
9500 "use": [
9501 {
9502 "protocol": "fuchsia.examples.Echo",
9503 "availability": "same_as_target",
9504 },
9505 ],
9506 }),
9507 Err(Error::Validate { err, .. }) if &err == "\"availability: same_as_target\" cannot be used with use declarations"
9508 ),
9509 test_offer_source_void_availability_required(
9510 json!({
9511 "children": [
9512 {
9513 "name": "foo",
9514 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9515 },
9516 ],
9517 "offer": [
9518 {
9519 "protocol": "fuchsia.examples.Echo",
9520 "from": "void",
9521 "to": "#foo",
9522 "availability": "required",
9523 },
9524 ],
9525 }),
9526 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
9527 ),
9528 test_offer_source_void_availability_same_as_target(
9529 json!({
9530 "children": [
9531 {
9532 "name": "foo",
9533 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9534 },
9535 ],
9536 "offer": [
9537 {
9538 "protocol": "fuchsia.examples.Echo",
9539 "from": "void",
9540 "to": "#foo",
9541 "availability": "same_as_target",
9542 },
9543 ],
9544 }),
9545 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
9546 ),
9547 test_offer_source_missing_availability_required(
9548 json!({
9549 "children": [
9550 {
9551 "name": "foo",
9552 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9553 },
9554 ],
9555 "offer": [
9556 {
9557 "protocol": "fuchsia.examples.Echo",
9558 "from": "#bar",
9559 "to": "#foo",
9560 "availability": "required",
9561 "source_availability": "unknown",
9562 },
9563 ],
9564 }),
9565 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\""
9566 ),
9567 test_offer_source_missing_availability_same_as_target(
9568 json!({
9569 "children": [
9570 {
9571 "name": "foo",
9572 "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm"
9573 },
9574 ],
9575 "offer": [
9576 {
9577 "protocol": "fuchsia.examples.Echo",
9578 "from": "#bar",
9579 "to": "#foo",
9580 "availability": "same_as_target",
9581 "source_availability": "unknown",
9582 },
9583 ],
9584 }),
9585 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\""
9586 ),
9587 test_expose_source_availability_unknown(
9588 json!({
9589 "expose": [
9590 {
9591 "protocol": "fuchsia.examples.Echo",
9592 "from": "#bar",
9593 "availability": "optional",
9594 "source_availability": "unknown",
9595 },
9596 ],
9597 }),
9598 Ok(())
9599 ),
9600 test_expose_source_availability_required(
9601 json!({
9602 "expose": [
9603 {
9604 "protocol": "fuchsia.examples.Echo",
9605 "from": "#bar",
9606 "source_availability": "required",
9607 },
9608 ],
9609 }),
9610 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
9611 ),
9612 test_expose_source_availability_omitted(
9613 json!({
9614 "expose": [
9615 {
9616 "protocol": "fuchsia.examples.Echo",
9617 "from": "#bar",
9618 },
9619 ],
9620 }),
9621 Err(Error::Validate { err, .. }) if &err == "\"expose\" source \"#bar\" does not appear in \"children\" or \"capabilities\""
9622 ),
9623 test_expose_source_void_availability_required(
9624 json!({
9625 "expose": [
9626 {
9627 "protocol": "fuchsia.examples.Echo",
9628 "from": "void",
9629 "availability": "required",
9630 },
9631 ],
9632 }),
9633 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
9634 ),
9635 test_expose_source_void_availability_same_as_target(
9636 json!({
9637 "expose": [
9638 {
9639 "protocol": "fuchsia.examples.Echo",
9640 "from": "void",
9641 "availability": "same_as_target",
9642 },
9643 ],
9644 }),
9645 Err(Error::Validate { err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\", capabilities: \"fuchsia.examples.Echo\", from: \"void\""
9646 ),
9647 test_expose_source_missing_availability_required(
9648 json!({
9649 "expose": [
9650 {
9651 "protocol": "fuchsia.examples.Echo",
9652 "from": "#bar",
9653 "availability": "required",
9654 "source_availability": "unknown",
9655 },
9656 ],
9657 }),
9658 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\""
9659 ),
9660 test_expose_source_missing_availability_same_as_target(
9661 json!({
9662 "expose": [
9663 {
9664 "protocol": "fuchsia.examples.Echo",
9665 "from": "#bar",
9666 "availability": "same_as_target",
9667 "source_availability": "unknown",
9668 },
9669 ],
9670 }),
9671 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\""
9672 ),
9673 }
9674
9675 test_validate_cml! {
9677 test_cml_validate_use_service(
9678 json!({
9679 "use": [
9680 { "service": "CoolFonts", "path": "/svc/fuchsia.fonts.Provider" },
9681 { "service": "fuchsia.component.Realm", "from": "framework" },
9682 ],
9683 }),
9684 Ok(())
9685 ),
9686 test_cml_use_invalid_from_with_service_no_span(
9687 json!({
9688 "use": [ { "service": "foo", "from": "debug" } ]
9689 }),
9690 Err(Error::Validate { err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
9691 ),
9692 test_cml_validate_offer_service(
9693 json!({
9694 "offer": [
9695 {
9696 "service": "fuchsia.logger.Log",
9697 "from": "#logger",
9698 "to": [ "#echo_server", "#modular" ],
9699 "as": "fuchsia.logger.SysLog"
9700 },
9701 {
9702 "service": "fuchsia.fonts.Provider",
9703 "from": "parent",
9704 "to": [ "#echo_server" ]
9705 },
9706 {
9707 "service": "fuchsia.net.Netstack",
9708 "from": "self",
9709 "to": [ "#echo_server" ]
9710 },
9711 ],
9712 "children": [
9713 {
9714 "name": "logger",
9715 "url": "fuchsia-pkg://logger",
9716 },
9717 {
9718 "name": "echo_server",
9719 "url": "fuchsia-pkg://echo_server",
9720 }
9721 ],
9722 "collections": [
9723 {
9724 "name": "modular",
9725 "durability": "transient",
9726 },
9727 ],
9728 "capabilities": [
9729 { "service": "fuchsia.net.Netstack" },
9730 ],
9731 }),
9732 Ok(())
9733 ),
9734 test_cml_validate_expose_service(
9735 json!(
9736 {
9737 "expose": [
9738 {
9739 "service": "fuchsia.fonts.Provider",
9740 "from": "self",
9741 },
9742 {
9743 "service": "fuchsia.logger.Log",
9744 "from": "#logger",
9745 "as": "logger"
9746 },
9747 ],
9748 "capabilities": [
9749 { "service": "fuchsia.fonts.Provider" },
9750 ],
9751 "children": [
9752 {
9753 "name": "logger",
9754 "url": "fuchsia-pkg://logger",
9755 },
9756 ]
9757 }
9758 ),
9759 Ok(())
9760 ),
9761 test_cml_validate_expose_service_multi_source(
9762 json!(
9763 {
9764 "expose": [
9765 {
9766 "service": "fuchsia.my.Service",
9767 "from": [ "self", "#a" ],
9768 },
9769 {
9770 "service": "fuchsia.my.Service",
9771 "from": "#coll",
9772 },
9773 ],
9774 "capabilities": [
9775 { "service": "fuchsia.my.Service" },
9776 ],
9777 "children": [
9778 {
9779 "name": "a",
9780 "url": "fuchsia-pkg://a",
9781 },
9782 ],
9783 "collections": [
9784 {
9785 "name": "coll",
9786 "durability": "transient",
9787 },
9788 ],
9789 }
9790 ),
9791 Ok(())
9792 ),
9793 test_cml_validate_offer_service_multi_source(
9794 json!(
9795 {
9796 "offer": [
9797 {
9798 "service": "fuchsia.my.Service",
9799 "from": [ "self", "parent" ],
9800 "to": "#b",
9801 },
9802 {
9803 "service": "fuchsia.my.Service",
9804 "from": [ "#a", "#coll" ],
9805 "to": "#b",
9806 },
9807 ],
9808 "capabilities": [
9809 { "service": "fuchsia.my.Service" },
9810 ],
9811 "children": [
9812 {
9813 "name": "a",
9814 "url": "fuchsia-pkg://a",
9815 },
9816 {
9817 "name": "b",
9818 "url": "fuchsia-pkg://b",
9819 },
9820 ],
9821 "collections": [
9822 {
9823 "name": "coll",
9824 "durability": "transient",
9825 },
9826 ],
9827 }
9828 ),
9829 Ok(())
9830 ),
9831 test_cml_service(
9832 json!({
9833 "capabilities": [
9834 {
9835 "protocol": "a",
9836 "path": "/minfs",
9837 },
9838 {
9839 "protocol": "b",
9840 "path": "/data",
9841 },
9842 {
9843 "protocol": "c",
9844 },
9845 ],
9846 }),
9847 Ok(())
9848 ),
9849 test_cml_service_multi(
9850 json!({
9851 "capabilities": [
9852 {
9853 "service": ["a", "b", "c"],
9854 },
9855 ],
9856 }),
9857 Ok(())
9858 ),
9859 test_cml_service_multi_invalid_path_no_span(
9860 json!({
9861 "capabilities": [
9862 {
9863 "service": ["a", "b", "c"],
9864 "path": "/minfs",
9865 },
9866 ],
9867 }),
9868 Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `service` is supplied."
9869 ),
9870 test_cml_service_all_valid_chars(
9871 json!({
9872 "capabilities": [
9873 {
9874 "service": "abcdefghijklmnopqrstuvwxyz0123456789_-service",
9875 },
9876 ],
9877 }),
9878 Ok(())
9879 ),
9880 }
9881
9882 test_validate_cml_with_feature! { FeatureSet::from(vec![]), {
9884 test_cml_configs(
9885 json!({
9886 "config": {
9887 "verbosity": {
9888 "type": "string",
9889 "max_size": 20,
9890 },
9891 "timeout": { "type": "uint64" },
9892 "tags": {
9893 "type": "vector",
9894 "max_count": 10,
9895 "element": {
9896 "type": "string",
9897 "max_size": 50
9898 }
9899 }
9900 }
9901 }),
9902 Ok(())
9903 ),
9904
9905 test_cml_configs_not_object(
9906 json!({
9907 "config": "abcd"
9908 }),
9909 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a map"
9910 ),
9911
9912 test_cml_configs_empty(
9913 json!({
9914 "config": {
9915 }
9916 }),
9917 Err(Error::Validate { err, .. }) if &err == "'config' section is empty"
9918 ),
9919
9920 test_cml_configs_bad_type(
9921 json!({
9922 "config": {
9923 "verbosity": 123456
9924 }
9925 }),
9926 Err(Error::Parse { err, .. }) if &err == "invalid type: integer `123456`, expected internally tagged enum ConfigValueType"
9927 ),
9928
9929 test_cml_configs_unknown_type(
9930 json!({
9931 "config": {
9932 "verbosity": {
9933 "type": "foo"
9934 }
9935 }
9936 }),
9937 Err(Error::Parse { err, .. }) if &err == "unknown variant `foo`, expected one of `bool`, `uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`, `int64`, `string`, `vector`"
9938 ),
9939
9940 test_cml_configs_no_max_count_vector(
9941 json!({
9942 "config": {
9943 "tags": {
9944 "type": "vector",
9945 "element": {
9946 "type": "string",
9947 "max_size": 50,
9948 }
9949 }
9950 }
9951 }),
9952 Err(Error::Parse { err, .. }) if &err == "missing field `max_count`"
9953 ),
9954
9955 test_cml_configs_no_max_size_string(
9956 json!({
9957 "config": {
9958 "verbosity": {
9959 "type": "string",
9960 }
9961 }
9962 }),
9963 Err(Error::Parse { err, .. }) if &err == "missing field `max_size`"
9964 ),
9965
9966 test_cml_configs_no_max_size_string_vector(
9967 json!({
9968 "config": {
9969 "tags": {
9970 "type": "vector",
9971 "max_count": 10,
9972 "element": {
9973 "type": "string",
9974 }
9975 }
9976 }
9977 }),
9978 Err(Error::Parse { err, .. }) if &err == "missing field `max_size`"
9979 ),
9980
9981 test_cml_configs_empty_key(
9982 json!({
9983 "config": {
9984 "": {
9985 "type": "bool"
9986 }
9987 }
9988 }),
9989 Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty name no more than 64 characters in length"
9990 ),
9991
9992 test_cml_configs_too_long_key(
9993 json!({
9994 "config": {
9995 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": {
9996 "type": "bool"
9997 }
9998 }
9999 }),
10000 Err(Error::Parse { err, .. }) if &err == "invalid length 74, expected a non-empty name no more than 64 characters in length"
10001 ),
10002 test_cml_configs_key_starts_with_number(
10003 json!({
10004 "config": {
10005 "8abcd": { "type": "uint8" }
10006 }
10007 }),
10008 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"
10009 ),
10010
10011 test_cml_configs_key_ends_with_underscore(
10012 json!({
10013 "config": {
10014 "abcd_": { "type": "uint8" }
10015 }
10016 }),
10017 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"
10018 ),
10019
10020 test_cml_configs_capitals_in_key(
10021 json!({
10022 "config": {
10023 "ABCD": { "type": "uint8" }
10024 }
10025 }),
10026 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"
10027 ),
10028
10029 test_cml_configs_special_chars_in_key(
10030 json!({
10031 "config": {
10032 "!@#$": { "type": "uint8" }
10033 }
10034 }),
10035 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"
10036 ),
10037
10038 test_cml_configs_dashes_in_key(
10039 json!({
10040 "config": {
10041 "abcd-efgh": { "type": "uint8" }
10042 }
10043 }),
10044 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"
10045 ),
10046
10047 test_cml_configs_bad_max_size_string(
10048 json!({
10049 "config": {
10050 "verbosity": {
10051 "type": "string",
10052 "max_size": "abcd"
10053 }
10054 }
10055 }),
10056 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
10057 ),
10058
10059 test_cml_configs_zero_max_size_string(
10060 json!({
10061 "config": {
10062 "verbosity": {
10063 "type": "string",
10064 "max_size": 0
10065 }
10066 }
10067 }),
10068 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
10069 ),
10070
10071 test_cml_configs_bad_max_count_on_vector(
10072 json!({
10073 "config": {
10074 "toggles": {
10075 "type": "vector",
10076 "max_count": "abcd",
10077 "element": {
10078 "type": "bool"
10079 }
10080 }
10081 }
10082 }),
10083 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
10084 ),
10085
10086 test_cml_configs_zero_max_count_on_vector(
10087 json!({
10088 "config": {
10089 "toggles": {
10090 "type": "vector",
10091 "max_count": 0,
10092 "element": {
10093 "type": "bool"
10094 }
10095 }
10096 }
10097 }),
10098 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
10099 ),
10100
10101 test_cml_configs_bad_max_size_string_vector(
10102 json!({
10103 "config": {
10104 "toggles": {
10105 "type": "vector",
10106 "max_count": 100,
10107 "element": {
10108 "type": "string",
10109 "max_size": "abcd"
10110 }
10111 }
10112 }
10113 }),
10114 Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a nonzero u32"
10115 ),
10116
10117 test_cml_configs_zero_max_size_string_vector(
10118 json!({
10119 "config": {
10120 "toggles": {
10121 "type": "vector",
10122 "max_count": 100,
10123 "element": {
10124 "type": "string",
10125 "max_size": 0
10126 }
10127 }
10128 }
10129 }),
10130 Err(Error::Parse { err, .. }) if &err == "invalid value: integer `0`, expected a nonzero u32"
10131 ),
10132 }}
10133
10134 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::AllowLongNames]), {
10136 test_cml_validate_set_allow_long_names_true(
10137 json!({
10138 "collections": [
10139 {
10140 "name": "foo",
10141 "durability": "transient",
10142 "allow_long_names": true
10143 },
10144 ],
10145 }),
10146 Ok(())
10147 ),
10148 test_cml_validate_set_allow_long_names_false(
10149 json!({
10150 "collections": [
10151 {
10152 "name": "foo",
10153 "durability": "transient",
10154 "allow_long_names": false
10155 },
10156 ],
10157 }),
10158 Ok(())
10159 ),
10160 }}
10161
10162 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::UseDictionaries]), {
10164 test_cml_validate_set_allow_use_dictionaries(
10165 json!({
10166 "use": [
10167 {
10168 "protocol": "fuchsia.examples.Echo",
10169 "path": "/svc/fuchsia.examples.Echo",
10170 },
10171 {
10172 "dictionary": "toolbox",
10173 "path": "/svc",
10174 },
10175 ],
10176 }),
10177 Ok(())
10178 ),
10179 }}
10180
10181 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::UseDictionaries]), {
10183 test_cml_validate_set_allow_use_2_dictionaries_at_same_path(
10184 json!({
10185 "use": [
10186 {
10187 "dictionary": "toolbox-1",
10188 "path": "/svc",
10189 },
10190 {
10191 "dictionary": "toolbox-2",
10192 "path": "/svc",
10193 },
10194 ],
10195 }),
10196 Ok(())
10197 ),
10198 }}
10199
10200 test_validate_cml! {
10203 test_cml_allow_long_names_without_feature_no_span(
10204 json!({
10205 "collections": [
10206 {
10207 "name": "foo",
10208 "durability": "transient",
10209 "allow_long_names": true
10210 },
10211 ],
10212 }),
10213 Err(Error::RestrictedFeature(s)) if s == "allow_long_names"
10214 ),
10215 }
10216
10217 test_validate_cml! {
10219 test_cml_allow_use_dictionary_without_feature(
10220 json!({
10221 "use": [
10222 {
10223 "dictionary": "foo",
10224 "path": "/foo",
10225 },
10226 ],
10227 }),
10228 Err(Error::RestrictedFeature(s)) if s == "use_dictionaries"
10229 ),
10230 }
10231
10232 test_validate_cml! {
10234 test_valid_empty_facets_no_span(
10235 json!({
10236 "facets": {}
10237 }),
10238 Ok(())
10239 ),
10240
10241 test_invalid_empty_facets_no_span(
10242 json!({
10243 "facets": ""
10244 }),
10245 Err(err) if err.to_string().contains("invalid type: string")
10246 ),
10247 test_valid_empty_fuchsia_test_facet_no_span(
10248 json!({
10249 "facets": {TEST_FACET_KEY: {}}
10250 }),
10251 Ok(())
10252 ),
10253
10254 test_valid_allowed_pkg_without_feature_no_span(
10255 json!({
10256 "facets": {
10257 TEST_TYPE_FACET_KEY: "some_realm",
10258 TEST_FACET_KEY: {
10259 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10260 }
10261 }
10262 }),
10263 Ok(())
10264 ),
10265 }
10266
10267 test_validate_cml_with_context! {
10269 test_valid_empty_facets(
10270 json!({
10271 "facets": {}
10272 }),
10273 Ok(())
10274 ),
10275
10276 test_invalid_empty_facets(
10277 json!({
10278 "facets": ""
10279 }),
10280 Err(err) if err.to_string().contains("invalid type: string")
10281 ),
10282 test_valid_empty_fuchsia_test_facet(
10283 json!({
10284 "facets": {TEST_FACET_KEY: {}}
10285 }),
10286 Ok(())
10287 ),
10288
10289 test_valid_allowed_pkg_without_feature(
10290 json!({
10291 "facets": {
10292 TEST_TYPE_FACET_KEY: "some_realm",
10293 TEST_FACET_KEY: {
10294 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10295 }
10296 }
10297 }),
10298 Ok(())
10299 ),
10300 }
10301
10302 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::RestrictTestTypeInFacet]), {
10304 test_valid_empty_facets_with_test_type_feature_enabled_no_span(
10305 json!({
10306 "facets": {}
10307 }),
10308 Ok(())
10309 ),
10310 test_valid_empty_fuchsia_test_facet_with_test_type_feature_enabled_no_span(
10311 json!({
10312 "facets": {TEST_FACET_KEY: {}}
10313 }),
10314 Ok(())
10315 ),
10316
10317 test_invalid_test_type_with_feature_enabled_no_span(
10318 json!({
10319 "facets": {
10320 TEST_FACET_KEY: {
10321 TEST_TYPE_FACET_KEY: "some_realm",
10322 }
10323 }
10324 }),
10325 Err(err) if err.to_string().contains(TEST_TYPE_FACET_KEY)
10326 ),
10327 }}
10328
10329 test_validate_cml_with_feature_context! { FeatureSet::from(vec![Feature::RestrictTestTypeInFacet]), {
10331 test_valid_empty_facets_with_test_type_feature_enabled(
10332 json!({
10333 "facets": {}
10334 }),
10335 Ok(())
10336 ),
10337 test_valid_empty_fuchsia_test_facet_with_test_type_feature_enabled(
10338 json!({
10339 "facets": {TEST_FACET_KEY: {}}
10340 }),
10341 Ok(())
10342 ),
10343
10344 test_invalid_test_type_with_feature_enabled(
10345 json!({
10346 "facets": {
10347 TEST_FACET_KEY: {
10348 TEST_TYPE_FACET_KEY: "some_realm",
10349 }
10350 }
10351 }),
10352 Err(err) if err.to_string().contains(TEST_TYPE_FACET_KEY)
10353 ),
10354 }}
10355
10356 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::AllowNonHermeticPackages]), {
10358 test_valid_empty_facets_with_feature_disabled_no_span(
10359 json!({
10360 "facets": {}
10361 }),
10362 Ok(())
10363 ),
10364 test_valid_empty_fuchsia_test_facet_with_feature_disabled_no_span(
10365 json!({
10366 "facets": {TEST_FACET_KEY: {}}
10367 }),
10368 Ok(())
10369 ),
10370
10371 test_valid_allowed_pkg_with_feature_disabled_no_span(
10372 json!({
10373 "facets": {
10374 TEST_FACET_KEY: {
10375 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10376 }
10377 }
10378 }),
10379 Ok(())
10380 ),
10381 }}
10382
10383 test_validate_cml_with_feature_context! { FeatureSet::from(vec![Feature::AllowNonHermeticPackages]), {
10385 test_valid_empty_facets_with_feature_disabled(
10386 json!({
10387 "facets": {}
10388 }),
10389 Ok(())
10390 ),
10391 test_valid_empty_fuchsia_test_facet_with_feature_disabled(
10392 json!({
10393 "facets": {TEST_FACET_KEY: {}}
10394 }),
10395 Ok(())
10396 ),
10397
10398 test_valid_allowed_pkg_with_feature_disabled(
10399 json!({
10400 "facets": {
10401 TEST_FACET_KEY: {
10402 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10403 }
10404 }
10405 }),
10406 Ok(())
10407 ),
10408 }}
10409
10410 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature]), {
10412 test_valid_empty_facets_with_feature_enabled_no_span(
10413 json!({
10414 "facets": {}
10415 }),
10416 Ok(())
10417 ),
10418 test_valid_empty_fuchsia_test_facet_with_feature_enabled_no_span(
10419 json!({
10420 "facets": {TEST_FACET_KEY: {}}
10421 }),
10422 Ok(())
10423 ),
10424
10425 test_invalid_allowed_pkg_with_feature_enabled_no_span(
10426 json!({
10427 "facets": {
10428 TEST_FACET_KEY: {
10429 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10430 }
10431 }
10432 }),
10433 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10434 ),
10435 }}
10436
10437 test_validate_cml_with_feature_context! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature]), {
10439 test_valid_empty_facets_with_feature_enabled(
10440 json!({
10441 "facets": {}
10442 }),
10443 Ok(())
10444 ),
10445 test_valid_empty_fuchsia_test_facet_with_feature_enabled(
10446 json!({
10447 "facets": {TEST_FACET_KEY: {}}
10448 }),
10449 Ok(())
10450 ),
10451
10452 test_invalid_allowed_pkg_with_feature_enabled(
10453 json!({
10454 "facets": {
10455 TEST_FACET_KEY: {
10456 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10457 }
10458 }
10459 }),
10460 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10461 ),
10462 }}
10463
10464 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature, Feature::AllowNonHermeticPackages]), {
10466 test_invalid_empty_facets_with_feature_enabled_no_span(
10467 json!({
10468 "facets": {}
10469 }),
10470 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10471 ),
10472 test_invalid_empty_fuchsia_test_facet_with_feature_enabled_no_span(
10473 json!({
10474 "facets": {TEST_FACET_KEY: {}}
10475 }),
10476 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10477 ),
10478
10479 test_valid_allowed_pkg_with_feature_enabled_no_span(
10480 json!({
10481 "facets": {
10482 TEST_FACET_KEY: {
10483 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10484 }
10485 }
10486 }),
10487 Ok(())
10488 ),
10489 }}
10490
10491 test_validate_cml_with_feature_context! { FeatureSet::from(vec![Feature::EnableAllowNonHermeticPackagesFeature, Feature::AllowNonHermeticPackages]), {
10493 test_invalid_empty_facets_with_feature_enabled(
10494 json!({
10495 "facets": {}
10496 }),
10497 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10498 ),
10499
10500 test_invalid_empty_fuchsia_test_facet_with_feature_enabled(
10501 json!({
10502 "facets": {TEST_FACET_KEY: {}}
10503 }),
10504 Err(err) if err.to_string().contains(&Feature::AllowNonHermeticPackages.to_string())
10505 ),
10506
10507 test_valid_allowed_pkg_with_feature_enabled(
10508 json!({
10509 "facets": {
10510 TEST_FACET_KEY: {
10511 TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: [ "some_pkg" ]
10512 }
10513 }
10514 }),
10515 Ok(())
10516 ),
10517 }}
10518
10519 test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::DynamicDictionaries]), {
10520 test_cml_offer_to_dictionary_unsupported(
10521 json!({
10522 "offer": [
10523 {
10524 "event_stream": "p",
10525 "from": "parent",
10526 "to": "self/dict",
10527 },
10528 ],
10529 "capabilities": [
10530 {
10531 "dictionary": "dict",
10532 },
10533 ],
10534 }),
10535 Err(Error::Validate { err, .. }) if &err == "\"offer\" to dictionary \
10536 \"self/dict\" for \"event_stream\" but dictionaries do not support this type yet."
10537 ),
10538 test_cml_dictionary_ref(
10539 json!({
10540 "use": [
10541 {
10542 "protocol": "a",
10543 "from": "parent/a",
10544 },
10545 {
10546 "protocol": "b",
10547 "from": "#child/a/b",
10548 },
10549 {
10550 "protocol": "c",
10551 "from": "self/a/b/c",
10552 },
10553 ],
10554 "capabilities": [
10555 {
10556 "dictionary": "a",
10557 },
10558 ],
10559 "children": [
10560 {
10561 "name": "child",
10562 "url": "fuchsia-pkg://child",
10563 },
10564 ],
10565 }),
10566 Ok(())
10567 ),
10568 test_cml_expose_dictionary_from_self(
10569 json!({
10570 "expose": [
10571 {
10572 "dictionary": "foo_dictionary",
10573 "from": "self",
10574 },
10575 ],
10576 "capabilities": [
10577 {
10578 "dictionary": "foo_dictionary",
10579 },
10580 ]
10581 }),
10582 Ok(())
10583 ),
10584 test_cml_offer_to_dictionary_duplicate(
10585 json!({
10586 "offer": [
10587 {
10588 "protocol": "p",
10589 "from": "parent",
10590 "to": "self/dict",
10591 },
10592 {
10593 "protocol": "p",
10594 "from": "#child",
10595 "to": "self/dict",
10596 },
10597 ],
10598 "capabilities": [
10599 {
10600 "dictionary": "dict",
10601 },
10602 ],
10603 "children": [
10604 {
10605 "name": "child",
10606 "url": "fuchsia-pkg://child",
10607 },
10608 ],
10609 }),
10610 Err(Error::Validate { err, .. }) if &err == "\"p\" is a duplicate \"offer\" target capability for \"self/dict\""
10611 ),
10612 test_cml_offer_to_dictionary_dynamic(
10613 json!({
10614 "offer": [
10615 {
10616 "protocol": "p",
10617 "from": "parent",
10618 "to": "self/dict",
10619 },
10620 ],
10621 "capabilities": [
10622 {
10623 "dictionary": "dict",
10624 "path": "/out/dir",
10625 },
10626 ],
10627 }),
10628 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."
10629 ),
10630 }}
10631
10632 test_validate_cml! {
10635 test_cml_aggregate_expose(
10636 json!({
10637 "expose": [
10638 {
10639 "service": "fuchsia.foo.Bar",
10640 "from": ["#a", "#b"],
10641 },
10642 ],
10643 "children": [
10644 {
10645 "name": "a",
10646 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm",
10647 },
10648 {
10649 "name": "b",
10650 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm",
10651 },
10652 ],
10653 }),
10654 Ok(())
10655 ),
10656 test_cml_aggregate_offer(
10657 json!({
10658 "offer": [
10659 {
10660 "service": "fuchsia.foo.Bar",
10661 "from": ["#a", "#b"],
10662 "to": "#target",
10663 },
10664 ],
10665 "children": [
10666 {
10667 "name": "a",
10668 "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm",
10669 },
10670 {
10671 "name": "b",
10672 "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm",
10673 },
10674 {
10675 "name": "target",
10676 "url": "fuchsia-pkg://fuchsia.com/target#meta/target.cm",
10677 },
10678 ],
10679 }),
10680 Ok(())
10681 ),
10682 }
10683
10684 use crate::translate::test_util::must_parse_cml;
10685 use crate::translate::{CompileOptions, compile};
10686
10687 #[test]
10688 fn test_cml_use_bad_config_from_self() {
10689 let input = must_parse_cml!({
10690 "use": [
10691 {
10692 "config": "fuchsia.config.MyConfig",
10693 "key": "my_config",
10694 "type": "bool",
10695 "from": "self",
10696 },
10697 ],
10698 });
10699
10700 let options = CompileOptions::new();
10701 assert_matches!(compile(&input, options), Err(Error::Validate { .. }));
10702 }
10703
10704 test_validate_cml! {
10706 a_test_cml_use_config(
10707 json!({"use": [
10708 {
10709 "config": "fuchsia.config.MyConfig",
10710 "key": "my_config",
10711 "type": "bool",
10712 },
10713 ],}),
10714 Ok(())
10715 ),
10716 test_cml_use_config_good_vector(
10717 json!({"use": [
10718 {
10719 "config": "fuchsia.config.MyConfig",
10720 "key": "my_config",
10721 "type": "vector",
10722 "element": { "type": "bool"},
10723 "max_count": 1,
10724 },
10725 ],}),
10726 Ok(())
10727 ),
10728 test_cml_use_config_bad_vector_no_span(
10729 json!({"use": [
10730 {
10731 "config": "fuchsia.config.MyConfig",
10732 "key": "my_config",
10733 "type": "vector",
10734 "element": { "type": "bool"},
10735 },
10737 ],}),
10738 Err(Error::Validate {err, .. })
10739 if &err == "Config 'fuchsia.config.MyConfig' is type Vector but is missing field 'max_count'"
10740 ),
10741 test_cml_use_config_bad_string_no_span(
10742 json!({"use": [
10743 {
10744 "config": "fuchsia.config.MyConfig",
10745 "key": "my_config",
10746 "type": "string",
10747 },
10749 ],}),
10750 Err(Error::Validate { err, .. })
10751 if &err == "Config 'fuchsia.config.MyConfig' is type String but is missing field 'max_size'"
10752 ),
10753
10754 test_cml_optional_use_no_config(
10755 json!({"use": [
10756 {
10757 "config": "fuchsia.config.MyConfig",
10758 "key": "my_config",
10759 "type": "bool",
10760 "availability": "optional",
10761 },
10762 ],}),
10763 Err(Error::Validate {err, ..})
10764 if &err == "Optionally using a config capability without a default requires a matching 'config' section."
10765 ),
10766 test_cml_transitional_use_no_config(
10767 json!({"use": [
10768 {
10769 "config": "fuchsia.config.MyConfig",
10770 "key": "my_config",
10771 "type": "bool",
10772 "availability": "transitional",
10773 },
10774 ],}),
10775 Err(Error::Validate {err, ..})
10776 if &err == "Optionally using a config capability without a default requires a matching 'config' section."
10777 ),
10778 test_cml_optional_use_bad_type(
10779 json!({"use": [
10780 {
10781 "config": "fuchsia.config.MyConfig",
10782 "key": "my_config",
10783 "type": "bool",
10784 "availability": "optional",
10785 },
10786 ],
10787 "config": {
10788 "my_config": { "type": "uint8"}
10789 }}),
10790 Err(Error::Validate {err, ..})
10791 if &err == "Use and config block differ on type for key 'my_config'"
10792 ),
10793
10794 test_config_required_with_default_no_span(
10795 json!({"use": [
10796 {
10797 "config": "fuchsia.config.MyConfig",
10798 "key": "my_config",
10799 "type": "bool",
10800 "default": "true",
10801 },
10802 ]}),
10803 Err(Error::Validate {err, ..})
10804 if &err == "Config 'fuchsia.config.MyConfig' is required and has a default value"
10805 ),
10806
10807 test_cml_optional_use_good(
10808 json!({"use": [
10809 {
10810 "config": "fuchsia.config.MyConfig",
10811 "key": "my_config",
10812 "type": "bool",
10813 "availability": "optional",
10814 },
10815 ],
10816 "config": {
10817 "my_config": { "type": "bool"},
10818 }
10819 }),
10820 Ok(())
10821 ),
10822 test_cml_use_two_types_bad_no_span(
10823 json!({"use": [
10824 {
10825 "protocol": "fuchsia.protocol.MyProtocol",
10826 "service": "fuchsia.service.MyService",
10827 },
10828 ],
10829 }),
10830 Err(Error::Validate {err, ..})
10831 if &err == "use declaration has multiple capability types defined: [\"service\", \"protocol\"]"
10832 ),
10833 test_cml_offer_two_types_bad(
10834 json!({"offer": [
10835 {
10836 "protocol": "fuchsia.protocol.MyProtocol",
10837 "service": "fuchsia.service.MyService",
10838 "from": "self",
10839 "to" : "#child",
10840 },
10841 ],
10842 }),
10843 Err(Error::Validate {err, ..})
10844 if &err == "offer declaration has multiple capability types defined: [\"service\", \"protocol\"]"
10845 ),
10846 test_cml_expose_two_types_bad_no_span(
10847 json!({"expose": [
10848 {
10849 "protocol": "fuchsia.protocol.MyProtocol",
10850 "service": "fuchsia.service.MyService",
10851 "from" : "self",
10852 },
10853 ],
10854 }),
10855 Err(Error::Validate {err, ..})
10856 if &err == "expose declaration has multiple capability types defined: [\"service\", \"protocol\"]"
10857 ),
10858 }
10859}