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