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