routing/bedrock/
dict_ext.rs

1// Copyright 2024 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::bedrock::request_metadata::METADATA_KEY_TYPE;
6use crate::error::RoutingError;
7use async_trait::async_trait;
8use cm_rust::CapabilityTypeName;
9use cm_types::{IterablePath, Name, RelativePath};
10use fidl_fuchsia_component_sandbox as fsandbox;
11use moniker::ExtendedMoniker;
12use router_error::RouterError;
13use sandbox::{
14    Capability, CapabilityBound, Connector, Data, Dict, DirEntry, Request, Routable, Router,
15    RouterResponse,
16};
17use std::fmt::Debug;
18
19#[async_trait]
20pub trait DictExt {
21    /// Returns the capability at the path, if it exists. Returns `None` if path is empty.
22    fn get_capability(&self, path: &impl IterablePath) -> Option<Capability>;
23
24    /// Looks up a top-level router in this [Dict] with return type `T`. If it's not found (or it's
25    /// not a router) returns a router that always returns `not_found_error`. If `path` has one
26    /// segment and a router was found, returns that router.
27    ///
28    /// If `path` is a multi-segment path, the returned router performs a [Dict] lookup with the
29    /// remaining path relative to the top-level router (see [LazyGet::lazy_get]).
30    ///
31    /// REQUIRES: `path` is not empty.
32    fn get_router_or_not_found<T>(
33        &self,
34        path: &impl IterablePath,
35        not_found_error: RoutingError,
36    ) -> Router<T>
37    where
38        T: CapabilityBound,
39        Router<T>: TryFrom<Capability>;
40
41    /// Inserts the capability at the path. Intermediary dictionaries are created as needed.
42    fn insert_capability(
43        &self,
44        path: &impl IterablePath,
45        capability: Capability,
46    ) -> Result<(), fsandbox::CapabilityStoreError>;
47
48    /// Removes the capability at the path, if it exists.
49    fn remove_capability(&self, path: &impl IterablePath);
50
51    /// Looks up the element at `path`. When encountering an intermediate router, use `request` to
52    /// request the underlying capability from it. In contrast, `get_capability` will return
53    /// `None`.
54    ///
55    /// Note that the return value can contain any capability type, instead of a parameterized `T`.
56    /// This is because some callers work with a generic capability and don't care about the
57    /// specific type. Callers who do care can use `TryFrom` to cast to the expected
58    /// [RouterResponse] type.
59    async fn get_with_request<'a>(
60        &self,
61        moniker: &ExtendedMoniker,
62        path: &'a impl IterablePath,
63        request: Option<Request>,
64        debug: bool,
65    ) -> Result<Option<GenericRouterResponse>, RouterError>;
66}
67
68/// The analogue of a [RouterResponse] that can hold any type of capability. This is the
69/// return type of [DictExt::get_with_request].
70#[derive(Debug)]
71pub enum GenericRouterResponse {
72    /// Routing succeeded and returned this capability.
73    Capability(Capability),
74
75    /// Routing succeeded, but the capability was marked unavailable.
76    Unavailable,
77
78    /// Routing succeeded in debug mode, `Data` contains the debug data.
79    Debug(Data),
80}
81
82impl<T: CapabilityBound> TryFrom<GenericRouterResponse> for RouterResponse<T> {
83    // Returns the capability's debug typename.
84    type Error = &'static str;
85
86    fn try_from(r: GenericRouterResponse) -> Result<Self, Self::Error> {
87        let r = match r {
88            GenericRouterResponse::Capability(c) => {
89                let debug_name = c.debug_typename();
90                RouterResponse::<T>::Capability(c.try_into().map_err(|_| debug_name)?)
91            }
92            GenericRouterResponse::Unavailable => RouterResponse::<T>::Unavailable,
93            GenericRouterResponse::Debug(d) => RouterResponse::<T>::Debug(d),
94        };
95        Ok(r)
96    }
97}
98
99#[async_trait]
100impl DictExt for Dict {
101    fn get_capability(&self, path: &impl IterablePath) -> Option<Capability> {
102        let mut segments = path.iter_segments();
103        let Some(mut current_name) = segments.next() else { return Some(self.clone().into()) };
104        let mut current_dict = self.clone();
105        loop {
106            match segments.next() {
107                Some(next_name) => {
108                    let sub_dict = current_dict
109                        .get(current_name)
110                        .ok()
111                        .flatten()
112                        .and_then(|value| value.to_dictionary())?;
113                    current_dict = sub_dict;
114
115                    current_name = next_name;
116                }
117                None => return current_dict.get(current_name).ok().flatten(),
118            }
119        }
120    }
121
122    fn get_router_or_not_found<T>(
123        &self,
124        path: &impl IterablePath,
125        not_found_error: RoutingError,
126    ) -> Router<T>
127    where
128        T: CapabilityBound,
129        Router<T>: TryFrom<Capability>,
130    {
131        let mut segments = path.iter_segments();
132        let root = segments.next().expect("path must be nonempty");
133
134        #[derive(Debug)]
135        struct ErrorRouter {
136            not_found_error: RouterError,
137        }
138
139        #[async_trait]
140        impl<T: CapabilityBound> Routable<T> for ErrorRouter {
141            async fn route(
142                &self,
143                _request: Option<Request>,
144                _debug: bool,
145            ) -> Result<RouterResponse<T>, RouterError> {
146                Err(self.not_found_error.clone())
147            }
148        }
149
150        /// This uses the same algorithm as [LazyGet], but that is implemented for
151        /// [Router<Dict>] while this is implemented for [Router]. This duplication will go
152        /// away once [Router] is replaced with [Router].
153        #[derive(Debug)]
154        struct ScopedDictRouter<P: IterablePath + Debug + 'static> {
155            router: Router<Dict>,
156            path: P,
157            not_found_error: RoutingError,
158        }
159
160        #[async_trait]
161        impl<P: IterablePath + Debug + 'static, T: CapabilityBound> Routable<T> for ScopedDictRouter<P> {
162            async fn route(
163                &self,
164                request: Option<Request>,
165                debug: bool,
166            ) -> Result<RouterResponse<T>, RouterError> {
167                // If `debug` is true, that should only apply to the capability at `path`.
168                // Here we're looking up the containing dictionary, so set `debug = false`, to
169                // obtain the actual Dict and not its debug info. For the same reason, we need
170                // to set the capability type on the first request to Dictionary.
171                let init_request = request_with_dictionary_replacement(request.as_ref())?;
172                match self.router.route(init_request, false).await? {
173                    RouterResponse::<Dict>::Capability(dict) => {
174                        let moniker: ExtendedMoniker = self.not_found_error.clone().into();
175                        let resp =
176                            dict.get_with_request(&moniker, &self.path, request, debug).await?;
177                        let resp =
178                            resp.ok_or_else(|| RouterError::from(self.not_found_error.clone()))?;
179                        let resp = resp.try_into().map_err(|debug_name: &'static str| {
180                            RoutingError::BedrockWrongCapabilityType {
181                                expected: T::debug_typename().into(),
182                                actual: debug_name.into(),
183                                moniker,
184                            }
185                        })?;
186                        Ok(resp)
187                    }
188                    _ => Err(RoutingError::BedrockMemberAccessUnsupported {
189                        moniker: self.not_found_error.clone().into(),
190                    }
191                    .into()),
192                }
193            }
194        }
195
196        if segments.next().is_none() {
197            // No nested lookup necessary.
198            let Some(router) =
199                self.get(root).ok().flatten().and_then(|cap| Router::<T>::try_from(cap).ok())
200            else {
201                return Router::<T>::new(ErrorRouter { not_found_error: not_found_error.into() });
202            };
203            return router;
204        }
205
206        let Some(cap) = self.get(root).ok().flatten() else {
207            return Router::<T>::new(ErrorRouter { not_found_error: not_found_error.into() });
208        };
209        let router = match cap {
210            Capability::Dictionary(d) => Router::<Dict>::new_ok(d),
211            Capability::DictionaryRouter(r) => r,
212            _ => {
213                return Router::<T>::new(ErrorRouter { not_found_error: not_found_error.into() });
214            }
215        };
216
217        let mut segments = path.iter_segments();
218        let _ = segments.next().unwrap();
219        let path = RelativePath::from(segments.collect::<Vec<_>>());
220
221        Router::<T>::new(ScopedDictRouter { router, path, not_found_error: not_found_error.into() })
222    }
223
224    fn insert_capability(
225        &self,
226        path: &impl IterablePath,
227        capability: Capability,
228    ) -> Result<(), fsandbox::CapabilityStoreError> {
229        let mut segments = path.iter_segments();
230        let mut current_name = segments.next().expect("path must be non-empty");
231        let mut current_dict = self.clone();
232        loop {
233            match segments.next() {
234                Some(next_name) => {
235                    let sub_dict = {
236                        match current_dict.get(current_name) {
237                            Ok(Some(cap)) => cap
238                                .to_dictionary()
239                                .ok_or(fsandbox::CapabilityStoreError::ItemNotFound)?,
240                            Ok(None) => {
241                                let dict = Dict::new();
242                                current_dict.insert(
243                                    current_name.into(),
244                                    Capability::Dictionary(dict.clone()),
245                                )?;
246                                dict
247                            }
248                            Err(_) => return Err(fsandbox::CapabilityStoreError::ItemNotFound),
249                        }
250                    };
251                    current_dict = sub_dict;
252
253                    current_name = next_name;
254                }
255                None => {
256                    return current_dict.insert(current_name.into(), capability);
257                }
258            }
259        }
260    }
261
262    fn remove_capability(&self, path: &impl IterablePath) {
263        let mut segments = path.iter_segments();
264        let mut current_name = segments.next().expect("path must be non-empty");
265        let mut current_dict = self.clone();
266        loop {
267            match segments.next() {
268                Some(next_name) => {
269                    let sub_dict = current_dict
270                        .get(current_name)
271                        .ok()
272                        .flatten()
273                        .and_then(|value| value.to_dictionary());
274                    if sub_dict.is_none() {
275                        // The capability doesn't exist, there's nothing to remove.
276                        return;
277                    }
278                    current_dict = sub_dict.unwrap();
279                    current_name = next_name;
280                }
281                None => {
282                    current_dict.remove(current_name);
283                    return;
284                }
285            }
286        }
287    }
288
289    async fn get_with_request<'a>(
290        &self,
291        moniker: &ExtendedMoniker,
292        path: &'a impl IterablePath,
293        request: Option<Request>,
294        debug: bool,
295    ) -> Result<Option<GenericRouterResponse>, RouterError> {
296        let mut current_dict = self.clone();
297        let num_segments = path.iter_segments().count();
298        for (next_idx, next_name) in path.iter_segments().enumerate() {
299            // Get the capability.
300            let capability = current_dict
301                .get(next_name)
302                .map_err(|_| RoutingError::BedrockNotCloneable { moniker: moniker.clone() })?;
303
304            // The capability doesn't exist.
305            let Some(capability) = capability else {
306                return Ok(None);
307            };
308
309            // Resolve the capability, this is a noop if it's not a router.
310            let debug = if next_idx < num_segments - 1 {
311                // If `request.debug` is true, that should only apply to the capability at `path`.
312                // Since we're not looking up the final path segment, set `debug = false`, to
313                // obtain the actual Dict and not its debug info.
314                false
315            } else {
316                debug
317            };
318
319            if next_idx < num_segments - 1 {
320                // Not at the end of the path yet, so there's more nesting. We expect to
321                // have found a [Dict], or a [Dict] router -- traverse into this [Dict].
322                let request = request_with_dictionary_replacement(request.as_ref())?;
323                match capability {
324                    Capability::Dictionary(d) => {
325                        current_dict = d;
326                    }
327                    Capability::DictionaryRouter(r) => match r.route(request, false).await? {
328                        RouterResponse::<Dict>::Capability(d) => {
329                            current_dict = d;
330                        }
331                        RouterResponse::<Dict>::Unavailable => {
332                            return Ok(Some(GenericRouterResponse::Unavailable));
333                        }
334                        RouterResponse::<Dict>::Debug(d) => {
335                            // This shouldn't happen (we passed debug=false). Just pass it up
336                            // the chain so the caller can decide how to deal with it.
337                            return Ok(Some(GenericRouterResponse::Debug(d)));
338                        }
339                    },
340                    _ => {
341                        return Err(RoutingError::BedrockWrongCapabilityType {
342                            expected: Dict::debug_typename().into(),
343                            actual: capability.debug_typename().into(),
344                            moniker: moniker.clone(),
345                        }
346                        .into());
347                    }
348                }
349            } else {
350                // We've reached the end of our path. The last capability should have type
351                // `T` or `Router<T>`.
352                //
353                // There's a bit of repetition here because this function supports multiple router
354                // types.
355                let request = request.as_ref().map(|r| r.try_clone()).transpose()?;
356                let capability: Capability = match capability {
357                    Capability::DictionaryRouter(r) => match r.route(request, debug).await? {
358                        RouterResponse::<Dict>::Capability(c) => c.into(),
359                        RouterResponse::<Dict>::Unavailable => {
360                            return Ok(Some(GenericRouterResponse::Unavailable));
361                        }
362                        RouterResponse::<Dict>::Debug(d) => {
363                            return Ok(Some(GenericRouterResponse::Debug(d)));
364                        }
365                    },
366                    Capability::ConnectorRouter(r) => match r.route(request, debug).await? {
367                        RouterResponse::<Connector>::Capability(c) => c.into(),
368                        RouterResponse::<Connector>::Unavailable => {
369                            return Ok(Some(GenericRouterResponse::Unavailable));
370                        }
371                        RouterResponse::<Connector>::Debug(d) => {
372                            return Ok(Some(GenericRouterResponse::Debug(d)));
373                        }
374                    },
375                    Capability::DataRouter(r) => match r.route(request, debug).await? {
376                        RouterResponse::<Data>::Capability(c) => c.into(),
377                        RouterResponse::<Data>::Unavailable => {
378                            return Ok(Some(GenericRouterResponse::Unavailable));
379                        }
380                        RouterResponse::<Data>::Debug(d) => {
381                            return Ok(Some(GenericRouterResponse::Debug(d)));
382                        }
383                    },
384                    Capability::DirEntryRouter(r) => match r.route(request, debug).await? {
385                        RouterResponse::<DirEntry>::Capability(c) => c.into(),
386                        RouterResponse::<DirEntry>::Unavailable => {
387                            return Ok(Some(GenericRouterResponse::Unavailable));
388                        }
389                        RouterResponse::<DirEntry>::Debug(d) => {
390                            return Ok(Some(GenericRouterResponse::Debug(d)));
391                        }
392                    },
393                    other => other,
394                };
395                return Ok(Some(GenericRouterResponse::Capability(capability)));
396            }
397        }
398        unreachable!("get_with_request: All cases are handled in the loop");
399    }
400}
401
402/// Creates a clone of `request` that is identical except `"type"` is set to `dictionary`
403/// if it is not already. If `request` is `None`, `None` will be returned.
404///
405/// This is convenient for router lookups of nested paths, since all lookups except the last
406/// segment are dictionary lookups.
407pub(super) fn request_with_dictionary_replacement(
408    request: Option<&Request>,
409) -> Result<Option<Request>, RoutingError> {
410    Ok(request.as_ref().map(|r| r.try_clone()).transpose()?.map(|r| {
411        let _ = r.metadata.insert(
412            Name::new(METADATA_KEY_TYPE).unwrap(),
413            Capability::Data(Data::String(CapabilityTypeName::Dictionary.to_string())),
414        );
415        r
416    }))
417}