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