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