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