Skip to main content

vfs/
symlink.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
5//! Server support for symbolic links.
6
7use crate::common::{
8    decode_extended_attribute_value, encode_extended_attribute_value, extended_attributes_sender,
9};
10use crate::execution_scope::ExecutionScope;
11use crate::name::parse_name;
12use crate::node::Node;
13use crate::object_request::{ConnectionCreator, Representation, run_synchronous_future_or_spawn};
14use crate::request_handler::{RequestHandler, RequestListener};
15use crate::{ObjectRequest, ObjectRequestRef, ProtocolsExt, ToObjectRequest};
16use fidl::endpoints::{ControlHandle as _, DiscoverableProtocolMarker as _, Responder, ServerEnd};
17use fidl_fuchsia_io as fio;
18use std::future::Future;
19use std::ops::ControlFlow;
20use std::pin::Pin;
21use std::sync::Arc;
22use storage_trace::{self as trace, TraceFutureExt};
23use zx_status::Status;
24
25pub trait Symlink: Node {
26    fn read_target(&self) -> impl Future<Output = Result<Vec<u8>, Status>> + Send;
27}
28
29pub struct Connection<T> {
30    scope: ExecutionScope,
31    symlink: Arc<T>,
32}
33
34pub struct SymlinkOptions;
35
36impl<T: Symlink> Connection<T> {
37    /// Creates a new connection to serve the symlink. The symlink will be served from a new async
38    /// `Task`, not from the current `Task`. Errors in constructing the connection are not
39    /// guaranteed to be returned, they may be sent directly to the client end of the connection.
40    /// This method should be called from within an `ObjectRequest` handler to ensure that errors
41    /// are sent to the client end of the connection.
42    pub async fn create(
43        scope: ExecutionScope,
44        symlink: Arc<T>,
45        protocols: impl ProtocolsExt,
46        object_request: ObjectRequestRef<'_>,
47    ) -> Result<(), Status> {
48        let _options = protocols.to_symlink_options()?;
49        let connection = Self { scope: scope.clone(), symlink };
50        if let Ok(requests) = object_request.take().into_request_stream(&connection).await {
51            scope.spawn(RequestListener::new(requests, connection));
52        }
53        Ok(())
54    }
55
56    /// Similar to `create` but optimized for symlinks whose implementation is synchronous and
57    /// creating the connection is being done from a non-async context.
58    pub fn create_sync(
59        scope: ExecutionScope,
60        symlink: Arc<T>,
61        options: impl ProtocolsExt,
62        object_request: ObjectRequest,
63    ) {
64        run_synchronous_future_or_spawn(
65            scope.clone(),
66            object_request.handle_async(async |object_request| {
67                Self::create(scope, symlink, options, object_request).await
68            }),
69        )
70    }
71
72    // Returns true if the connection should terminate.
73    async fn handle_request(&mut self, req: fio::SymlinkRequest) -> Result<bool, fidl::Error> {
74        match req {
75            #[cfg(any(
76                fuchsia_api_level_at_least = "PLATFORM",
77                not(fuchsia_api_level_at_least = "29")
78            ))]
79            fio::SymlinkRequest::DeprecatedClone { flags, object, control_handle: _ } => {
80                crate::common::send_on_open_with_error(
81                    flags.contains(fio::OpenFlags::DESCRIBE),
82                    object,
83                    Status::NOT_SUPPORTED,
84                );
85            }
86            fio::SymlinkRequest::Clone { request, control_handle: _ } => {
87                self.handle_clone(ServerEnd::new(request.into_channel()))
88                    .trace(trace::trace_future_args!("storage", "Symlink::Clone"))
89                    .await
90            }
91            fio::SymlinkRequest::Close { responder } => {
92                trace::duration!("storage", "Symlink::Close");
93                responder.send(Ok(()))?;
94                return Ok(true);
95            }
96            fio::SymlinkRequest::LinkInto { dst_parent_token, dst, responder } => {
97                async move {
98                    responder.send(
99                        self.handle_link_into(dst_parent_token, dst)
100                            .await
101                            .map_err(|s| s.into_raw()),
102                    )
103                }
104                .trace(trace::trace_future_args!("storage", "Symlink::LinkInto"))
105                .await?;
106            }
107            fio::SymlinkRequest::Sync { responder } => {
108                trace::duration!("storage", "Symlink::Sync");
109                responder.send(Ok(()))?;
110            }
111            #[cfg(fuchsia_api_level_at_least = "28")]
112            fio::SymlinkRequest::DeprecatedGetAttr { responder } => {
113                // TODO(https://fxbug.dev/293947862): Restrict GET_ATTRIBUTES.
114                let (status, attrs) = crate::common::io2_to_io1_attrs(
115                    self.symlink.as_ref(),
116                    fio::Rights::GET_ATTRIBUTES,
117                )
118                .await;
119                responder.send(status.into_raw(), &attrs)?;
120            }
121            #[cfg(not(fuchsia_api_level_at_least = "28"))]
122            fio::SymlinkRequest::GetAttr { responder } => {
123                // TODO(https://fxbug.dev/293947862): Restrict GET_ATTRIBUTES.
124                let (status, attrs) = crate::common::io2_to_io1_attrs(
125                    self.symlink.as_ref(),
126                    fio::Rights::GET_ATTRIBUTES,
127                )
128                .await;
129                responder.send(status.into_raw(), &attrs)?;
130            }
131            #[cfg(fuchsia_api_level_at_least = "28")]
132            fio::SymlinkRequest::DeprecatedSetAttr { responder, .. } => {
133                responder.send(Status::ACCESS_DENIED.into_raw())?;
134            }
135            #[cfg(not(fuchsia_api_level_at_least = "28"))]
136            fio::SymlinkRequest::SetAttr { responder, .. } => {
137                responder.send(Status::ACCESS_DENIED.into_raw())?;
138            }
139            fio::SymlinkRequest::GetAttributes { query, responder } => {
140                async move {
141                    // TODO(https://fxbug.dev/293947862): Restrict GET_ATTRIBUTES.
142                    let attrs = self.symlink.get_attributes(query).await;
143                    responder.send(
144                        attrs
145                            .as_ref()
146                            .map(|attrs| (&attrs.mutable_attributes, &attrs.immutable_attributes))
147                            .map_err(|status| status.into_raw()),
148                    )
149                }
150                .trace(trace::trace_future_args!("storage", "Symlink::GetAttributes"))
151                .await?;
152            }
153            fio::SymlinkRequest::UpdateAttributes { payload: _, responder } => {
154                trace::duration!("storage", "Symlink::UpdateAttributes");
155                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
156            }
157            fio::SymlinkRequest::ListExtendedAttributes { iterator, control_handle: _ } => {
158                self.handle_list_extended_attribute(iterator)
159                    .trace(trace::trace_future_args!("storage", "Symlink::ListExtendedAttributes"))
160                    .await;
161            }
162            fio::SymlinkRequest::GetExtendedAttribute { responder, name } => {
163                async move {
164                    let res = self.handle_get_extended_attribute(name).await;
165                    responder.send(res.map_err(Status::into_raw))
166                }
167                .trace(trace::trace_future_args!("storage", "Symlink::GetExtendedAttribute"))
168                .await?;
169            }
170            fio::SymlinkRequest::SetExtendedAttribute { responder, name, value, mode } => {
171                async move {
172                    let res = self.handle_set_extended_attribute(name, value, mode).await;
173                    responder.send(res.map_err(Status::into_raw))
174                }
175                .trace(trace::trace_future_args!("storage", "Symlink::SetExtendedAttribute"))
176                .await?;
177            }
178            fio::SymlinkRequest::RemoveExtendedAttribute { responder, name } => {
179                async move {
180                    let res = self.handle_remove_extended_attribute(name).await;
181                    responder.send(res.map_err(Status::into_raw))
182                }
183                .trace(trace::trace_future_args!("storage", "Symlink::RemoveExtendedAttribute"))
184                .await?;
185            }
186            fio::SymlinkRequest::Describe { responder } => {
187                return async move {
188                    match self.symlink.read_target().await {
189                        Ok(target) => {
190                            responder.send(&fio::SymlinkInfo {
191                                target: Some(target),
192                                ..Default::default()
193                            })?;
194                            Ok(false)
195                        }
196                        Err(status) => {
197                            responder.control_handle().shutdown_with_epitaph(status);
198                            Ok(true)
199                        }
200                    }
201                }
202                .trace(trace::trace_future_args!("storage", "Symlink::Describe"))
203                .await;
204            }
205            fio::SymlinkRequest::GetFlags { responder } => {
206                trace::duration!("storage", "Symlink::GetFlags");
207                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
208            }
209            fio::SymlinkRequest::SetFlags { flags: _, responder } => {
210                trace::duration!("storage", "Symlink::SetFlags");
211                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
212            }
213            fio::SymlinkRequest::DeprecatedGetFlags { responder } => {
214                responder.send(Status::NOT_SUPPORTED.into_raw(), fio::OpenFlags::empty())?;
215            }
216            fio::SymlinkRequest::DeprecatedSetFlags { responder, .. } => {
217                responder.send(Status::ACCESS_DENIED.into_raw())?;
218            }
219            fio::SymlinkRequest::Query { responder } => {
220                trace::duration!("storage", "Symlink::Query");
221                responder.send(fio::SymlinkMarker::PROTOCOL_NAME.as_bytes())?;
222            }
223            fio::SymlinkRequest::QueryFilesystem { responder } => {
224                trace::duration!("storage", "Symlink::QueryFilesystem");
225                match self.symlink.query_filesystem() {
226                    Err(status) => responder.send(status.into_raw(), None)?,
227                    Ok(info) => responder.send(0, Some(&info))?,
228                }
229            }
230            #[cfg(fuchsia_api_level_at_least = "HEAD")]
231            fio::SymlinkRequest::Open { object, .. } => {
232                use fidl::epitaph::ChannelEpitaphExt;
233                let _ = object.close_with_epitaph(Status::NOT_DIR);
234            }
235            fio::SymlinkRequest::_UnknownMethod { ordinal: _ordinal, .. } => {
236                #[cfg(any(test, feature = "use_log"))]
237                log::warn!(_ordinal; "Received unknown method")
238            }
239        }
240        Ok(false)
241    }
242
243    async fn handle_clone(&mut self, server_end: ServerEnd<fio::SymlinkMarker>) {
244        let flags = fio::Flags::PROTOCOL_SYMLINK | fio::Flags::PERM_GET_ATTRIBUTES;
245        flags
246            .to_object_request(server_end)
247            .handle_async(async |object_request| {
248                Self::create(self.scope.clone(), self.symlink.clone(), flags, object_request).await
249            })
250            .await;
251    }
252
253    async fn handle_link_into(
254        &mut self,
255        target_parent_token: fidl::Event,
256        target_name: String,
257    ) -> Result<(), Status> {
258        let target_name = parse_name(target_name).map_err(|_| Status::INVALID_ARGS)?;
259
260        let (target_parent, target_rights) = self
261            .scope
262            .token_registry()
263            .get_owner_and_rights(target_parent_token.into())?
264            .ok_or(Err(Status::NOT_FOUND))?;
265
266        if !target_rights.contains(fio::Rights::MODIFY_DIRECTORY) {
267            return Err(Status::ACCESS_DENIED);
268        }
269
270        self.symlink.clone().link_into(target_parent, target_name).await
271    }
272
273    async fn handle_list_extended_attribute(
274        &self,
275        iterator: ServerEnd<fio::ExtendedAttributeIteratorMarker>,
276    ) {
277        let attributes = match self.symlink.list_extended_attributes().await {
278            Ok(attributes) => attributes,
279            Err(status) => {
280                #[cfg(any(test, feature = "use_log"))]
281                log::error!(status:?; "list extended attributes failed");
282                #[allow(clippy::unnecessary_lazy_evaluations)]
283                iterator.close_with_epitaph(status).unwrap_or_else(|_error| {
284                    #[cfg(any(test, feature = "use_log"))]
285                    log::error!(_error:?; "failed to send epitaph")
286                });
287                return;
288            }
289        };
290        self.scope.spawn(extended_attributes_sender(iterator, attributes));
291    }
292
293    async fn handle_get_extended_attribute(
294        &self,
295        name: Vec<u8>,
296    ) -> Result<fio::ExtendedAttributeValue, Status> {
297        let value = self.symlink.get_extended_attribute(name).await?;
298        encode_extended_attribute_value(value)
299    }
300
301    async fn handle_set_extended_attribute(
302        &self,
303        name: Vec<u8>,
304        value: fio::ExtendedAttributeValue,
305        mode: fio::SetExtendedAttributeMode,
306    ) -> Result<(), Status> {
307        if name.contains(&0) {
308            return Err(Status::INVALID_ARGS);
309        }
310        let val = decode_extended_attribute_value(value)?;
311        self.symlink.set_extended_attribute(name, val, mode).await
312    }
313
314    async fn handle_remove_extended_attribute(&self, name: Vec<u8>) -> Result<(), Status> {
315        self.symlink.remove_extended_attribute(name).await
316    }
317}
318
319impl<T: Symlink> RequestHandler for Connection<T> {
320    type Request = Result<fio::SymlinkRequest, fidl::Error>;
321
322    async fn handle_request(self: Pin<&mut Self>, request: Self::Request) -> ControlFlow<()> {
323        let this = self.get_mut();
324        if let Some(_guard) = this.scope.try_active_guard() {
325            match request {
326                Ok(request) => match this.handle_request(request).await {
327                    Ok(false) => ControlFlow::Continue(()),
328                    Ok(true) | Err(_) => ControlFlow::Break(()),
329                },
330                Err(_) => ControlFlow::Break(()),
331            }
332        } else {
333            ControlFlow::Break(())
334        }
335    }
336}
337
338impl<T: Symlink> Representation for Connection<T> {
339    type Protocol = fio::SymlinkMarker;
340
341    async fn get_representation(
342        &self,
343        requested_attributes: fio::NodeAttributesQuery,
344    ) -> Result<fio::Representation, Status> {
345        Ok(fio::Representation::Symlink(fio::SymlinkInfo {
346            attributes: if requested_attributes.is_empty() {
347                None
348            } else {
349                Some(self.symlink.get_attributes(requested_attributes).await?)
350            },
351            target: Some(self.symlink.read_target().await?),
352            ..Default::default()
353        }))
354    }
355
356    #[cfg(any(fuchsia_api_level_at_least = "PLATFORM", not(fuchsia_api_level_at_least = "NEXT")))]
357    async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
358        Ok(fio::NodeInfoDeprecated::Symlink(fio::SymlinkObject {
359            target: self.symlink.read_target().await?,
360        }))
361    }
362}
363
364impl<T: Symlink> ConnectionCreator<T> for Connection<T> {
365    async fn create<'a>(
366        scope: ExecutionScope,
367        node: Arc<T>,
368        protocols: impl ProtocolsExt,
369        object_request: ObjectRequestRef<'a>,
370    ) -> Result<(), Status> {
371        Self::create(scope, node, protocols, object_request).await
372    }
373}
374
375/// Helper to open a symlink or node as required.
376pub fn serve(
377    link: Arc<impl Symlink>,
378    scope: ExecutionScope,
379    protocols: impl ProtocolsExt,
380    object_request: ObjectRequestRef<'_>,
381) -> Result<(), Status> {
382    if protocols.is_node() {
383        let options = protocols.to_node_options(link.entry_info().type_())?;
384        link.open_as_node(scope, options, object_request)
385    } else {
386        Connection::create_sync(scope, link, protocols, object_request.take());
387        Ok(())
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::{Connection, Symlink};
394    use crate::ToObjectRequest;
395    use crate::directory::entry::{EntryInfo, GetEntryInfo};
396    use crate::execution_scope::ExecutionScope;
397    use crate::node::Node;
398    use assert_matches::assert_matches;
399    use fidl::endpoints::{ServerEnd, create_proxy};
400    use fidl_fuchsia_io as fio;
401    use fuchsia_sync::Mutex;
402    use futures::StreamExt;
403    use std::collections::HashMap;
404    use std::sync::Arc;
405    use zx_status::Status;
406
407    const TARGET: &[u8] = b"target";
408
409    struct TestSymlink {
410        xattrs: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
411    }
412
413    impl TestSymlink {
414        fn new() -> Self {
415            TestSymlink { xattrs: Mutex::new(HashMap::new()) }
416        }
417    }
418
419    impl Symlink for TestSymlink {
420        async fn read_target(&self) -> Result<Vec<u8>, Status> {
421            Ok(TARGET.to_vec())
422        }
423    }
424
425    impl Node for TestSymlink {
426        async fn get_attributes(
427            &self,
428            requested_attributes: fio::NodeAttributesQuery,
429        ) -> Result<fio::NodeAttributes2, Status> {
430            Ok(immutable_attributes!(
431                requested_attributes,
432                Immutable {
433                    content_size: TARGET.len() as u64,
434                    storage_size: TARGET.len() as u64,
435                    protocols: fio::NodeProtocolKinds::SYMLINK,
436                    abilities: fio::Abilities::GET_ATTRIBUTES,
437                }
438            ))
439        }
440        async fn list_extended_attributes(&self) -> Result<Vec<Vec<u8>>, Status> {
441            let map = self.xattrs.lock();
442            Ok(map.values().map(|x| x.clone()).collect())
443        }
444        async fn get_extended_attribute(&self, name: Vec<u8>) -> Result<Vec<u8>, Status> {
445            let map = self.xattrs.lock();
446            map.get(&name).map(|x| x.clone()).ok_or(Status::NOT_FOUND)
447        }
448        async fn set_extended_attribute(
449            &self,
450            name: Vec<u8>,
451            value: Vec<u8>,
452            _mode: fio::SetExtendedAttributeMode,
453        ) -> Result<(), Status> {
454            let mut map = self.xattrs.lock();
455            // Don't bother replicating the mode behavior, we just care that this method is hooked
456            // up at all.
457            map.insert(name, value);
458            Ok(())
459        }
460        async fn remove_extended_attribute(&self, name: Vec<u8>) -> Result<(), Status> {
461            let mut map = self.xattrs.lock();
462            map.remove(&name);
463            Ok(())
464        }
465    }
466
467    impl GetEntryInfo for TestSymlink {
468        fn entry_info(&self) -> EntryInfo {
469            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Symlink)
470        }
471    }
472
473    async fn serve_test_symlink() -> fio::SymlinkProxy {
474        let (client_end, server_end) = create_proxy::<fio::SymlinkMarker>();
475        let flags = fio::PERM_READABLE | fio::Flags::PROTOCOL_SYMLINK;
476
477        Connection::create_sync(
478            ExecutionScope::new(),
479            Arc::new(TestSymlink::new()),
480            flags,
481            flags.to_object_request(server_end),
482        );
483
484        client_end
485    }
486
487    #[fuchsia::test]
488    async fn test_read_target() {
489        let client_end = serve_test_symlink().await;
490
491        assert_eq!(
492            client_end.describe().await.expect("fidl failed").target.expect("missing target"),
493            b"target"
494        );
495    }
496
497    #[fuchsia::test]
498    async fn test_validate_flags() {
499        let scope = ExecutionScope::new();
500
501        let check = |mut flags: fio::Flags| {
502            let (client_end, server_end) = create_proxy::<fio::SymlinkMarker>();
503            flags |= fio::Flags::FLAG_SEND_REPRESENTATION;
504            flags.to_object_request(server_end).create_connection_sync::<Connection<_>, _>(
505                scope.clone(),
506                Arc::new(TestSymlink::new()),
507                flags,
508            );
509
510            async move { client_end.take_event_stream().next().await.expect("no event") }
511        };
512
513        for flags in [
514            fio::Flags::PROTOCOL_DIRECTORY,
515            fio::Flags::PROTOCOL_FILE,
516            fio::Flags::PROTOCOL_SERVICE,
517        ] {
518            assert_matches!(
519                check(fio::PERM_READABLE | flags).await,
520                Err(fidl::Error::ClientChannelClosed { status: Status::WRONG_TYPE, .. }),
521                "{flags:?}"
522            );
523        }
524
525        assert_matches!(
526            check(fio::PERM_READABLE | fio::Flags::PROTOCOL_SYMLINK)
527                .await
528                .expect("error from next")
529                .into_on_representation()
530                .expect("expected on representation"),
531            fio::Representation::Symlink(fio::SymlinkInfo { .. })
532        );
533        assert_matches!(
534            check(fio::PERM_READABLE)
535                .await
536                .expect("error from next")
537                .into_on_representation()
538                .expect("expected on representation"),
539            fio::Representation::Symlink(fio::SymlinkInfo { .. })
540        );
541    }
542
543    #[fuchsia::test]
544    async fn test_get_attr() {
545        let client_end = serve_test_symlink().await;
546
547        let (mutable_attrs, immutable_attrs) = client_end
548            .get_attributes(fio::NodeAttributesQuery::all())
549            .await
550            .expect("fidl failed")
551            .expect("GetAttributes failed");
552
553        assert_eq!(mutable_attrs, Default::default());
554        assert_eq!(
555            immutable_attrs,
556            fio::ImmutableNodeAttributes {
557                content_size: Some(TARGET.len() as u64),
558                storage_size: Some(TARGET.len() as u64),
559                protocols: Some(fio::NodeProtocolKinds::SYMLINK),
560                abilities: Some(fio::Abilities::GET_ATTRIBUTES),
561                ..Default::default()
562            }
563        );
564    }
565
566    #[fuchsia::test]
567    async fn test_clone() {
568        let client_end = serve_test_symlink().await;
569
570        let orig_attrs = client_end
571            .get_attributes(fio::NodeAttributesQuery::all())
572            .await
573            .expect("fidl failed")
574            .unwrap();
575        // Clone the original connection and query it's attributes, which should match the original.
576        let (cloned_client, cloned_server) = create_proxy::<fio::SymlinkMarker>();
577        client_end.clone(ServerEnd::new(cloned_server.into_channel())).unwrap();
578        let cloned_attrs = cloned_client
579            .get_attributes(fio::NodeAttributesQuery::all())
580            .await
581            .expect("fidl failed")
582            .unwrap();
583        assert_eq!(orig_attrs, cloned_attrs);
584    }
585
586    #[fuchsia::test]
587    async fn test_describe() {
588        let client_end = serve_test_symlink().await;
589
590        assert_matches!(
591            client_end.describe().await.expect("fidl failed"),
592            fio::SymlinkInfo {
593                target: Some(target),
594                ..
595            } if target == b"target"
596        );
597    }
598
599    #[fuchsia::test]
600    async fn test_xattrs() {
601        let client_end = serve_test_symlink().await;
602
603        client_end
604            .set_extended_attribute(
605                b"foo",
606                fio::ExtendedAttributeValue::Bytes(b"bar".to_vec()),
607                fio::SetExtendedAttributeMode::Set,
608            )
609            .await
610            .unwrap()
611            .unwrap();
612        assert_eq!(
613            client_end.get_extended_attribute(b"foo").await.unwrap().unwrap(),
614            fio::ExtendedAttributeValue::Bytes(b"bar".to_vec()),
615        );
616        let (iterator_client_end, iterator_server_end) =
617            create_proxy::<fio::ExtendedAttributeIteratorMarker>();
618        client_end.list_extended_attributes(iterator_server_end).unwrap();
619        assert_eq!(
620            iterator_client_end.get_next().await.unwrap().unwrap(),
621            (vec![b"bar".to_vec()], true)
622        );
623        client_end.remove_extended_attribute(b"foo").await.unwrap().unwrap();
624        assert_eq!(
625            client_end.get_extended_attribute(b"foo").await.unwrap().unwrap_err(),
626            Status::NOT_FOUND.into_raw(),
627        );
628    }
629
630    #[cfg(fuchsia_api_level_at_least = "HEAD")]
631    #[fuchsia::test]
632    async fn test_open() {
633        use fidl::endpoints::Proxy;
634
635        let client_end = serve_test_symlink().await;
636
637        let (object, server_end) = fidl::Channel::create();
638        client_end
639            .open("path", fio::Flags::empty(), &fio::Options::default(), server_end)
640            .expect("fidl failed");
641
642        let requests = fio::NodeProxy::from_channel(fuchsia_async::Channel::from_channel(object));
643
644        let error = requests
645            .take_event_stream()
646            .next()
647            .await
648            .expect("no event")
649            .expect_err("error expected");
650
651        assert_matches!(error, fidl::Error::ClientChannelClosed { status: Status::NOT_DIR, .. });
652    }
653}