Skip to main content

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