Skip to main content

zerofs_client/
dir.rs

1use crate::error::{ClientResultExt, ZeroFsError};
2use crate::file::File;
3use crate::path::validate_name;
4use crate::session::{FidGuard, Session};
5use crate::types::{DirEntry, FileType, Metadata, NodeKind, OpenOptions, SetAttrs};
6use std::collections::VecDeque;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10/// An open directory: a pull-based listing cursor plus at-style child
11/// operations taking ONE byte-exact name component (no `/` or NUL). The `*_at`
12/// suite is the FFI-clean escape hatch for non-UTF-8 names discovered via
13/// [`DirEntry::name_bytes`]; chain [`Dir::open_dir_at`] to reach arbitrary
14/// depth.
15///
16/// Internally a `Dir` holds two fids, because the server rejects creates on an
17/// already-opened fid: an unopened fid serves the `*_at` operations, and a
18/// lazily opened sibling serves the listing cursor.
19pub struct Dir {
20    session: Arc<Session>,
21    guard: FidGuard,
22    closed: AtomicBool,
23    list: tokio::sync::Mutex<ListState>,
24    path: String,
25}
26
27struct ListState {
28    guard: Option<FidGuard>,
29    /// 9P cookie for the next readdir.
30    cookie: u64,
31    eof: bool,
32    /// Entries fetched but not yet handed out (a `max_entries` smaller than a
33    /// server batch leaves a remainder here).
34    buf: VecDeque<DirEntry>,
35}
36
37impl std::fmt::Debug for Dir {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("Dir")
40            .field("path", &self.path)
41            .field("closed", &self.closed.load(Ordering::Relaxed))
42            .finish_non_exhaustive()
43    }
44}
45
46impl Dir {
47    pub(crate) fn new(session: Arc<Session>, guard: FidGuard, path: String) -> Arc<Self> {
48        Arc::new(Self {
49            session,
50            guard,
51            closed: AtomicBool::new(false),
52            list: tokio::sync::Mutex::new(ListState {
53                guard: None,
54                cookie: 0,
55                eof: false,
56                buf: VecDeque::new(),
57            }),
58            path,
59        })
60    }
61
62    fn check(&self) -> Result<u32, ZeroFsError> {
63        if self.closed.load(Ordering::Acquire) {
64            Err(ZeroFsError::Closed)
65        } else {
66            Ok(self.guard.fid())
67        }
68    }
69
70    fn child_display(&self, name: &[u8]) -> String {
71        let base = self.path.trim_end_matches('/');
72        format!("{base}/{}", String::from_utf8_lossy(name))
73    }
74
75    /// Shared `*_at` preamble: closed check, name validation, display path.
76    fn at(&self, name: &[u8]) -> Result<(u32, String), ZeroFsError> {
77        let fid = self.check()?;
78        let display = self.child_display(name);
79        validate_name(name, &display)?;
80        Ok((fid, display))
81    }
82
83    /// Next batch of entries in directory order; `None` max returns one server
84    /// batch. An empty Vec means end of directory.
85    pub async fn next_batch(&self, max_entries: Option<u32>) -> Result<Vec<DirEntry>, ZeroFsError> {
86        let fid = self.check()?;
87        let mut st = self.list.lock().await;
88
89        if st.guard.is_none() {
90            // Open the listing sibling on a fresh fid (shared op: Tlopenat on
91            // v2, clone + lopen otherwise), leaving the ops fid unopened.
92            let flags = (libc::O_RDONLY | libc::O_DIRECTORY) as u32;
93            let g = self.session.alloc_guard();
94            let guard = match self
95                .session
96                .client
97                .open_clone(fid, g.fid(), flags, None)
98                .await
99            {
100                Ok((_list_fid, _, _)) => g,
101                Err(e) => {
102                    g.discard();
103                    return Err(ZeroFsError::from_client(&e, &self.path));
104                }
105            };
106            st.guard = Some(guard);
107        }
108        let list_fid = st.guard.as_ref().expect("listing fid just ensured").fid();
109
110        // Fetch until at least one entry survives the `.`/`..` filter or EOF.
111        while st.buf.is_empty() && !st.eof {
112            let count = self.session.client.max_io();
113            if self.session.ext_v1() {
114                let entries = self
115                    .session
116                    .client
117                    .readdirplus(list_fid, st.cookie, count)
118                    .await
119                    .ctx(&self.path)?;
120                match entries.last() {
121                    Some(last) => st.cookie = last.offset,
122                    None => st.eof = true,
123                }
124                st.buf.extend(
125                    entries
126                        .iter()
127                        .filter(|e| e.name.data != b"." && e.name.data != b"..")
128                        .map(DirEntry::from_plus),
129                );
130            } else {
131                let entries = self
132                    .session
133                    .client
134                    .readdir(list_fid, st.cookie, count)
135                    .await
136                    .ctx(&self.path)?;
137                match entries.last() {
138                    Some(last) => st.cookie = last.offset,
139                    None => st.eof = true,
140                }
141                st.buf.extend(
142                    entries
143                        .iter()
144                        .filter(|e| e.name.data != b"." && e.name.data != b"..")
145                        .map(DirEntry::from_plain),
146                );
147            }
148        }
149
150        let want = max_entries.map_or(usize::MAX, |n| n as usize);
151        let take = want.min(st.buf.len());
152        Ok(st.buf.drain(..take).collect())
153    }
154
155    /// A [`futures_core::Stream`] of this directory's entries, yielding them one
156    /// at a time (fetched a server batch at a time). Shares this `Dir`'s listing
157    /// cursor, so consuming the stream advances the same position as
158    /// [`Self::next_batch`]. Rust-only sugar (`stream` feature); never crosses FFI.
159    #[cfg(feature = "stream")]
160    pub fn entries(self: &Arc<Dir>) -> crate::stream::DirStream {
161        crate::stream::DirStream::new(Arc::clone(self))
162    }
163
164    /// Restart iteration from the first entry.
165    pub async fn rewind(&self) -> Result<(), ZeroFsError> {
166        self.check()?;
167        let mut st = self.list.lock().await;
168        st.cookie = 0;
169        st.eof = false;
170        st.buf.clear();
171        Ok(())
172    }
173
174    /// Metadata for the directory itself.
175    pub async fn metadata(&self) -> Result<Metadata, ZeroFsError> {
176        let fid = self.check()?;
177        let stat = self.session.stat_fid(fid, &self.path).await?;
178        Ok(Metadata::from_stat(&stat))
179    }
180
181    /// Apply metadata changes to the directory itself (chmod/chown/utimens).
182    pub async fn set_attr(&self, attrs: SetAttrs) -> Result<Metadata, ZeroFsError> {
183        let fid = self.check()?;
184        let stat = self.session.setattr_fid(fid, &attrs, &self.path).await?;
185        Ok(Metadata::from_stat(&stat))
186    }
187
188    /// openat(2)-alike: open (and optionally create) a child file.
189    pub async fn open_at(&self, name: &[u8], opts: OpenOptions) -> Result<Arc<File>, ZeroFsError> {
190        let (fid, display) = self.at(name)?;
191        let guard = self
192            .session
193            .open_relative(fid, name, &opts, &display)
194            .await?;
195        Ok(File::new(Arc::clone(&self.session), guard, display))
196    }
197
198    /// Open a child directory (descend without UTF-8).
199    pub async fn open_dir_at(&self, name: &[u8]) -> Result<Arc<Dir>, ZeroFsError> {
200        let (fid, display) = self.at(name)?;
201        let (guard, stat) = self.session.walk_from(fid, &[name], &display).await?;
202        if let Some(stat) = &stat
203            && FileType::from_mode(stat.mode) != FileType::Dir
204        {
205            return Err(ZeroFsError::NotADirectory { path: display });
206        }
207        Ok(Dir::new(Arc::clone(&self.session), guard, display))
208    }
209
210    /// fstatat(2)-alike; never follows symlinks.
211    pub async fn metadata_at(&self, name: &[u8]) -> Result<Metadata, ZeroFsError> {
212        let (fid, display) = self.at(name)?;
213        let (_guard, stat) = self.session.walk_stat_from(fid, &[name], &display).await?;
214        Ok(Metadata::from_stat(&stat))
215    }
216
217    /// Apply metadata changes to a child without opening it (works on
218    /// symlinks, fifos, and non-UTF-8 names).
219    pub async fn set_attr_at(&self, name: &[u8], attrs: SetAttrs) -> Result<Metadata, ZeroFsError> {
220        let (fid, display) = self.at(name)?;
221        let (guard, _) = self.session.walk_from(fid, &[name], &display).await?;
222        let stat = self
223            .session
224            .setattr_fid(guard.fid(), &attrs, &display)
225            .await?;
226        Ok(Metadata::from_stat(&stat))
227    }
228
229    /// mkdirat(2) with explicit mode; returns the new directory's metadata.
230    pub async fn create_dir_at(&self, name: &[u8], mode: u32) -> Result<Metadata, ZeroFsError> {
231        let (fid, display) = self.at(name)?;
232        self.session.mkdir_at(fid, name, mode, &display).await
233    }
234
235    /// symlinkat(2): create child `name` containing raw byte `target` verbatim.
236    pub async fn symlink_at(&self, name: &[u8], target: &[u8]) -> Result<Metadata, ZeroFsError> {
237        let (fid, display) = self.at(name)?;
238        self.session.symlink_at(fid, name, target, &display).await
239    }
240
241    /// linkat(2): hard-link `original_dir`/`original_name` (any file type) as
242    /// `self`/`new_name`; returns metadata with the updated nlink.
243    pub async fn link_at(
244        &self,
245        original_dir: &Dir,
246        original_name: &[u8],
247        new_name: &[u8],
248    ) -> Result<Metadata, ZeroFsError> {
249        let (fid, display) = self.at(new_name)?;
250        let (original_fid, original_display) = original_dir.at(original_name)?;
251
252        let (target_guard, _) = self
253            .session
254            .walk_from(original_fid, &[original_name], &original_display)
255            .await?;
256        self.session
257            .link_at(fid, target_guard.fid(), new_name, &display)
258            .await
259    }
260
261    /// mknodat(2): create a fifo, socket, or device node child.
262    pub async fn mknod_at(
263        &self,
264        name: &[u8],
265        kind: NodeKind,
266        mode: u32,
267    ) -> Result<Metadata, ZeroFsError> {
268        let (fid, display) = self.at(name)?;
269        self.session.mknod_at(fid, name, kind, mode, &display).await
270    }
271
272    /// unlinkat(2).
273    pub async fn remove_file_at(&self, name: &[u8]) -> Result<(), ZeroFsError> {
274        let (fid, display) = self.at(name)?;
275        self.session
276            .client
277            .unlinkat(fid, name, 0)
278            .await
279            .ctx(&display)
280    }
281
282    /// unlinkat(2) with AT_REMOVEDIR.
283    pub async fn remove_dir_at(&self, name: &[u8]) -> Result<(), ZeroFsError> {
284        let (fid, display) = self.at(name)?;
285        self.session
286            .client
287            .unlinkat(fid, name, libc::AT_REMOVEDIR as u32)
288            .await
289            .ctx(&display)
290    }
291
292    /// renameat(2) across two open directories (`new_dir` may be `self`).
293    pub async fn rename_at(
294        &self,
295        old_name: &[u8],
296        new_dir: &Dir,
297        new_name: &[u8],
298    ) -> Result<(), ZeroFsError> {
299        let (fid, old_display) = self.at(old_name)?;
300        let (new_fid, _) = new_dir.at(new_name)?;
301        self.session
302            .client
303            .renameat(fid, old_name, new_fid, new_name)
304            .await
305            .ctx(&old_display)
306    }
307
308    /// readlinkat(2): raw target bytes.
309    pub async fn read_link_at(&self, name: &[u8]) -> Result<Vec<u8>, ZeroFsError> {
310        let (fid, display) = self.at(name)?;
311        let (guard, _) = self.session.walk_from(fid, &[name], &display).await?;
312        self.session
313            .client
314            .readlink(guard.fid())
315            .await
316            .ctx(&display)
317    }
318
319    /// Mark the handle closed (later calls return `Closed`). Both fids are
320    /// clunked and their numbers recycled when the handle is dropped. Always
321    /// succeeds; idempotent; never blocks.
322    pub async fn close(&self) {
323        // Mark closed so later calls return `Closed`. Both fids (ops + listing)
324        // are clunked and their numbers recycled when the handle is dropped.
325        self.closed.store(true, Ordering::Release);
326    }
327}