Skip to main content

cml/
validate.rs

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