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