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