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