cml/
validate.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
94/// Validates a given cml.
95pub(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/// Validates a given cml with error span.
112#[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        // Disallow multiple capability ids of the same name.
413        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        // Disallow multiple capability ids of the same name.
632        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                    // Dictionaries may have conflicting use paths, as they'll be merged together
642                    // at runtime.
643                } 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            // Capability paths must not conflict with `/pkg`, or namespace generation might fail
659            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            // Validate that paths-based capabilities (service, directory, protocol)
676            // are not prefixes of each other.
677            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                    // This variant is never constructed if we're at an API version before "use
692                    // dictionary" was added.
693                    #[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                        // All combinations of two dictionaries are valid.
741                    }
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            // All directory "use" expressions must have directory rights.
762            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        // Ensure directory rights are valid.
793        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            // Exposing a subdirectory makes sense for routing but when exposing to framework,
805            // the subdir should be exposed directly.
806            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        // Ensure we haven't already exposed an entity of the same name.
888        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                    // Services may have duplicates (aggregation).
901                } 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        // validate duplicate offer to all
932        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        // Ensure directory rights are valid.
1026        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        // Ensure that dependency is set for the right capabilities.
1087        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    /// Validates that directory rights for all route types are valid, i.e that it does not
1110    /// contain duplicate rights.
1111    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
1195// Facet key for fuchsia.test
1196const TEST_FACET_KEY: &'static str = "fuchsia.test";
1197
1198// Facet key for deprecated-allowed-packages.
1199const TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: &'static str = "deprecated-allowed-packages";
1200
1201// Facet key for type.
1202const 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        // Ensure child components, collections, and storage don't use the
1231        // same name.
1232        //
1233        // We currently have the ability to distinguish between storage and
1234        // children/collections based on context, but still enforce name
1235        // uniqueness to give us flexibility in future.
1236        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        // Populate the sets of children and collections.
1261        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        // Validate "children".
1277        if let Some(children) = &self.document.children {
1278            for child in children {
1279                self.validate_child(&child)?;
1280            }
1281        }
1282
1283        // Validate "collections".
1284        if let Some(collections) = &self.document.collections {
1285            for collection in collections {
1286                self.validate_collection(&collection)?;
1287            }
1288        }
1289
1290        // Validate "capabilities".
1291        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        // Validate "use".
1299        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        // Validate "expose".
1311        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        // Validate "offer".
1320        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            // Component "use"s a runner. Ensure we don't also have a runner specified in "program",
1378            // which would necessarily conflict.
1379            self.validate_runner_not_specified(self.document.program.as_ref())?;
1380        } else {
1381            // Component doesn't "use" a runner. Ensure we don't have a component with a "program"
1382            // block which fails to specify a runner.
1383            self.validate_runner_specified(self.document.program.as_ref())?;
1384        }
1385
1386        // Validate "environments".
1387        if let Some(environments) = &self.document.environments {
1388            for env in environments {
1389                self.validate_environment(&env)?;
1390            }
1391        }
1392
1393        // Validate "config"
1394        self.validate_config(&self.document.config)?;
1395
1396        // Check that required offers are present
1397        self.validate_required_offer_decls()?;
1398
1399        // Check that required use decls are present
1400        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        // Disallow multiple capability ids of the same name.
1538        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        // Disallow multiple capability ids of the same name.
1635        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                    // Dictionaries may have conflicting use paths, as they'll be merged together
1644                    // at runtime.
1645                } 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            // Capability paths must not conflict with `/pkg`, or namespace generation might fail
1656            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            // Validate that paths-based capabilities (service, directory, protocol)
1668            // are not prefixes of each other.
1669            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                    // This variant is never constructed if we're at an API version before "use
1684                    // dictionary" was added.
1685                    #[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                        // All combinations of two dictionaries are valid.
1733                    }
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            // All directory "use" expressions must have directory rights.
1749            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        // Ensure directory rights are valid.
1802        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            // Exposing a subdirectory makes sense for routing but when exposing to framework,
1810            // the subdir should be exposed directly.
1811            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        // Ensure we haven't already exposed an entity of the same name.
1871        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                    // Services may have duplicates (aggregation).
1880                } 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        // Validate `from` (done last because this validation depends on the capability type, which
1891        // must be validated first)
1892        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        // Ensure directory rights are valid.
1939        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        // Ensure that dependency is set for the right capabilities.
1980        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        // Validate every target of this offer.
1987        let target_cap_ids = CapabilityId::from_offer_expose(offer)?;
1988        for to in &offer.to {
1989            // Ensure the "to" value is a child, collection, or dictionary capability.
1990            let to_target = match to {
1991                OfferToRef::All => continue,
1992                OfferToRef::Named(to_target) => {
1993                    // Verify that only a legal set of offers-to-all are made, including that any
1994                    // offer to all duplicated as an offer to a specific component are exactly the same
1995                    for offer_to_all in protocols_offered_to_all {
1996                        offer_to_all_would_duplicate(offer_to_all, offer, to_target)?;
1997                    }
1998
1999                    // Check that any referenced child actually exists.
2000                    // Skip the check if target availability is unknown.
2001                    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                        // Allowed.
2006                    } 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                    // Ensure we are not offering a capability back to its source.
2021                    if let Some(storage) = offer.storage.as_ref() {
2022                        for storage in storage {
2023                            // Storage can only have a single `from` clause and this has been
2024                            // verified.
2025                            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                            // Weak offers from a child to itself are acceptable.
2041                            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                    // Check that any referenced child actually exists.
2089                    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            // Ensure that a target is not offered more than once.
2113            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                        // Services may have duplicates (aggregation).
2119                    } 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        // Validate `from` (done last because this validation depends on the capability type, which
2130        // must be validated first)
2131        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 each child, check if any offer is:
2146            //   1) Targeting this child (or all)
2147            //   AND
2148            //   2) Offering the current required capability
2149            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    /// Validates that the from clause:
2230    ///
2231    /// - is applicable to the capability type,
2232    /// - does not contain duplicates,
2233    /// - references names that exist.
2234    /// - has availability "optional" if the source is "void"
2235    ///
2236    /// `verb` is used in any error messages and is expected to be "offer", "expose", etc.
2237    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            // If this is a protocol, it could reference either a child or a storage capability
2262            // (for the storage admin protocol).
2263            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                // Services can also be sourced from collections.
2270                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                    // The source is valid and void
2281                    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                    // The source is valid and not void.
2291                }
2292                Err(_) if source_availability == &Some(SourceAvailability::Unknown) => {
2293                    // The source is invalid, and will be rewritten to void
2294                    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                    // The source is invalid, but we're expecting it to be valid.
2304                    return Err(e);
2305                }
2306            }
2307        }
2308        Ok(())
2309    }
2310
2311    /// Validates that the given component exists.
2312    ///
2313    /// - `reference_description` is a human-readable description of the reference used in error
2314    ///   message, such as `"offer" source`.
2315    /// - `component_ref` is a reference to a component. If the reference is a named child, we
2316    ///   ensure that the child component exists.
2317    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                // Ensure we have a child defined by that name.
2325                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            // We don't attempt to validate other reference types.
2334            _ => Ok(()),
2335        }
2336    }
2337
2338    /// Validates that the given component/collection exists.
2339    ///
2340    /// - `reference_description` is a human-readable description of the reference used in error
2341    ///   message, such as `"offer" source`.
2342    /// - `component_ref` is a reference to a component/collection. If the reference is a named
2343    ///   child or collection, we ensure that the child component/collection exists.
2344    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                // Ensure we have a child or collection defined by that name.
2352                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            // We don't attempt to validate other reference types.
2361            _ => Ok(()),
2362        }
2363    }
2364
2365    /// Validates that the given capability exists.
2366    ///
2367    /// - `reference_description` is a human-readable description of the reference used in error
2368    ///   message, such as `"offer" source`.
2369    /// - `capability_ref` is a reference to a capability. If the reference is a named capability,
2370    ///   we ensure that the capability exists.
2371    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    /// Validates that the given child component, collection, or capability exists.
2391    ///
2392    /// - `reference_description` is a human-readable description of the reference used in error
2393    ///   message, such as `"offer" source`.
2394    /// - `ref_` is a reference to a child component or capability. If the reference contains a
2395    ///   name, we ensure that a child component or a capability with the name exists.
2396    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    /// Validates that directory rights for all route types are valid, i.e that it does not
2413    /// contain duplicate rights.
2414    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    /// Ensure we don't have a component with a "program" block which fails to specify a runner.
2430    /// This should only be called if the manifest doesn't "use" a runner.
2431    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    /// Ensure we don't have a component with a "program" block which fails to specify a runner.
2447    /// This should only be called if the manifest "use"s a runner.
2448    fn validate_runner_not_specified(&self, program: Option<&Program>) -> Result<(), Error> {
2449        match program {
2450            Some(program) => match program.runner {
2451                Some(_) => {
2452                    // Use/runner always conflicts with program/runner, because use/runner
2453                    // can't be from environment in CML.
2454                    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        // If we `use` a config capability optionally without a default then it has to exist in the `config` block.
2469        // Collect the names of the keys here.
2470        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                // Validate that this name is not already used.
2540                let name = registration.r#as.as_ref().unwrap_or(&registration.runner);
2541                if let Some(previous_runner) = used_names.insert(name, &registration.runner) {
2542                    return Err(Error::validate(format!(
2543                        "Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".",
2544                        name, &registration.runner, previous_runner
2545                    )));
2546                }
2547
2548                // Ensure that the environment is defined in `runners` if it comes from `self`.
2549                if registration.from == RegistrationRef::Self_
2550                    && !self.all_runners.contains(&registration.runner.as_ref())
2551                {
2552                    return Err(Error::validate(format!(
2553                        "Runner \"{}\" registered in environment is not in \"runners\"",
2554                        &registration.runner,
2555                    )));
2556                }
2557
2558                self.validate_component_child_ref(
2559                    &format!("\"{}\" runner source", &registration.runner),
2560                    &AnyRef::from(&registration.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                // Validate that the scheme is not already used.
2569                if let Some(previous_resolver) =
2570                    used_schemes.insert(&registration.scheme, &registration.resolver)
2571                {
2572                    return Err(Error::validate(format!(
2573                        "scheme \"{}\" for resolver \"{}\" is already registered; \
2574                        previously registered to resolver \"{}\".",
2575                        &registration.scheme, &registration.resolver, previous_resolver
2576                    )));
2577                }
2578
2579                self.validate_component_child_ref(
2580                    &format!("\"{}\" resolver source", &registration.resolver),
2581                    &AnyRef::from(&registration.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
2700/// Helper type that assists with validating declarations of `{use, offer, expose} from self`.
2701struct RouteFromSelfChecker<'a> {
2702    /// The value of the capability property (protocol, service, etc.)
2703    capability_name: Option<OneOrMany<&'a BorrowedName>>,
2704
2705    /// The value of `from`.
2706    from: OneOrMany<AnyRef<'a>>,
2707
2708    /// A [Container] which is used to check for the existence of a capability definition.
2709    container: &'a dyn Container,
2710
2711    /// Reference to [ValidationContext::all_dictionaries].
2712    all_dictionaries: &'a HashMap<&'a BorrowedName, &'a Capability>,
2713
2714    /// The string name for the capability's type.
2715    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
2750/// [Container] provides a capability type agnostic trait to check for the existence of a
2751/// capability definition of a particular type. This is useful for writing common validation
2752/// functions.
2753trait 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
2769// Construct the config type information out of a `use` for a configuration capability.
2770// This will return validation errors if the `use` is missing fields.
2771pub 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
2820// Construct the config type information out of a `use` for a configuration capability.
2821// This will return validation errors if the `use` is missing fields.
2822pub 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
2883/// Given an iterator with `(key, name)` tuples, ensure that `key` doesn't
2884/// appear twice. `name` is used in generated error messages.
2885fn 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
2901/// Returns an error if the iterator contains duplicate values.
2902fn 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        // exact duplication is allowed
3364        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        // aliased duplications are forbidden
3402        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        // offering the same protocol without an alias from different sources is forbidden
3447        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        // exact duplication is allowed
3495        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        // aliased duplications are forbidden
3533        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        // offering the same dictionary without an alias from different sources is forbidden
3578        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        // include
4681        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        // program
4707        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        // use
4758        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        // expose
5309        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        // if "as" is specified, only 1 array item is allowed.
5446        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        // offer
5720        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        // if "as" is specified, only 1 array item is allowed.
6274        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        // children
6535        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        // collections
6798        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        // capabilities
6847        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        // environments
7129        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        // facets
7376        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        // constraints
7396        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        // deny unknown fields
7876        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    // Tests for services.
8123    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    // Tests structured config
8330    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    // Tests the use of `allow_long_names` when the "AllowLongNames" feature is set.
8582    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    // Tests using a dictionary when the UseDictionaries feature is set
8610    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    // Tests that two dictionaries can be used at the same path
8629    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    // Tests that the use of `allow_long_names` fails when the "AllowLongNames"
8648    // feature is not set.
8649    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    // Tests that using a dictionary fails when the "UseDictionaries" feature is not set.
8665    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    // Tests validate_facets function without the feature set
8680    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    // Tests validate_facets function with the RestrictTestTypeInFacet enabled.
8715    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    // Tests validate_facets function with the EnableAllowNonHermeticPackagesFeature disabled.
8742    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    // Tests validate_facets function with the EnableAllowNonHermeticPackagesFeature enabled.
8769    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    // Tests validate_facets function with the feature enabled and allowed pkg feature set.
8796    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    // Tests that offering and exposing service capabilities to the same target and target name is
8936    // allowed.
8937    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    // Tests for config capabilities
9008    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                // Missing max count.
9039            },
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                // Missing max size.
9051            },
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}