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