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