Skip to main content

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