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