Skip to main content

sync_io_client/
lib.rs

1// Copyright 2026 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//! This library provides a synchronous wrapper around fuchsia.io.  It is usually better to use the
6//! Rust standard library for this (which will use fdio and zxio).  The primary user of this library
7//! is Starnix which uses this for performance and memory reasons.
8
9use fidl::endpoints::SynchronousProxy;
10use fidl_fuchsia_io as fio;
11use fuchsia_sync::Mutex;
12use smallvec::SmallVec;
13use std::ops::{ControlFlow, Range};
14use syncio::{AllocateMode, zxio_fsverity_descriptor_t, zxio_node_attributes_t};
15use zerocopy::FromBytes;
16
17/// Converts FIDL attributes to `zxio_node_attributes_t`.
18///
19/// NOTE: This does not work for _all_ attributes e.g. fsverity options and root hash.
20fn zxio_attr_from_fidl(
21    mutable: &fio::MutableNodeAttributes,
22    immutable: &fio::ImmutableNodeAttributes,
23) -> zxio_node_attributes_t {
24    let mut out_attr = zxio_node_attributes_t::default();
25    if let Some(protocols) = immutable.protocols {
26        out_attr.protocols = protocols.bits();
27        out_attr.has.protocols = true;
28    }
29    if let Some(abilities) = immutable.abilities {
30        out_attr.abilities = abilities.bits();
31        out_attr.has.abilities = true;
32    }
33    if let Some(id) = immutable.id {
34        out_attr.id = id;
35        out_attr.has.id = true;
36    }
37    if let Some(content_size) = immutable.content_size {
38        out_attr.content_size = content_size;
39        out_attr.has.content_size = true;
40    }
41    if let Some(storage_size) = immutable.storage_size {
42        out_attr.storage_size = storage_size;
43        out_attr.has.storage_size = true;
44    }
45    if let Some(link_count) = immutable.link_count {
46        out_attr.link_count = link_count;
47        out_attr.has.link_count = true;
48    }
49    if let Some(creation_time) = mutable.creation_time {
50        out_attr.creation_time = creation_time;
51        out_attr.has.creation_time = true;
52    }
53    if let Some(modification_time) = mutable.modification_time {
54        out_attr.modification_time = modification_time;
55        out_attr.has.modification_time = true;
56    }
57    if let Some(access_time) = mutable.access_time {
58        out_attr.access_time = access_time;
59        out_attr.has.access_time = true;
60    }
61    if let Some(mode) = mutable.mode {
62        out_attr.mode = mode;
63        out_attr.has.mode = true;
64    }
65    if let Some(uid) = mutable.uid {
66        out_attr.uid = uid;
67        out_attr.has.uid = true;
68    }
69    if let Some(gid) = mutable.gid {
70        out_attr.gid = gid;
71        out_attr.has.gid = true;
72    }
73    if let Some(rdev) = mutable.rdev {
74        out_attr.rdev = rdev;
75        out_attr.has.rdev = true;
76    }
77    if let Some(change_time) = immutable.change_time {
78        out_attr.change_time = change_time;
79        out_attr.has.change_time = true;
80    }
81    if let Some(casefold) = mutable.casefold {
82        out_attr.casefold = casefold;
83        out_attr.has.casefold = true;
84    }
85    if let Some(verity_enabled) = immutable.verity_enabled {
86        out_attr.fsverity_enabled = verity_enabled;
87        out_attr.has.fsverity_enabled = true;
88    }
89    if let Some(wrapping_key_id) = mutable.wrapping_key_id {
90        out_attr.wrapping_key_id = wrapping_key_id;
91        out_attr.has.wrapping_key_id = true;
92    }
93    out_attr
94}
95
96/// This trait is used so that clients can create their own wrappers around types exposed here.
97pub trait Factory {
98    type Result;
99
100    fn create_node(self, io: RemoteIo, info: fio::NodeInfo) -> Self::Result;
101    fn create_directory(self, io: RemoteIo, info: fio::DirectoryInfo) -> Self::Result;
102    fn create_file(self, io: RemoteIo, info: fio::FileInfo) -> Self::Result;
103    fn create_symlink(self, io: RemoteIo, info: fio::SymlinkInfo) -> Self::Result;
104}
105
106/// Waits for the fuchsia.io `OnRepresentation` event and then uses the factory to create an an
107/// appropriate object.  This returns attributes (as requested by the corresponding `open` call)
108/// that are present in the `OnRepresentation` event.
109///
110/// NOTE: The attributes returned are not comprehensive.  Check `zxio_attr_from_fidl` above for
111/// supported attributes.
112pub fn create_with_on_representation<F: Factory>(
113    proxy: fio::NodeSynchronousProxy,
114    factory: F,
115) -> Result<F::Result, zx::Status> {
116    match proxy.wait_for_event(zx::MonotonicInstant::INFINITE) {
117        Ok(fio::NodeEvent::OnRepresentation { payload }) => match payload {
118            fio::Representation::Node(info) => Ok(factory.create_node(RemoteIo::new(proxy), info)),
119            fio::Representation::Directory(info) => {
120                Ok(factory.create_directory(RemoteIo::new(proxy), info))
121            }
122            fio::Representation::File(mut info) => {
123                let io = RemoteIo {
124                    proxy,
125                    stream: info
126                        .stream
127                        .take()
128                        .map(zx::Stream::from)
129                        .unwrap_or_else(|| zx::NullableHandle::invalid().into()),
130                };
131                Ok(factory.create_file(io, info))
132            }
133            fio::Representation::Symlink(info) => {
134                Ok(factory.create_symlink(RemoteIo::new(proxy), info))
135            }
136            _ => Err(zx::Status::NOT_SUPPORTED),
137        },
138        Err(fidl::Error::ClientChannelClosed { epitaph, .. }) => match epitaph.into() {
139            Err(status) => Err(status),
140            Ok(()) => Err(zx::Status::PEER_CLOSED),
141        },
142        _ => Err(zx::Status::IO),
143    }
144}
145
146/// Wraps a proxy and optional stream and provides wrappers around most fuchsia.io methods.
147///
148/// NOTE: The caller must take care to call appropriate methods for the underlying type.  Calling
149/// the wrong methods (e.g. calling file methods on a directory) will result in the connection being
150/// closed.
151pub struct RemoteIo {
152    proxy: fio::NodeSynchronousProxy,
153    // NOTE: This can be invalid if the remote end did not return a stream in which case
154    // file I/O will use FIDL (slow).
155    stream: zx::Stream,
156}
157
158impl RemoteIo {
159    pub fn new(proxy: fio::NodeSynchronousProxy) -> Self {
160        Self { proxy, stream: zx::NullableHandle::invalid().into() }
161    }
162
163    pub fn with_stream(proxy: fio::NodeSynchronousProxy, stream: zx::Stream) -> Self {
164        Self { proxy, stream }
165    }
166
167    pub fn into_proxy(self) -> fio::NodeSynchronousProxy {
168        self.proxy
169    }
170
171    fn cast_proxy<T: From<zx::Channel> + Into<zx::NullableHandle>>(&self) -> zx::Unowned<'_, T> {
172        zx::Unowned::new(self.proxy.as_channel())
173    }
174
175    /// Returns attributes in fuchsia.io's FIDL representation.
176    pub fn attr_get(
177        &self,
178        query: fio::NodeAttributesQuery,
179    ) -> Result<(fio::MutableNodeAttributes, fio::ImmutableNodeAttributes), zx::Status> {
180        self.proxy
181            .get_attributes(query, zx::MonotonicInstant::INFINITE)
182            .map_err(|_| zx::Status::IO)?
183            .map_err(zx::Status::from_raw)
184    }
185
186    /// Returns attributes mapped to `zxio_node_attributes_t`
187    ///
188    /// NOTE: Not all attributes are supported.  See `zxio_attr_from_fidl` above for supported
189    /// attributes.
190    pub fn attr_get_zxio(
191        &self,
192        query: fio::NodeAttributesQuery,
193    ) -> Result<zxio_node_attributes_t, zx::Status> {
194        self.attr_get(query).map(|(m, i)| zxio_attr_from_fidl(&m, &i))
195    }
196
197    /// Sets attributes.
198    pub fn attr_set(&self, attributes: fio::MutableNodeAttributes) -> Result<(), zx::Status> {
199        self.proxy
200            .update_attributes(&attributes, zx::MonotonicInstant::INFINITE)
201            .map_err(|_| zx::Status::IO)?
202            .map_err(zx::Status::from_raw)
203    }
204
205    /// Wraps fuchsia.io/Directory's Open.
206    pub fn open<F: Factory>(
207        &self,
208        path: &str,
209        flags: fio::Flags,
210        create_attributes: Option<fio::MutableNodeAttributes>,
211        query: fio::NodeAttributesQuery,
212        factory: F,
213    ) -> Result<F::Result, zx::Status> {
214        let (client_end, server_end) = zx::Channel::create();
215        let dir_proxy = self.cast_proxy::<fio::DirectorySynchronousProxy>();
216        dir_proxy
217            .open(
218                path,
219                flags | fio::Flags::FLAG_SEND_REPRESENTATION,
220                &fio::Options {
221                    attributes: (!query.is_empty()).then_some(query),
222                    create_attributes,
223                    ..Default::default()
224                },
225                server_end,
226            )
227            .map_err(|_| zx::Status::IO)?;
228        create_with_on_representation(client_end.into(), factory)
229    }
230
231    /// Opens all nodes iteratively along the relative sub-paths given in `paths`.
232    /// Each item in `paths` is opened from the node returned by opening the previous item in
233    /// `paths`.
234    ///
235    /// `factory_fn` is used to create the `factory` argument to `create_with_on_representation`.
236    ///
237    /// The return vector can be smaller than the initial `paths` vector, and execution will
238    /// always stop with the first error appending an `Err(status)` into the results.
239    ///
240    /// NOTE: To prevent opening and writing through non-directory nodes, this function adds
241    /// `fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_SYMLINK` to intermediate components.
242    pub fn open_pipelined<F: Factory>(
243        &self,
244        paths: &[&str],
245        flags: fio::Flags,
246        query: fio::NodeAttributesQuery,
247        mut factory_fn: impl FnMut() -> F,
248    ) -> impl Iterator<Item = Result<F::Result, zx::Status>> {
249        let mut proxy_to_result = move |proxy: fio::NodeSynchronousProxy| {
250            create_with_on_representation(proxy, factory_fn())
251        };
252
253        let mut proxies = SmallVec::<[fio::NodeSynchronousProxy; 8]>::new();
254        let (client_end, server_end) = zx::Channel::create();
255        proxies.push(fio::NodeSynchronousProxy::new(client_end));
256        let mut next_server_end = Some(server_end);
257
258        for (i, path) in paths.iter().enumerate().rev() {
259            let server_end = next_server_end.take().unwrap();
260            let channel = if i == 0 {
261                self.proxy.as_channel()
262            } else {
263                let (client_end, server_end) = zx::Channel::create();
264                next_server_end = Some(server_end);
265                proxies.push(fio::NodeSynchronousProxy::new(client_end));
266                proxies.last().unwrap().as_channel()
267            };
268            let dir_proxy = zx::Unowned::<fio::DirectorySynchronousProxy>::new(channel);
269
270            let mut open_flags = flags | fio::Flags::FLAG_SEND_REPRESENTATION;
271            if i < paths.len() - 1 {
272                open_flags |= fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_SYMLINK;
273            }
274
275            if let Err(_) = dir_proxy.open(
276                path,
277                open_flags,
278                &fio::Options {
279                    attributes: (!query.is_empty()).then_some(query),
280                    ..Default::default()
281                },
282                server_end,
283            ) {
284                // This should only be possible for the call from self. Other channels cannot be
285                // closed as they have not been sent yet.
286                debug_assert!(i == 0);
287                // Nothing to do. The first proxy just got disconnected. `proxy_to_result` will
288                // return an error.
289            }
290        }
291
292        let mut proxies = proxies.into_iter().rev();
293        std::iter::successors(proxies.next().map(&mut proxy_to_result), move |prev| match prev {
294            Err(_) => None,
295            Ok(_) => proxies.next().map(&mut proxy_to_result),
296        })
297    }
298
299    /// Returns `(data, eof)`, where `eof` is true if we encountered the end of the file.  If `eof`
300    /// is false, then it is still possible that a subsequent read would read no more i.e. the end
301    /// of the file _might_ have been reached.  This might return fewer bytes than `max`.
302    pub fn read_partial(&self, offset: u64, max: usize) -> Result<(Vec<u8>, bool), zx::Status> {
303        if self.stream.is_invalid() {
304            let file_proxy = self.cast_proxy::<fio::FileSynchronousProxy>();
305            let max = std::cmp::min(max as u64, fio::MAX_TRANSFER_SIZE);
306            let data = file_proxy
307                .read_at(max, offset, zx::MonotonicInstant::INFINITE)
308                .map_err(|_| zx::Status::IO)?
309                .map_err(zx::Status::from_raw)?;
310            let eof = (data.len() as u64) < max;
311            Ok((data, eof))
312        } else {
313            // Use an intermediate buffer.
314            let bytes =
315                self.stream.read_at_to_vec(zx::StreamReadOptions::empty(), offset as u64, max)?;
316            let eof = bytes.len() < max;
317            Ok((bytes, eof))
318        }
319    }
320
321    /// Attempts to read `len` bytes and will only return fewer if it encounters the end of the
322    /// file, or an error.  `callback` will be called for each chunk.  If any bytes are successfully
323    /// passed to `callback`, `read` will return the total number of bytes successfully written and
324    /// any error encountered will be discarded.
325    pub fn read<E>(
326        &self,
327        offset: u64,
328        len: usize,
329        mut callback: impl FnMut(Vec<u8>) -> Result<usize, E>,
330        map_err: impl FnOnce(zx::Status) -> E,
331    ) -> Result<usize, E> {
332        let mut total = 0;
333        while total < len {
334            match self.read_partial(offset + total as u64, len - total) {
335                Ok((data, eof)) => {
336                    if data.is_empty() {
337                        break;
338                    }
339                    let data_len = data.len();
340                    let written = callback(data)?;
341                    total += written;
342                    if eof || written < data_len {
343                        break;
344                    }
345                }
346                Err(e) => {
347                    if total > 0 {
348                        break;
349                    }
350                    return Err(map_err(e));
351                }
352            }
353        }
354        Ok(total)
355    }
356
357    /// Writes `data` at `offset`.
358    pub fn write(&self, offset: u64, data: &[u8]) -> Result<usize, zx::Status> {
359        let file_proxy = self.cast_proxy::<fio::FileSynchronousProxy>();
360        let mut total_written = 0;
361        for chunk in data.chunks(fio::MAX_TRANSFER_SIZE as usize) {
362            let result = file_proxy
363                .write_at(chunk, offset + total_written as u64, zx::MonotonicInstant::INFINITE)
364                .map_err(|_| zx::Status::IO)
365                .and_then(|res| res.map_err(zx::Status::from_raw));
366            match result {
367                Ok(actual) => {
368                    let actual = actual as usize;
369                    total_written += actual;
370                    if actual < chunk.len() {
371                        return Ok(total_written);
372                    }
373                }
374                Err(e) => {
375                    if total_written > 0 {
376                        break;
377                    }
378                    return Err(e);
379                }
380            }
381        }
382        Ok(total_written)
383    }
384
385    /// Returns true if vectored operations are supported.
386    pub fn supports_vectored(&self) -> bool {
387        // We only support readv and writev if we have a stream.
388        !self.stream.is_invalid()
389    }
390
391    /// Reads into `iovecs` using a vectored read.  This is only supported with a valid stream.  See
392    /// `supports_vectored` above.
393    ///
394    /// # Safety
395    ///
396    /// Same as `zx::Stream::readv`.
397    pub unsafe fn readv(
398        &self,
399        offset: u64,
400        iovecs: &mut [zx::sys::zx_iovec_t],
401    ) -> Result<usize, zx::Status> {
402        if self.stream.is_invalid() {
403            return Err(zx::Status::NOT_SUPPORTED);
404        }
405        // SAFETY: See `zx::Stream::readv`.
406        unsafe { self.stream.readv_at(zx::StreamReadOptions::empty(), offset as u64, iovecs) }
407    }
408
409    /// Writes from `iovecs` using vectored write.  This is only supported with a valid stream.  See
410    /// `supports_vectored` above.
411    pub fn writev(&self, offset: u64, iovecs: &[zx::sys::zx_iovec_t]) -> Result<usize, zx::Status> {
412        if self.stream.is_invalid() {
413            return Err(zx::Status::NOT_SUPPORTED);
414        }
415        self.stream.writev_at(zx::StreamWriteOptions::empty(), offset, &iovecs)
416    }
417
418    /// Wraps fuchsia.io/File's Truncate.
419    pub fn truncate(&self, length: u64) -> Result<(), zx::Status> {
420        self.cast_proxy::<fio::FileSynchronousProxy>()
421            .resize(length, zx::MonotonicInstant::INFINITE)
422            .map_err(|_| zx::Status::IO)?
423            .map_err(zx::Status::from_raw)
424    }
425
426    /// Returns a VMO backing the file.
427    pub fn vmo_get(&self, flags: zx::VmarFlags) -> Result<zx::Vmo, zx::Status> {
428        let mut fio_flags = fio::VmoFlags::empty();
429        if flags.contains(zx::VmarFlags::PERM_READ) {
430            fio_flags |= fio::VmoFlags::READ;
431        }
432        if flags.contains(zx::VmarFlags::PERM_WRITE) {
433            fio_flags |= fio::VmoFlags::WRITE;
434        }
435        if flags.contains(zx::VmarFlags::PERM_EXECUTE) {
436            fio_flags |= fio::VmoFlags::EXECUTE;
437        }
438        let file_proxy = self.cast_proxy::<fio::FileSynchronousProxy>();
439        let vmo = file_proxy
440            .get_backing_memory(fio_flags, zx::MonotonicInstant::INFINITE)
441            .map_err(|_| zx::Status::IO)?
442            .map_err(zx::Status::from_raw)?;
443        Ok(vmo)
444    }
445
446    /// Wraps fuchsia.io/Node's Sync.
447    pub fn sync(&self) -> Result<(), zx::Status> {
448        self.proxy
449            .sync(zx::MonotonicInstant::INFINITE)
450            .map_err(|_| zx::Status::IO)?
451            .map_err(zx::Status::from_raw)
452    }
453
454    /// Closes and updates access time asynchronously.
455    pub fn close_and_update_access_time(self) {
456        let _ = self.proxy.get_attributes(
457            fio::NodeAttributesQuery::PENDING_ACCESS_TIME_UPDATE,
458            zx::MonotonicInstant::INFINITE_PAST,
459        );
460    }
461
462    /// Clones (in the fuchsia.unknown.Clonable sense) the underlying proxy.
463    pub fn clone_proxy(&self) -> Result<fio::NodeSynchronousProxy, zx::Status> {
464        let (client_end, server_end) = zx::Channel::create();
465        self.proxy.clone(server_end.into()).map_err(|_| zx::Status::IO)?;
466        Ok(client_end.into())
467    }
468
469    /// Wraps fuchsia.io/Node's LinkInto.
470    pub fn link_into(&self, target_dir: &Self, name: &str) -> Result<(), zx::Status> {
471        let target_dir_proxy = target_dir.cast_proxy::<fio::DirectorySynchronousProxy>();
472        let (status, token) = target_dir_proxy
473            .get_token(zx::MonotonicInstant::INFINITE)
474            .map_err(|_| zx::Status::IO)?;
475        zx::Status::ok(status)?;
476        let token = token.ok_or(zx::Status::NOT_SUPPORTED)?;
477
478        // Linkable::LinkInto is a separate protocol.
479        let linkable_proxy = self.cast_proxy::<fio::LinkableSynchronousProxy>();
480
481        linkable_proxy
482            .link_into(token.into(), name, zx::MonotonicInstant::INFINITE)
483            .map_err(|_| zx::Status::IO)?
484            .map_err(zx::Status::from_raw)
485    }
486
487    /// Wraps fuchsia.io/Directory's Unlink.
488    pub fn unlink(&self, name: &str, flags: fio::UnlinkFlags) -> Result<(), zx::Status> {
489        let options = fio::UnlinkOptions { flags: Some(flags), ..Default::default() };
490        let dir_proxy = self.cast_proxy::<fio::DirectorySynchronousProxy>();
491        dir_proxy
492            .unlink(name, &options, zx::MonotonicInstant::INFINITE)
493            .map_err(|_| zx::Status::IO)?
494            .map_err(zx::Status::from_raw)
495    }
496
497    /// Wraps fuchsia.io/Directory's Rename.
498    pub fn rename(
499        &self,
500        old_path: &str,
501        new_directory: &Self,
502        new_path: &str,
503    ) -> Result<(), zx::Status> {
504        let new_dir_proxy = new_directory.cast_proxy::<fio::DirectorySynchronousProxy>();
505        let (status, token) =
506            new_dir_proxy.get_token(zx::MonotonicInstant::INFINITE).map_err(|_| zx::Status::IO)?;
507        zx::Status::ok(status)?;
508        let token = token.ok_or(zx::Status::NOT_SUPPORTED)?;
509        let dir_proxy = self.cast_proxy::<fio::DirectorySynchronousProxy>();
510        dir_proxy
511            .rename(old_path, token.into(), new_path, zx::MonotonicInstant::INFINITE)
512            .map_err(|_| zx::Status::IO)?
513            .map_err(zx::Status::from_raw)
514    }
515
516    /// Wraps fuchsia.io/Directory's CreateSymlink.
517    pub fn create_symlink(&self, name: &str, target: &[u8]) -> Result<RemoteIo, zx::Status> {
518        let dir_proxy = self.cast_proxy::<fio::DirectorySynchronousProxy>();
519        let (client_end, server_end) = zx::Channel::create();
520        dir_proxy
521            .create_symlink(name, target, Some(server_end.into()), zx::MonotonicInstant::INFINITE)
522            .map_err(|_| zx::Status::IO)?
523            .map_err(zx::Status::from_raw)?;
524        Ok(RemoteIo::new(client_end.into()))
525    }
526
527    /// Wraps fuchsia.io/File's EnableVerity.
528    pub fn enable_verity(&self, descriptor: &zxio_fsverity_descriptor_t) -> Result<(), zx::Status> {
529        let file_proxy = self.cast_proxy::<fio::FileSynchronousProxy>();
530        let options = fio::VerificationOptions {
531            hash_algorithm: Some(match descriptor.hash_algorithm {
532                1 => fio::HashAlgorithm::Sha256,
533                2 => fio::HashAlgorithm::Sha512,
534                _ => return Err(zx::Status::INVALID_ARGS),
535            }),
536            salt: Some(descriptor.salt[..descriptor.salt_size as usize].to_vec()),
537            ..Default::default()
538        };
539        file_proxy
540            .enable_verity(&options, zx::MonotonicInstant::INFINITE)
541            .map_err(|_| zx::Status::IO)?
542            .map_err(zx::Status::from_raw)
543    }
544
545    /// Wraps fuchsia.io/File's Allocate.
546    pub fn allocate(&self, offset: u64, len: u64, mode: AllocateMode) -> Result<(), zx::Status> {
547        let file_proxy = self.cast_proxy::<fio::FileSynchronousProxy>();
548        let mut fio_mode = fio::AllocateMode::empty();
549        if mode.contains(AllocateMode::KEEP_SIZE) {
550            fio_mode |= fio::AllocateMode::KEEP_SIZE;
551        }
552        if mode.contains(AllocateMode::UNSHARE_RANGE) {
553            fio_mode |= fio::AllocateMode::UNSHARE_RANGE;
554        }
555        if mode.contains(AllocateMode::PUNCH_HOLE) {
556            fio_mode |= fio::AllocateMode::PUNCH_HOLE;
557        }
558        if mode.contains(AllocateMode::COLLAPSE_RANGE) {
559            fio_mode |= fio::AllocateMode::COLLAPSE_RANGE;
560        }
561        if mode.contains(AllocateMode::ZERO_RANGE) {
562            fio_mode |= fio::AllocateMode::ZERO_RANGE;
563        }
564        if mode.contains(AllocateMode::INSERT_RANGE) {
565            fio_mode |= fio::AllocateMode::INSERT_RANGE;
566        }
567        file_proxy
568            .allocate(offset, len, fio_mode, zx::MonotonicInstant::INFINITE)
569            .map_err(|_| zx::Status::IO)?
570            .map_err(zx::Status::from_raw)
571    }
572
573    /// Wraps fuchsia.io/Node's GetExtendedAttribute.
574    pub fn xattr_get(&self, name: &[u8]) -> Result<Vec<u8>, zx::Status> {
575        let name_str = std::str::from_utf8(name.as_ref()).map_err(|_| zx::Status::INVALID_ARGS)?;
576        let result = self
577            .proxy
578            .get_extended_attribute(name_str.as_bytes(), zx::MonotonicInstant::INFINITE)
579            .map_err(|_| zx::Status::IO)?
580            .map_err(zx::Status::from_raw)?;
581        match result {
582            fio::ExtendedAttributeValue::Bytes(bytes) => Ok(bytes),
583            fio::ExtendedAttributeValue::Buffer(vmo) => {
584                let size = vmo.get_content_size().map_err(|_| zx::Status::IO)?;
585                let mut bytes = vec![0u8; size as usize];
586                vmo.read(&mut bytes, 0).map_err(|_| zx::Status::IO)?;
587                Ok(bytes)
588            }
589            _ => Err(zx::Status::NOT_SUPPORTED),
590        }
591    }
592
593    /// Wraps fuchsia.io/Node's SetExtendedAttribute.
594    pub fn xattr_set(
595        &self,
596        name: &[u8],
597        value: &[u8],
598        mode: syncio::XattrSetMode,
599    ) -> Result<(), zx::Status> {
600        let val = fio::ExtendedAttributeValue::Bytes(value.to_vec());
601        let fidl_mode = match mode {
602            syncio::XattrSetMode::Set => fio::SetExtendedAttributeMode::Set,
603            syncio::XattrSetMode::Create => fio::SetExtendedAttributeMode::Create,
604            syncio::XattrSetMode::Replace => fio::SetExtendedAttributeMode::Replace,
605        };
606        self.proxy
607            .set_extended_attribute(name, val, fidl_mode, zx::MonotonicInstant::INFINITE)
608            .map_err(|_| zx::Status::IO)?
609            .map_err(zx::Status::from_raw)
610    }
611
612    /// Wraps fuchsia.io/Node's RenoveExtendedAttribute.
613    pub fn xattr_remove(&self, name: &[u8]) -> Result<(), zx::Status> {
614        let name_str = std::str::from_utf8(name.as_ref()).map_err(|_| zx::Status::INVALID_ARGS)?;
615        self.proxy
616            .remove_extended_attribute(name_str.as_bytes(), zx::MonotonicInstant::INFINITE)
617            .map_err(|_| zx::Status::IO)?
618            .map_err(zx::Status::from_raw)
619    }
620
621    /// Wraps fuchsia.io/Node's ListExtendedAttributes.
622    pub fn xattr_list(&self) -> Result<Vec<Vec<u8>>, zx::Status> {
623        let (client_end, server_end) = zx::Channel::create();
624        self.proxy.list_extended_attributes(server_end.into()).map_err(|_| zx::Status::IO)?;
625        let iterator = fio::ExtendedAttributeIteratorSynchronousProxy::new(client_end);
626        let mut all_attrs = vec![];
627        loop {
628            let (attributes, last) = iterator
629                .get_next(zx::MonotonicInstant::INFINITE)
630                .map_err(|_| zx::Status::IO)?
631                .map_err(zx::Status::from_raw)?;
632            all_attrs.extend(attributes);
633            if last {
634                break;
635            }
636        }
637        Ok(all_attrs)
638    }
639}
640
641/// RemoteDirectory supports iteration of directories and things that you can do to directories via
642/// a file descriptor.  Other opterations, such as creating children, can be done via `RemoteIo`.
643/// Iteration is not safe to be done concurrently because there is a seek pointer; `readdir` will
644/// resume from the seek position.
645pub struct RemoteDirectory {
646    proxy: fio::DirectorySynchronousProxy,
647    state: Mutex<State>,
648}
649
650#[derive(Default)]
651struct State {
652    /// Buffer contains the last batch of entries read from the remote end.
653    buffer: Vec<u8>,
654
655    /// Position in the buffer for the next entry.
656    offset: usize,
657
658    /// If the last attempt to write to the sink failed, this contains the entry that is pending to
659    /// be added. This is also used to synthesize dot-dot.
660    pending_entry: Entry,
661
662    /// The current iterator position in the directory.
663    current_index: u64,
664}
665
666impl State {
667    fn name(&self, range: Range<usize>) -> &[u8] {
668        &self.buffer[range]
669    }
670
671    /// Returns the next dir entry. If no more entries are found, returns None.  Returns an error if
672    /// the iterator fails.
673    fn next(&mut self, proxy: &fio::DirectorySynchronousProxy) -> Result<Entry, zx::Status> {
674        let mut next_dirent = || -> Result<Entry, zx::Status> {
675            if self.offset >= self.buffer.len() {
676                match proxy.read_dirents(fio::MAX_BUF, zx::MonotonicInstant::INFINITE) {
677                    Ok((status, dirents)) => {
678                        zx::Status::ok(status)?;
679                        if dirents.is_empty() {
680                            return Ok(Entry::None);
681                        }
682                        self.buffer = dirents;
683                        self.offset = 0;
684                    }
685                    Err(_) => return Err(zx::Status::IO),
686                }
687            }
688
689            #[repr(C, packed)]
690            #[derive(FromBytes)]
691            struct DirectoryEntry {
692                ino: u64,
693                name_len: u8,
694                entry_type: u8,
695            }
696
697            let Some((ino, name, entry_type)) =
698                DirectoryEntry::read_from_prefix(&self.buffer[self.offset..]).ok().and_then(
699                    |(DirectoryEntry { ino, name_len, entry_type }, remainder)| {
700                        let name_len = name_len as usize;
701                        let name_start = self.offset + std::mem::size_of::<DirectoryEntry>();
702                        (remainder.len() >= name_len).then_some((
703                            ino,
704                            name_start..name_start + name_len,
705                            entry_type,
706                        ))
707                    },
708                )
709            else {
710                // Truncated entry.
711                return Ok(Entry::None);
712            };
713
714            self.offset = name.end;
715
716            Ok(Entry::Some {
717                ino,
718                entry_type: fio::DirentType::from_primitive(entry_type).ok_or(zx::Status::IO)?,
719                name,
720            })
721        };
722
723        let mut next = self.pending_entry.take();
724        if let Entry::None = next {
725            next = next_dirent()?;
726        }
727        // We only want to synthesize .. if . exists because the . and .. entries get removed if the
728        // directory is unlinked, so if the remote filesystem has removed ., we know to omit the
729        // .. entry.
730        match &next {
731            Entry::Some { name, .. } if self.name(name.clone()) == b"." => {
732                self.pending_entry = Entry::DotDot;
733            }
734            _ => {}
735        }
736        self.current_index += 1;
737        Ok(next)
738    }
739
740    fn rewind(&mut self, proxy: &fio::DirectorySynchronousProxy) -> Result<(), zx::Status> {
741        self.pending_entry = Entry::None;
742        let status = proxy.rewind(zx::MonotonicInstant::INFINITE).map_err(|_| zx::Status::IO)?;
743        zx::Status::ok(status)?;
744        self.buffer.clear();
745        self.offset = 0;
746        self.current_index = 0;
747        Ok(())
748    }
749}
750
751#[derive(Default)]
752enum Entry {
753    // Indicates no more entries.
754    #[default]
755    None,
756
757    Some {
758        ino: u64,
759        entry_type: fio::DirentType,
760        name: Range<usize>,
761    },
762
763    // Indicates dot-dot should be synthesized.
764    DotDot,
765}
766
767impl Entry {
768    fn take(&mut self) -> Entry {
769        std::mem::replace(self, Entry::None)
770    }
771}
772
773impl RemoteDirectory {
774    pub fn new(proxy: fio::DirectorySynchronousProxy) -> Self {
775        Self { proxy, state: Mutex::default() }
776    }
777
778    /// Seeks to `new_index` in the directory.
779    pub fn seek(&self, new_index: u64) -> Result<u64, zx::Status> {
780        let mut state = self.state.lock();
781
782        if new_index < state.current_index {
783            // Our iterator only goes forward, so reset it here.  Note: we *must* rewind it rather
784            // than just create a new iterator because the remote end maintains the offset.
785            state.rewind(&self.proxy)?;
786            state.current_index = 0;
787        }
788
789        // Advance the iterator to catch up with the offset.
790        for i in state.current_index..new_index {
791            match state.next(&self.proxy) {
792                Ok(Entry::Some { .. } | Entry::DotDot) => {}
793                Ok(Entry::None) => break, // No more entries.
794                Err(_) => {
795                    // In order to keep the offset and the iterator in sync, set the new offset
796                    // to be as far as we could get.
797                    // Note that failing the seek here would also cause the iterator and the
798                    // offset to not be in sync, because the iterator has already moved from
799                    // where it was.
800                    return Ok(i);
801                }
802            }
803        }
804
805        Ok(new_index)
806    }
807
808    /// Returns `None` if there are no more entries to be read.  `sink` can choose to return
809    /// `ControlFlow::Break(_)` in which case the entry will be returned the next time `readdir` is
810    /// called.
811    pub fn readdir<B, S: FnMut(u64, fio::DirentType, &[u8]) -> ControlFlow<B, ()>>(
812        &self,
813        mut sink: S,
814    ) -> Result<Option<B>, zx::Status> {
815        let mut state = self.state.lock();
816        loop {
817            let entry = state.next(&self.proxy)?;
818            if let ControlFlow::Break(b) = match &entry {
819                Entry::Some { ino, entry_type, name } => {
820                    sink(*ino, *entry_type, state.name(name.clone()))
821                }
822                Entry::DotDot => sink(0, fio::DirentType::Directory, b".."),
823                Entry::None => break,
824            } {
825                state.pending_entry = entry;
826                return Ok(Some(b));
827            }
828        }
829        Ok(None)
830    }
831
832    /// Wraps fuchsia.io/Node's Sync.
833    pub fn sync(&self) -> Result<(), zx::Status> {
834        self.proxy
835            .sync(zx::MonotonicInstant::INFINITE)
836            .map_err(|_| zx::Status::IO)?
837            .map_err(zx::Status::from_raw)
838    }
839
840    /// Clones (in the fuchsia.unknown.Clonable sense) the underlying proxy.
841    pub fn clone_proxy(&self) -> Result<fio::DirectorySynchronousProxy, zx::Status> {
842        let (client_end, server_end) = zx::Channel::create();
843        self.proxy.clone(server_end.into()).map_err(|_| zx::Status::IO)?;
844        Ok(client_end.into())
845    }
846}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851    use fidl::endpoints::RequestStream;
852    use fuchsia_async as fasync;
853    use futures::StreamExt;
854    use std::sync::Arc;
855    use std::sync::atomic::{AtomicU64, Ordering};
856
857    #[fuchsia::test]
858    async fn test_read_chunking() {
859        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::FileMarker>();
860        let content = vec![0xAB; (fio::MAX_TRANSFER_SIZE + 100) as usize];
861        let content_clone = content.clone();
862
863        let _server_task = fasync::Task::spawn(async move {
864            while let Some(Ok(request)) = stream.next().await {
865                match request {
866                    fio::FileRequest::ReadAt { count, offset, responder } => {
867                        let start = offset as usize;
868                        let end = std::cmp::min(start + count as usize, content_clone.len());
869                        let data = if start < content_clone.len() {
870                            &content_clone[start..end]
871                        } else {
872                            &[]
873                        };
874                        responder.send(Ok(data)).unwrap();
875                    }
876                    _ => panic!("Unexpected request: {:?}", request),
877                }
878            }
879        });
880
881        let io = RemoteIo::new(client.into_channel().into());
882        fasync::unblock(move || {
883            let mut data = Vec::new();
884            let actual = io
885                .read(
886                    0,
887                    content.len(),
888                    |chunk| -> Result<usize, zx::Status> {
889                        let len = chunk.len();
890                        data.extend(chunk);
891                        Ok(len)
892                    },
893                    |status| status,
894                )
895                .unwrap();
896            assert_eq!(actual, content.len());
897            assert_eq!(data, content);
898        })
899        .await;
900    }
901
902    #[fuchsia::test]
903    async fn test_read_error_after_data() {
904        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::FileMarker>();
905        let chunk_size = 100;
906        let content = vec![0xAA; chunk_size];
907        let content_clone = content.clone();
908
909        let _server_task = fasync::Task::spawn(async move {
910            let mut request_count = 0;
911            while let Some(Ok(request)) = stream.next().await {
912                match request {
913                    fio::FileRequest::ReadAt { count: _, offset: _, responder } => {
914                        request_count += 1;
915                        if request_count == 1 {
916                            responder.send(Ok(&content_clone)).unwrap();
917                        } else {
918                            responder.send(Err(zx::sys::ZX_ERR_IO)).unwrap();
919                        }
920                    }
921                    _ => panic!("Unexpected request: {:?}", request),
922                }
923            }
924        });
925
926        let io = RemoteIo::new(client.into_channel().into());
927        fasync::unblock(move || {
928            let mut data = Vec::new();
929            // Ask for more than chunk_size to ensure a second request is made.
930            let actual = io
931                .read(
932                    0,
933                    chunk_size * 2,
934                    |chunk| -> Result<usize, zx::Status> {
935                        let len = chunk.len();
936                        data.extend(chunk);
937                        Ok(len)
938                    },
939                    |status| status,
940                )
941                .expect("read should succeed even if later chunks fail");
942            assert_eq!(actual, chunk_size);
943            assert_eq!(data.len(), chunk_size);
944            assert_eq!(data, content);
945        })
946        .await;
947    }
948
949    #[fuchsia::test]
950    async fn test_write_chunking() {
951        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::FileMarker>();
952        let content = vec![0xCD; (fio::MAX_TRANSFER_SIZE + 100) as usize];
953        let content2 = content.clone();
954
955        let server_task = fasync::Task::spawn(async move {
956            let mut written = vec![0; content2.len()];
957            while let Some(Ok(request)) = stream.next().await {
958                match request {
959                    fio::FileRequest::WriteAt { offset, data, responder, .. } => {
960                        let offset = offset as usize;
961                        written[offset..offset + data.len()].copy_from_slice(&data);
962                        responder.send(Ok(data.len() as u64)).unwrap();
963                    }
964                    _ => panic!("Unexpected request: {:?}", request),
965                }
966            }
967            assert_eq!(written, content2);
968        });
969
970        let io = RemoteIo::new(client.into_channel().into());
971        fasync::unblock(move || {
972            let written = io.write(0, &content).expect("write failed");
973            assert_eq!(written, content.len());
974        })
975        .await;
976
977        server_task.await;
978    }
979
980    #[fuchsia::test]
981    async fn test_write_error_after_data() {
982        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::FileMarker>();
983        let chunk_size = fio::MAX_TRANSFER_SIZE as usize;
984        let content = vec![0xEE; chunk_size * 2];
985
986        let _server_task = fasync::Task::spawn(async move {
987            let mut request_count = 0;
988            while let Some(Ok(request)) = stream.next().await {
989                match request {
990                    fio::FileRequest::WriteAt { offset: _, data, responder, .. } => {
991                        request_count += 1;
992                        if request_count == 1 {
993                            responder.send(Ok(data.len() as u64)).unwrap();
994                        } else {
995                            responder.send(Err(zx::sys::ZX_ERR_IO)).unwrap();
996                        }
997                    }
998                    _ => panic!("Unexpected request: {:?}", request),
999                }
1000            }
1001        });
1002
1003        let io = RemoteIo::new(client.into_channel().into());
1004        fasync::unblock(move || {
1005            let written = io.write(0, &content).expect("write should succeed partial");
1006            assert_eq!(written, chunk_size);
1007        })
1008        .await;
1009    }
1010
1011    #[fuchsia::test]
1012    async fn test_large_directory() {
1013        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::DirectoryMarker>();
1014        let num_entries = 2000;
1015
1016        let task = fasync::Task::spawn(async move {
1017            let mut sent_count = 0;
1018            let mut num_requests = 0;
1019            while let Some(Ok(request)) = stream.next().await {
1020                match request {
1021                    fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
1022                        num_requests += 1;
1023                        let mut buffer = Vec::new();
1024                        while sent_count < num_entries {
1025                            let name = if sent_count == 0 {
1026                                ".".to_string()
1027                            } else {
1028                                format!("file_{}", sent_count - 1)
1029                            };
1030                            let name_bytes = name.as_bytes();
1031                            let entry_size = 10 + name_bytes.len();
1032                            if buffer.len() + entry_size > max_bytes as usize {
1033                                break;
1034                            }
1035                            buffer.extend_from_slice(&(sent_count as u64 + 1).to_le_bytes());
1036                            buffer.push(name_bytes.len() as u8);
1037                            let entry_type = if sent_count == 0 {
1038                                fio::DirentType::Directory
1039                            } else {
1040                                fio::DirentType::File
1041                            };
1042                            buffer.push(entry_type.into_primitive());
1043                            buffer.extend_from_slice(name_bytes);
1044                            sent_count += 1;
1045                        }
1046                        let _ = responder.send(0, &buffer);
1047                    }
1048                    fio::DirectoryRequest::Rewind { responder } => {
1049                        sent_count = 0;
1050                        let _ = responder.send(0);
1051                    }
1052                    fio::DirectoryRequest::Close { responder } => {
1053                        let _ = responder.send(Ok(()));
1054                    }
1055                    _ => {}
1056                }
1057            }
1058            assert!(num_requests > 0);
1059        });
1060
1061        let dir = RemoteDirectory::new(client.into_channel().into());
1062        let count = Arc::new(AtomicU64::new(0));
1063        let count2 = count.clone();
1064        fasync::unblock(move || {
1065            dir.readdir::<(), _>(|_ino, _type, _name| {
1066                count.fetch_add(1, Ordering::Relaxed);
1067                ControlFlow::Continue(())
1068            })
1069            .unwrap();
1070        })
1071        .await;
1072        // Expect num_entries + 1 (for synthesized "..")
1073        assert_eq!(count2.load(Ordering::Relaxed), num_entries + 1);
1074        task.await;
1075    }
1076
1077    #[fuchsia::test]
1078    async fn test_seek_backwards() {
1079        let (client, mut stream) = fidl::endpoints::create_request_stream::<fio::DirectoryMarker>();
1080        let _server_task = fasync::Task::spawn(async move {
1081            let entries = vec![
1082                (1, fio::DirentType::Directory, "."),
1083                (2, fio::DirentType::File, "file_0"),
1084                (3, fio::DirentType::File, "file_1"),
1085                (4, fio::DirentType::File, "file_2"),
1086            ];
1087            let mut current_entry = 0;
1088
1089            while let Some(Ok(request)) = stream.next().await {
1090                match request {
1091                    fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
1092                        let mut buffer = Vec::new();
1093                        while current_entry < entries.len() {
1094                            let (ino, type_, name) = entries[current_entry];
1095                            let name_bytes = name.as_bytes();
1096                            let entry_size = 10 + name_bytes.len();
1097                            if buffer.len() + entry_size > max_bytes as usize {
1098                                break;
1099                            }
1100                            buffer.extend_from_slice(&(ino as u64).to_le_bytes());
1101                            buffer.push(name_bytes.len() as u8);
1102                            buffer.push(type_.into_primitive());
1103                            buffer.extend_from_slice(name_bytes);
1104                            current_entry += 1;
1105                        }
1106                        responder.send(0, &buffer).unwrap();
1107                    }
1108                    fio::DirectoryRequest::Rewind { responder } => {
1109                        current_entry = 0;
1110                        responder.send(0).unwrap();
1111                    }
1112                    _ => {}
1113                }
1114            }
1115        });
1116
1117        let dir = RemoteDirectory::new(client.into_channel().into());
1118        fasync::unblock(move || {
1119            let mut names = Vec::new();
1120            // Read 3 entries: ".", "..", "file_0".
1121            dir.readdir::<(), _>(|_ino, _type, name| {
1122                names.push(name.to_vec());
1123                if names.len() == 3 { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
1124            })
1125            .unwrap();
1126
1127            assert_eq!(names[0], b".");
1128            assert_eq!(names[1], b"..");
1129            assert_eq!(names[2], b"file_0");
1130
1131            // Seek to 1. This triggers rewind() internally because 1 < current_index (3).
1132            // Index 1 corresponds to "..".
1133            dir.seek(1).unwrap();
1134
1135            let mut names_after_seek = Vec::new();
1136            // Read 2 entries: "..", "file_0".
1137            dir.readdir::<(), _>(|_ino, _type, name| {
1138                names_after_seek.push(name.to_vec());
1139                if names_after_seek.len() == 2 {
1140                    ControlFlow::Break(())
1141                } else {
1142                    ControlFlow::Continue(())
1143                }
1144            })
1145            .unwrap();
1146
1147            assert_eq!(names_after_seek[0], b"..");
1148            assert_eq!(names_after_seek[1], b"file_0");
1149        })
1150        .await;
1151    }
1152
1153    struct DummyFactory;
1154
1155    impl Factory for DummyFactory {
1156        type Result = RemoteIo;
1157        fn create_node(self, io: RemoteIo, _info: fio::NodeInfo) -> Self::Result {
1158            io
1159        }
1160        fn create_directory(self, io: RemoteIo, _info: fio::DirectoryInfo) -> Self::Result {
1161            io
1162        }
1163        fn create_file(self, io: RemoteIo, _info: fio::FileInfo) -> Self::Result {
1164            io
1165        }
1166        fn create_symlink(self, io: RemoteIo, _info: fio::SymlinkInfo) -> Self::Result {
1167            io
1168        }
1169    }
1170
1171    #[fuchsia::test]
1172    async fn test_open_pipelined() {
1173        let (client, stream) = fidl::endpoints::create_request_stream::<fio::DirectoryMarker>();
1174
1175        fn serve_mock_directory(
1176            mut stream: fio::DirectoryRequestStream,
1177            mut expected_paths: Vec<String>,
1178        ) {
1179            fasync::Task::spawn(async move {
1180                if let Some(Ok(request)) = stream.next().await {
1181                    match request {
1182                        fio::DirectoryRequest::Open { path, flags, options: _, object, .. } => {
1183                            if !expected_paths.is_empty() {
1184                                let expected = expected_paths.remove(0);
1185                                assert_eq!(path, expected);
1186                                if expected == "path1" {
1187                                    assert!(flags.contains(fio::Flags::PROTOCOL_DIRECTORY));
1188                                } else if expected == "path2" {
1189                                    assert!(!flags.contains(fio::Flags::PROTOCOL_DIRECTORY));
1190                                }
1191                            }
1192                            let server_end =
1193                                fidl::endpoints::ServerEnd::<fio::DirectoryMarker>::new(object);
1194                            let dir_stream = server_end.into_stream();
1195                            let control_handle = dir_stream.control_handle();
1196                            let representation =
1197                                fio::Representation::Directory(fio::DirectoryInfo::default());
1198                            control_handle.send_on_representation(representation).unwrap();
1199
1200                            serve_mock_directory(dir_stream, expected_paths);
1201                        }
1202                        _ => {}
1203                    }
1204                }
1205            })
1206            .detach();
1207        }
1208
1209        serve_mock_directory(stream, vec!["path1".to_string(), "path2".to_string()]);
1210
1211        let io = RemoteIo::new(client.into_channel().into());
1212        fasync::unblock(move || {
1213            let results = io
1214                .open_pipelined(
1215                    &["path1", "path2"],
1216                    fio::Flags::empty(),
1217                    fio::NodeAttributesQuery::empty(),
1218                    || DummyFactory,
1219                )
1220                .collect::<Vec<_>>();
1221            assert_eq!(results.len(), 2);
1222            assert!(results.iter().all(|r| r.is_ok()));
1223        })
1224        .await;
1225    }
1226
1227    #[fuchsia::test]
1228    async fn test_open_pipelined_not_found() {
1229        let (client, stream) = fidl::endpoints::create_request_stream::<fio::DirectoryMarker>();
1230
1231        fn serve_mock_directory_not_found(mut stream: fio::DirectoryRequestStream) {
1232            fasync::Task::spawn(async move {
1233                if let Some(Ok(request)) = stream.next().await {
1234                    match request {
1235                        fio::DirectoryRequest::Open {
1236                            path, flags: _, options: _, object, ..
1237                        } => {
1238                            let server_end =
1239                                fidl::endpoints::ServerEnd::<fio::DirectoryMarker>::new(object);
1240                            let dir_stream = server_end.into_stream();
1241                            let control_handle = dir_stream.control_handle();
1242
1243                            if path == "not_found" {
1244                                control_handle.shutdown_with_epitaph(zx::Status::NOT_FOUND);
1245                            } else {
1246                                let representation =
1247                                    fio::Representation::Directory(fio::DirectoryInfo::default());
1248                                control_handle.send_on_representation(representation).unwrap();
1249                                serve_mock_directory_not_found(dir_stream);
1250                            }
1251                        }
1252                        _ => {}
1253                    }
1254                }
1255            })
1256            .detach();
1257        }
1258
1259        serve_mock_directory_not_found(stream);
1260
1261        let io = RemoteIo::new(client.into_channel().into());
1262        fasync::unblock(move || {
1263            let results = io
1264                .open_pipelined(
1265                    &["path1", "not_found", "path3"],
1266                    fio::Flags::empty(),
1267                    fio::NodeAttributesQuery::empty(),
1268                    || DummyFactory,
1269                )
1270                .collect::<Vec<_>>();
1271            assert_eq!(results.len(), 2);
1272            assert!(results[0].is_ok());
1273            assert_eq!(results[1].as_ref().err(), Some(&zx::Status::NOT_FOUND));
1274        })
1275        .await;
1276    }
1277
1278    #[fuchsia::test]
1279    async fn test_open_pipelined_peer_closed() {
1280        let (client, stream) = fidl::endpoints::create_request_stream::<fio::DirectoryMarker>();
1281
1282        fn serve_mock_directory_close_early(mut stream: fio::DirectoryRequestStream) {
1283            fasync::Task::spawn(async move {
1284                if let Some(Ok(request)) = stream.next().await {
1285                    match request {
1286                        fio::DirectoryRequest::Open { object, .. } => {
1287                            // We just drop the object (the server_end of the channel), closing it.
1288                            drop(object);
1289                        }
1290                        _ => {}
1291                    }
1292                }
1293            })
1294            .detach();
1295        }
1296
1297        serve_mock_directory_close_early(stream);
1298
1299        let io = RemoteIo::new(client.into_channel().into());
1300        fasync::unblock(move || {
1301            let results = io
1302                .open_pipelined(
1303                    &["path1", "path2"],
1304                    fio::Flags::empty(),
1305                    fio::NodeAttributesQuery::empty(),
1306                    || DummyFactory,
1307                )
1308                .collect::<Vec<_>>();
1309            assert_eq!(results.len(), 1);
1310            assert_eq!(results[0].as_ref().err(), Some(&zx::Status::PEER_CLOSED));
1311        })
1312        .await;
1313    }
1314}