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