vfs/directory/
connection.rs

1// Copyright 2019 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::common::{inherit_rights_for_clone, send_on_open_with_error};
6use crate::directory::common::check_child_connection_flags;
7use crate::directory::entry_container::{Directory, DirectoryWatcher};
8use crate::directory::traversal_position::TraversalPosition;
9use crate::directory::{read_dirents, DirectoryOptions};
10use crate::execution_scope::{yield_to_executor, ExecutionScope};
11use crate::node::OpenNode;
12use crate::object_request::Representation;
13use crate::path::Path;
14
15use anyhow::Error;
16use fidl::endpoints::ServerEnd;
17use fidl_fuchsia_io as fio;
18use std::convert::TryInto as _;
19use storage_trace::{self as trace, TraceFutureExt};
20use zx_status::Status;
21
22use crate::common::CreationMode;
23use crate::{ObjectRequest, ObjectRequestRef, ProtocolsExt};
24
25/// Return type for `BaseConnection::handle_request`.
26pub enum ConnectionState {
27    /// Connection is still alive.
28    Alive,
29    /// Connection have received Node::Close message and should be closed.
30    Closed,
31}
32
33/// Handles functionality shared between mutable and immutable FIDL connections to a directory.  A
34/// single directory may contain multiple connections.  Instances of the `BaseConnection`
35/// will also hold any state that is "per-connection".  Currently that would be the access flags
36/// and the seek position.
37pub(in crate::directory) struct BaseConnection<DirectoryType: Directory> {
38    /// Execution scope this connection and any async operations and connections it creates will
39    /// use.
40    pub(in crate::directory) scope: ExecutionScope,
41
42    pub(in crate::directory) directory: OpenNode<DirectoryType>,
43
44    /// Flags set on this connection when it was opened or cloned.
45    pub(in crate::directory) options: DirectoryOptions,
46
47    /// Seek position for this connection to the directory.  We just store the element that was
48    /// returned last by ReadDirents for this connection.  Next call will look for the next element
49    /// in alphabetical order and resume from there.
50    ///
51    /// An alternative is to use an intrusive tree to have a dual index in both names and IDs that
52    /// are assigned to the entries in insertion order.  Then we can store an ID instead of the
53    /// full entry name.  This is what the C++ version is doing currently.
54    ///
55    /// It should be possible to do the same intrusive dual-indexing using, for example,
56    ///
57    ///     https://docs.rs/intrusive-collections/0.7.6/intrusive_collections/
58    ///
59    /// but, as, I think, at least for the pseudo directories, this approach is fine, and it simple
60    /// enough.
61    seek: TraversalPosition,
62}
63
64impl<DirectoryType: Directory> BaseConnection<DirectoryType> {
65    /// Constructs an instance of `BaseConnection` - to be used by derived connections, when they
66    /// need to create a nested `BaseConnection` "sub-object".  But when implementing
67    /// `create_connection`, derived connections should use the [`create_connection`] call.
68    pub(in crate::directory) fn new(
69        scope: ExecutionScope,
70        directory: OpenNode<DirectoryType>,
71        options: DirectoryOptions,
72    ) -> Self {
73        BaseConnection { scope, directory, options, seek: Default::default() }
74    }
75
76    /// Handle a [`DirectoryRequest`].  This function is responsible for handing all the basic
77    /// directory operations.
78    pub(in crate::directory) async fn handle_request(
79        &mut self,
80        request: fio::DirectoryRequest,
81    ) -> Result<ConnectionState, Error> {
82        match request {
83            #[cfg(fuchsia_api_level_at_least = "26")]
84            fio::DirectoryRequest::DeprecatedClone { flags, object, control_handle: _ } => {
85                trace::duration!(c"storage", c"Directory::DeprecatedClone");
86                self.handle_deprecated_clone(flags, object);
87            }
88            #[cfg(not(fuchsia_api_level_at_least = "26"))]
89            fio::DirectoryRequest::Clone { flags, object, control_handle: _ } => {
90                trace::duration!(c"storage", c"Directory::Clone");
91                self.handle_deprecated_clone(flags, object);
92            }
93            #[cfg(fuchsia_api_level_at_least = "26")]
94            fio::DirectoryRequest::Clone { request, control_handle: _ } => {
95                trace::duration!(c"storage", c"Directory::Clone");
96                self.handle_clone(request.into_channel());
97            }
98            #[cfg(not(fuchsia_api_level_at_least = "26"))]
99            fio::DirectoryRequest::Clone2 { request, control_handle: _ } => {
100                trace::duration!(c"storage", c"Directory::Clone2");
101                self.handle_clone(request.into_channel());
102            }
103            fio::DirectoryRequest::Close { responder } => {
104                trace::duration!(c"storage", c"Directory::Close");
105                responder.send(Ok(()))?;
106                return Ok(ConnectionState::Closed);
107            }
108            #[cfg(fuchsia_api_level_at_least = "NEXT")]
109            fio::DirectoryRequest::DeprecatedGetAttr { responder } => {
110                async move {
111                    let (status, attrs) = crate::common::io2_to_io1_attrs(
112                        self.directory.as_ref(),
113                        self.options.rights,
114                    )
115                    .await;
116                    responder.send(status.into_raw(), &attrs)
117                }
118                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
119                .await?;
120            }
121            #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
122            fio::DirectoryRequest::GetAttr { responder } => {
123                async move {
124                    let (status, attrs) = crate::common::io2_to_io1_attrs(
125                        self.directory.as_ref(),
126                        self.options.rights,
127                    )
128                    .await;
129                    responder.send(status.into_raw(), &attrs)
130                }
131                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttr"))
132                .await?;
133            }
134            fio::DirectoryRequest::GetAttributes { query, responder } => {
135                async move {
136                    // TODO(https://fxbug.dev/346585458): Restrict or remove GET_ATTRIBUTES.
137                    let attrs = self.directory.get_attributes(query).await;
138                    responder.send(
139                        attrs
140                            .as_ref()
141                            .map(|attrs| (&attrs.mutable_attributes, &attrs.immutable_attributes))
142                            .map_err(|status| status.into_raw()),
143                    )
144                }
145                .trace(trace::trace_future_args!(c"storage", c"Directory::GetAttributes"))
146                .await?;
147            }
148            fio::DirectoryRequest::UpdateAttributes { payload: _, responder } => {
149                trace::duration!(c"storage", c"Directory::UpdateAttributes");
150                // TODO(https://fxbug.dev/324112547): Handle unimplemented io2 method.
151                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
152            }
153            fio::DirectoryRequest::ListExtendedAttributes { iterator, .. } => {
154                trace::duration!(c"storage", c"Directory::ListExtendedAttributes");
155                iterator.close_with_epitaph(Status::NOT_SUPPORTED)?;
156            }
157            fio::DirectoryRequest::GetExtendedAttribute { responder, .. } => {
158                trace::duration!(c"storage", c"Directory::GetExtendedAttribute");
159                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
160            }
161            fio::DirectoryRequest::SetExtendedAttribute { responder, .. } => {
162                trace::duration!(c"storage", c"Directory::SetExtendedAttribute");
163                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
164            }
165            fio::DirectoryRequest::RemoveExtendedAttribute { responder, .. } => {
166                trace::duration!(c"storage", c"Directory::RemoveExtendedAttribute");
167                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
168            }
169            #[cfg(fuchsia_api_level_at_least = "27")]
170            fio::DirectoryRequest::GetFlags { responder } => {
171                trace::duration!(c"storage", c"Directory::GetFlags");
172                responder.send(Ok(fio::Flags::from(&self.options)))?;
173            }
174            #[cfg(fuchsia_api_level_at_least = "27")]
175            fio::DirectoryRequest::SetFlags { flags: _, responder } => {
176                trace::duration!(c"storage", c"Directory::SetFlags");
177                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
178            }
179            #[cfg(fuchsia_api_level_at_least = "27")]
180            fio::DirectoryRequest::DeprecatedGetFlags { responder } => {
181                trace::duration!(c"storage", c"Directory::DeprecatedGetFlags");
182                responder.send(Status::OK.into_raw(), self.options.to_io1())?;
183            }
184            #[cfg(fuchsia_api_level_at_least = "27")]
185            fio::DirectoryRequest::DeprecatedSetFlags { flags: _, responder } => {
186                trace::duration!(c"storage", c"Directory::DeprecatedSetFlags");
187                responder.send(Status::NOT_SUPPORTED.into_raw())?;
188            }
189            #[cfg(not(fuchsia_api_level_at_least = "27"))]
190            fio::DirectoryRequest::GetFlags { responder } => {
191                trace::duration!(c"storage", c"Directory::GetFlags");
192                responder.send(Status::OK.into_raw(), self.options.to_io1())?;
193            }
194            #[cfg(not(fuchsia_api_level_at_least = "27"))]
195            fio::DirectoryRequest::SetFlags { flags: _, responder } => {
196                trace::duration!(c"storage", c"Directory::SetFlags");
197                responder.send(Status::NOT_SUPPORTED.into_raw())?;
198            }
199            #[cfg(fuchsia_api_level_at_least = "27")]
200            fio::DirectoryRequest::DeprecatedOpen {
201                flags,
202                mode: _,
203                path,
204                object,
205                control_handle: _,
206            } => {
207                {
208                    trace::duration!(c"storage", c"Directory::Open");
209                    self.handle_deprecated_open(flags, path, object);
210                }
211                // Since open typically spawns a task, yield to the executor now to give that task a
212                // chance to run before we try and process the next request for this directory.
213                yield_to_executor().await;
214            }
215            #[cfg(not(fuchsia_api_level_at_least = "27"))]
216            fio::DirectoryRequest::Open { flags, mode: _, path, object, control_handle: _ } => {
217                {
218                    trace::duration!(c"storage", c"Directory::Open");
219                    self.handle_deprecated_open(flags, path, object);
220                }
221                // Since open typically spawns a task, yield to the executor now to give that task a
222                // chance to run before we try and process the next request for this directory.
223                yield_to_executor().await;
224            }
225            fio::DirectoryRequest::AdvisoryLock { request: _, responder } => {
226                trace::duration!(c"storage", c"Directory::AdvisoryLock");
227                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
228            }
229            fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
230                async move {
231                    let (status, entries) = self.handle_read_dirents(max_bytes).await;
232                    responder.send(status.into_raw(), entries.as_slice())
233                }
234                .trace(trace::trace_future_args!(c"storage", c"Directory::ReadDirents"))
235                .await?;
236            }
237            fio::DirectoryRequest::Rewind { responder } => {
238                trace::duration!(c"storage", c"Directory::Rewind");
239                self.seek = Default::default();
240                responder.send(Status::OK.into_raw())?;
241            }
242            fio::DirectoryRequest::Link { src, dst_parent_token, dst, responder } => {
243                async move {
244                    let status: Status = self.handle_link(&src, dst_parent_token, dst).await.into();
245                    responder.send(status.into_raw())
246                }
247                .trace(trace::trace_future_args!(c"storage", c"Directory::Link"))
248                .await?;
249            }
250            fio::DirectoryRequest::Watch { mask, options, watcher, responder } => {
251                trace::duration!(c"storage", c"Directory::Watch");
252                let status = if options != 0 {
253                    Status::INVALID_ARGS
254                } else {
255                    let watcher = watcher.try_into()?;
256                    self.handle_watch(mask, watcher).into()
257                };
258                responder.send(status.into_raw())?;
259            }
260            fio::DirectoryRequest::Query { responder } => {
261                let () = responder.send(fio::DIRECTORY_PROTOCOL_NAME.as_bytes())?;
262            }
263            fio::DirectoryRequest::QueryFilesystem { responder } => {
264                trace::duration!(c"storage", c"Directory::QueryFilesystem");
265                match self.directory.query_filesystem() {
266                    Err(status) => responder.send(status.into_raw(), None)?,
267                    Ok(info) => responder.send(0, Some(&info))?,
268                }
269            }
270            fio::DirectoryRequest::Unlink { name: _, options: _, responder } => {
271                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
272            }
273            fio::DirectoryRequest::GetToken { responder } => {
274                responder.send(Status::NOT_SUPPORTED.into_raw(), None)?;
275            }
276            fio::DirectoryRequest::Rename { src: _, dst_parent_token: _, dst: _, responder } => {
277                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
278            }
279            #[cfg(fuchsia_api_level_at_least = "NEXT")]
280            fio::DirectoryRequest::DeprecatedSetAttr { flags: _, attributes: _, responder } => {
281                responder.send(Status::NOT_SUPPORTED.into_raw())?;
282            }
283            #[cfg(not(fuchsia_api_level_at_least = "NEXT"))]
284            fio::DirectoryRequest::SetAttr { flags: _, attributes: _, responder } => {
285                responder.send(Status::NOT_SUPPORTED.into_raw())?;
286            }
287            fio::DirectoryRequest::Sync { responder } => {
288                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
289            }
290            fio::DirectoryRequest::CreateSymlink { responder, .. } => {
291                responder.send(Err(Status::NOT_SUPPORTED.into_raw()))?;
292            }
293            #[cfg(fuchsia_api_level_at_least = "27")]
294            fio::DirectoryRequest::Open { path, mut flags, options, object, control_handle: _ } => {
295                {
296                    // Remove POSIX flags when the respective rights are not available.
297                    if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
298                        flags &= !fio::Flags::PERM_INHERIT_WRITE;
299                    }
300                    if !self.options.rights.contains(fio::Rights::EXECUTE) {
301                        flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
302                    }
303
304                    ObjectRequest::new(flags, &options, object)
305                        .handle_async(async |req| self.handle_open(path, flags, req).await)
306                        .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
307                        .await;
308                }
309                // Since open typically spawns a task, yield to the executor now to give that task a
310                // chance to run before we try and process the next request for this directory.
311                yield_to_executor().await;
312            }
313            #[cfg(not(fuchsia_api_level_at_least = "27"))]
314            fio::DirectoryRequest::Open3 {
315                path,
316                mut flags,
317                options,
318                object,
319                control_handle: _,
320            } => {
321                {
322                    // Remove POSIX flags when the respective rights are not available.
323                    if !self.options.rights.contains(fio::INHERITED_WRITE_PERMISSIONS) {
324                        flags &= !fio::Flags::PERM_INHERIT_WRITE;
325                    }
326                    if !self.options.rights.contains(fio::Rights::EXECUTE) {
327                        flags &= !fio::Flags::PERM_INHERIT_EXECUTE;
328                    }
329
330                    ObjectRequest::new(flags, &options, object)
331                        .handle_async(async |req| self.handle_open(path, flags, req).await)
332                        .trace(trace::trace_future_args!(c"storage", c"Directory::Open3"))
333                        .await;
334                }
335                // Since open typically spawns a task, yield to the executor now to give that task a
336                // chance to run before we try and process the next request for this directory.
337                yield_to_executor().await;
338            }
339            fio::DirectoryRequest::_UnknownMethod { .. } => (),
340        }
341        Ok(ConnectionState::Alive)
342    }
343
344    fn handle_deprecated_clone(
345        &self,
346        flags: fio::OpenFlags,
347        server_end: ServerEnd<fio::NodeMarker>,
348    ) {
349        let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
350        let flags = match inherit_rights_for_clone(self.options.to_io1(), flags) {
351            Ok(updated) => updated,
352            Err(status) => {
353                send_on_open_with_error(describe, server_end, status);
354                return;
355            }
356        };
357
358        self.directory.clone().deprecated_open(self.scope.clone(), flags, Path::dot(), server_end);
359    }
360
361    fn handle_clone(&mut self, object: fidl::Channel) {
362        let flags = fio::Flags::from(&self.options);
363        ObjectRequest::new(flags, &Default::default(), object)
364            .handle(|req| self.directory.clone().open(self.scope.clone(), Path::dot(), flags, req));
365    }
366
367    fn handle_deprecated_open(
368        &self,
369        mut flags: fio::OpenFlags,
370        path: String,
371        server_end: ServerEnd<fio::NodeMarker>,
372    ) {
373        let describe = flags.intersects(fio::OpenFlags::DESCRIBE);
374
375        let path = match Path::validate_and_split(path) {
376            Ok(path) => path,
377            Err(status) => {
378                send_on_open_with_error(describe, server_end, status);
379                return;
380            }
381        };
382
383        if path.is_dir() {
384            flags |= fio::OpenFlags::DIRECTORY;
385        }
386
387        let flags = match check_child_connection_flags(self.options.to_io1(), flags) {
388            Ok(updated) => updated,
389            Err(status) => {
390                send_on_open_with_error(describe, server_end, status);
391                return;
392            }
393        };
394        if path.is_dot() {
395            if flags.intersects(fio::OpenFlags::NOT_DIRECTORY) {
396                send_on_open_with_error(describe, server_end, Status::INVALID_ARGS);
397                return;
398            }
399            if flags.intersects(fio::OpenFlags::CREATE_IF_ABSENT) {
400                send_on_open_with_error(describe, server_end, Status::ALREADY_EXISTS);
401                return;
402            }
403        }
404
405        // It is up to the open method to handle OPEN_FLAG_DESCRIBE from this point on.
406        let directory = self.directory.clone();
407        directory.deprecated_open(self.scope.clone(), flags, path, server_end);
408    }
409
410    async fn handle_open(
411        &self,
412        path: String,
413        flags: fio::Flags,
414        object_request: ObjectRequestRef<'_>,
415    ) -> Result<(), Status> {
416        let path = Path::validate_and_split(path)?;
417
418        // Child connection must have stricter or same rights as the parent connection.
419        if let Some(rights) = flags.rights() {
420            if rights.intersects(!self.options.rights) {
421                return Err(Status::ACCESS_DENIED);
422            }
423        }
424
425        // If requesting attributes, check permission.
426        if !object_request.attributes().is_empty()
427            && !self.options.rights.contains(fio::Operations::GET_ATTRIBUTES)
428        {
429            return Err(Status::ACCESS_DENIED);
430        }
431
432        match flags.creation_mode() {
433            CreationMode::Never => {
434                if object_request.create_attributes().is_some() {
435                    return Err(Status::INVALID_ARGS);
436                }
437            }
438            CreationMode::UnnamedTemporary | CreationMode::UnlinkableUnnamedTemporary => {
439                // We only support creating unnamed temporary files.
440                if !flags.intersects(fio::Flags::PROTOCOL_FILE) {
441                    return Err(Status::NOT_SUPPORTED);
442                }
443                // The parent connection must be able to modify directories if creating an object.
444                if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
445                    return Err(Status::ACCESS_DENIED);
446                }
447                // The ability to create an unnamed temporary file is dependent on the filesystem.
448                // We won't know if the directory the path eventually leads to supports the creation
449                // of unnamed temporary files until we have fully traversed the path. The way that
450                // Rust VFS is set up is such that the filesystem is responsible for traversing the
451                // path, so it is the filesystem's responsibility to report if it does not support
452                // this feature.
453            }
454            CreationMode::AllowExisting | CreationMode::Always => {
455                // The parent connection must be able to modify directories if creating an object.
456                if !self.options.rights.contains(fio::Rights::MODIFY_DIRECTORY) {
457                    return Err(Status::ACCESS_DENIED);
458                }
459
460                let protocol_flags = flags & fio::MASK_KNOWN_PROTOCOLS;
461                // If creating an object, exactly one protocol must be specified (the flags must be
462                // a power of two and non-zero).
463                if protocol_flags.is_empty()
464                    || (protocol_flags.bits() & (protocol_flags.bits() - 1)) != 0
465                {
466                    return Err(Status::INVALID_ARGS);
467                }
468                // Only a directory or file object can be created.
469                if !protocol_flags
470                    .intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_FILE)
471                {
472                    return Err(Status::NOT_SUPPORTED);
473                }
474            }
475        }
476
477        if path.is_dot() && flags.creation_mode() == CreationMode::Always {
478            return Err(Status::ALREADY_EXISTS);
479        }
480
481        self.directory.clone().open_async(self.scope.clone(), path, flags, object_request).await
482    }
483
484    async fn handle_read_dirents(&mut self, max_bytes: u64) -> (Status, Vec<u8>) {
485        async {
486            let (new_pos, sealed) =
487                self.directory.read_dirents(&self.seek, read_dirents::Sink::new(max_bytes)).await?;
488            self.seek = new_pos;
489            let read_dirents::Done { buf, status } = *sealed
490                .open()
491                .downcast::<read_dirents::Done>()
492                .map_err(|_: Box<dyn std::any::Any>| {
493                    #[cfg(debug)]
494                    panic!(
495                        "`read_dirents()` returned a `dirents_sink::Sealed`
496                        instance that is not an instance of the \
497                        `read_dirents::Done`. This is a bug in the \
498                        `read_dirents()` implementation."
499                    );
500                    Status::NOT_SUPPORTED
501                })?;
502            Ok((status, buf))
503        }
504        .await
505        .unwrap_or_else(|status| (status, Vec::new()))
506    }
507
508    async fn handle_link(
509        &self,
510        source_name: &str,
511        target_parent_token: fidl::Handle,
512        target_name: String,
513    ) -> Result<(), Status> {
514        if source_name.contains('/') || target_name.contains('/') {
515            return Err(Status::INVALID_ARGS);
516        }
517
518        // To avoid rights escalation, we must make sure that the connection to the source directory
519        // has the maximal set of file rights.  We do not check for EXECUTE because mutable
520        // filesystems that support link don't currently support EXECUTE rights.  The target rights
521        // are verified by virtue of the fact that it is not possible to get a token without the
522        // MODIFY_DIRECTORY right (see `MutableConnection::handle_get_token`).
523        if !self.options.rights.contains(fio::RW_STAR_DIR) {
524            return Err(Status::BAD_HANDLE);
525        }
526
527        let target_parent = self
528            .scope
529            .token_registry()
530            .get_owner(target_parent_token)?
531            .ok_or(Err(Status::NOT_FOUND))?;
532
533        target_parent.link(target_name, self.directory.clone().into_any(), source_name).await
534    }
535
536    fn handle_watch(
537        &mut self,
538        mask: fio::WatchMask,
539        watcher: DirectoryWatcher,
540    ) -> Result<(), Status> {
541        let directory = self.directory.clone();
542        directory.register_watcher(self.scope.clone(), mask, watcher)
543    }
544}
545
546impl<DirectoryType: Directory> Representation for BaseConnection<DirectoryType> {
547    type Protocol = fio::DirectoryMarker;
548
549    async fn get_representation(
550        &self,
551        requested_attributes: fio::NodeAttributesQuery,
552    ) -> Result<fio::Representation, Status> {
553        Ok(fio::Representation::Directory(fio::DirectoryInfo {
554            attributes: if requested_attributes.is_empty() {
555                None
556            } else {
557                Some(self.directory.get_attributes(requested_attributes).await?)
558            },
559            ..Default::default()
560        }))
561    }
562
563    async fn node_info(&self) -> Result<fio::NodeInfoDeprecated, Status> {
564        Ok(fio::NodeInfoDeprecated::Directory(fio::DirectoryObject))
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use crate::directory::immutable::Simple;
572    use assert_matches::assert_matches;
573    use fidl_fuchsia_io as fio;
574
575    #[fuchsia::test]
576    async fn test_open_not_found() {
577        let dir = Simple::new();
578        let dir_proxy = crate::directory::serve(dir, fio::PERM_READABLE);
579
580        // Try to open a file that doesn't exist.
581        let node_proxy = fuchsia_fs::directory::open_async::<fio::NodeMarker>(
582            &dir_proxy,
583            "foo",
584            fio::PERM_READABLE,
585        )
586        .unwrap();
587
588        // The channel is closed with a NOT_FOUND epitaph.
589        assert_matches!(
590            node_proxy.query().await,
591            Err(fidl::Error::ClientChannelClosed {
592                status: Status::NOT_FOUND,
593                protocol_name: "fuchsia.io.Node",
594                ..
595            })
596        );
597    }
598
599    #[fuchsia::test]
600    async fn test_open_with_send_representation_not_found() {
601        let dir = Simple::new();
602        let dir_proxy = crate::directory::serve(dir, fio::PERM_READABLE);
603
604        // Try to open a file that doesn't exist.
605        let node_proxy = fuchsia_fs::directory::open_async::<fio::NodeMarker>(
606            &dir_proxy,
607            "foo",
608            fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
609        )
610        .unwrap();
611
612        // The channel is closed with a NOT_FOUND epitaph.
613        assert_matches!(
614            node_proxy.query().await,
615            Err(fidl::Error::ClientChannelClosed {
616                status: Status::NOT_FOUND,
617                protocol_name: "fuchsia.io.Node",
618                ..
619            })
620        );
621    }
622}