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