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