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