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