Skip to main content

starnix_core/vfs/pseudo/
simple_directory.rs

1// Copyright 2025 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
5use crate::task::CurrentTask;
6use crate::vfs::{
7    CloseFreeSafe, DirectoryEntryType, DirentSink, FileObject, FileOps, FileSystemHandle, FsNode,
8    FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString, SymlinkNode, emit_dotdot,
9    fileops_impl_directory, fileops_impl_noop_sync, fileops_impl_unbounded_seek,
10    fs_node_impl_dir_readonly,
11};
12use starnix_sync::{FileOpsCore, Locked, Mutex};
13use starnix_uapi::auth::FsCred;
14use starnix_uapi::device_id::DeviceId;
15use starnix_uapi::errno;
16use starnix_uapi::errors::Errno;
17use starnix_uapi::file_mode::{FileMode, mode};
18use starnix_uapi::open_flags::OpenFlags;
19use std::collections::BTreeMap;
20use std::sync::Arc;
21
22/// Helper used to populate a `SimpleDirectory` with nodes for a specific `FileSystem`.
23pub struct SimpleDirectoryMutator {
24    fs: FileSystemHandle,
25    pub directory: Arc<SimpleDirectory>,
26}
27
28impl SimpleDirectoryMutator {
29    /// Creates a mutator that will allocate nodes in `fs` and insert them into `directory`.
30    pub fn new(fs: FileSystemHandle, directory: Arc<SimpleDirectory>) -> Self {
31        Self { fs, directory }
32    }
33
34    pub fn node(&self, name: FsString, node: FsNodeHandle) {
35        self.directory.entries.lock().insert(name, node);
36    }
37
38    pub fn entry(&self, name: &str, ops: impl Into<Box<dyn FsNodeOps>>, mode: FileMode) {
39        let name: FsString = name.into();
40        let node =
41            self.fs.create_node_and_allocate_node_id(ops, FsNodeInfo::new(mode, FsCred::root()));
42        self.node(name, node);
43    }
44
45    pub fn entry_etc(
46        &self,
47        name: FsString,
48        ops: impl Into<Box<dyn FsNodeOps>>,
49        mode: FileMode,
50        dev: DeviceId,
51        creds: FsCred,
52    ) {
53        let mut info = FsNodeInfo::new(mode, creds);
54        info.rdev = dev;
55        let node = self.fs.create_node_and_allocate_node_id(ops, info);
56        self.node(name, node);
57    }
58
59    pub fn symlink(&self, name: &FsStr, target: &FsStr) {
60        let (ops, info) = SymlinkNode::new(target, FsCred::root());
61        let node = self.fs.create_node_and_allocate_node_id(ops, info);
62        self.node(name.into(), node);
63    }
64
65    pub fn subdir(&self, name: &str, mode: u32, build_subdir: impl FnOnce(&Self)) {
66        let name: &FsStr = name.into();
67        self.subdir2(name, mode, build_subdir);
68    }
69
70    // TODO: Figure out a better way to overload this function for &str and &FsStr.
71    pub fn subdir2(&self, name: &FsStr, mode: u32, build_subdir: impl FnOnce(&Self)) {
72        let dir = self.directory.subdir(&self.fs, name, mode);
73        let mutator = SimpleDirectoryMutator::new(self.fs.clone(), dir);
74        build_subdir(&mutator);
75    }
76
77    pub fn remove(&self, name: &FsStr) {
78        self.directory.remove(name);
79    }
80}
81
82/// Common implementation of a simple read-only directory `FsNodeOps`.
83///
84/// `SimpleDirectoryMutator` is used to populate the directory with child `FsNode`s allocated
85/// in the desired (usually kernel-internal, e.g. "sysfs", "proc", etc) filesystem.
86pub struct SimpleDirectory {
87    entries: Mutex<BTreeMap<FsString, FsNodeHandle>>,
88    not_found_handler:
89        Box<dyn Fn(&FsStr, &BTreeMap<FsString, FsNodeHandle>) -> Errno + Send + Sync + 'static>,
90}
91
92impl SimpleDirectory {
93    /// Returns a new instance with a default handler that returns `ENOENT` and logs context
94    /// when a child is not found.
95    pub fn new() -> Arc<Self> {
96        Self::new_with_handler(|name, locked_entries| {
97            errno!(
98                ENOENT,
99                format!(
100                    "looking for {name} in {:?}",
101                    locked_entries.keys().map(|e| e.to_string()).collect::<Vec<_>>()
102                )
103            )
104        })
105    }
106
107    /// Returns a new instance configured to call the supplied `not_found_handler` whenever
108    /// `FsNodeOps::lookup()` is called for an unknown child path.
109    ///
110    /// The handler is invoked with the `name` of the requested child and a reference to
111    /// the current directory `entries`.
112    pub fn new_with_handler(
113        not_found_handler: impl Fn(&FsStr, &BTreeMap<FsString, FsNodeHandle>) -> Errno
114        + Send
115        + Sync
116        + 'static,
117    ) -> Arc<Self> {
118        let not_found_handler = Box::new(not_found_handler);
119        Arc::new(SimpleDirectory { entries: Default::default(), not_found_handler })
120    }
121
122    pub fn remove(&self, name: &FsStr) {
123        self.entries.lock().remove(name);
124    }
125
126    fn walk<'a>(self: &Arc<Self>, path: &'a FsStr) -> Option<(Arc<Self>, &'a FsStr)> {
127        fn check_component(component: &FsStr) {
128            assert!(!component.is_empty());
129
130            let dot: &FsStr = b".".into();
131            assert_ne!(component, dot);
132
133            let dotdot: &FsStr = b"..".into();
134            assert_ne!(component, dotdot);
135        }
136
137        let mut components = path.split(|c| *c == b'/');
138        let basename = components.next_back()?;
139        let basename: &FsStr = basename.into();
140        check_component(basename);
141        let mut parent = self.clone();
142        while let Some(component) = components.next() {
143            let component: &FsStr = component.into();
144            check_component(component);
145            let Some(next) = parent.get_dir(component) else {
146                return None;
147            };
148            parent = next;
149        }
150        Some((parent, basename))
151    }
152
153    pub fn edit(
154        self: &Arc<Self>,
155        fs: &FileSystemHandle,
156        callback: impl FnOnce(&SimpleDirectoryMutator),
157    ) {
158        let mutator = SimpleDirectoryMutator::new(fs.clone(), self.clone());
159        callback(&mutator);
160    }
161
162    pub fn subdir(&self, fs: &FileSystemHandle, name: &FsStr, mode: u32) -> Arc<SimpleDirectory> {
163        let mut entries = self.entries.lock();
164        if let Some(node) = entries.get(name) {
165            assert!(node.info().mode == mode!(IFDIR, mode));
166            let dir =
167                node.downcast_ops::<Arc<SimpleDirectory>>().expect("subdir is a SimpleDirectory");
168            dir.clone()
169        } else {
170            let dir = SimpleDirectory::new();
171            let info = FsNodeInfo::new(mode!(IFDIR, mode), FsCred::root());
172            let node = fs.create_node_and_allocate_node_id(dir.clone(), info);
173            entries.insert(name.into(), node);
174            dir
175        }
176    }
177
178    fn get(&self, name: &FsStr) -> Option<FsNodeHandle> {
179        let entries = self.entries.lock();
180        entries.get(name).cloned()
181    }
182
183    fn get_dir(&self, name: &FsStr) -> Option<Arc<SimpleDirectory>> {
184        let entries = self.entries.lock();
185        entries
186            .get(name)
187            .and_then(|node| node.downcast_ops::<Arc<SimpleDirectory>>())
188            .map(Arc::clone)
189    }
190
191    pub fn lookup(self: &Arc<Self>, path: &FsStr) -> Option<FsNodeHandle> {
192        let (parent, basename) = self.walk(path)?;
193        parent.get(basename)
194    }
195
196    pub fn into_node(self: Arc<Self>, fs: &FileSystemHandle, mode: u32) -> FsNodeHandle {
197        let info = FsNodeInfo::new(mode!(IFDIR, mode), FsCred::root());
198        fs.create_node_and_allocate_node_id(self, info)
199    }
200}
201
202impl FsNodeOps for Arc<SimpleDirectory> {
203    fs_node_impl_dir_readonly!();
204
205    fn create_file_ops(
206        &self,
207        _locked: &mut Locked<FileOpsCore>,
208        _node: &FsNode,
209        _current_task: &CurrentTask,
210        _flags: OpenFlags,
211    ) -> Result<Box<dyn FileOps>, Errno> {
212        Ok(Box::new(self.clone()))
213    }
214
215    fn lookup(
216        &self,
217        _locked: &mut Locked<FileOpsCore>,
218        _node: &FsNode,
219        _current_task: &CurrentTask,
220        name: &FsStr,
221    ) -> Result<FsNodeHandle, Errno> {
222        let entries = self.entries.lock();
223        entries.get(name).cloned().ok_or_else(|| (self.not_found_handler)(name, &entries))
224    }
225}
226
227/// `SimpleDirectory` doesn't implement the `close` method.
228impl CloseFreeSafe for SimpleDirectory {}
229impl FileOps for SimpleDirectory {
230    fileops_impl_directory!();
231    fileops_impl_noop_sync!();
232    fileops_impl_unbounded_seek!();
233
234    fn readdir(
235        &self,
236        _locked: &mut Locked<FileOpsCore>,
237        file: &FileObject,
238        _current_task: &CurrentTask,
239        sink: &mut dyn DirentSink,
240    ) -> Result<(), Errno> {
241        emit_dotdot(file, sink)?;
242
243        // Skip through the entries until the current offset is reached.
244        // Subtract 2 from the offset to account for `.` and `..`.
245        let entries = self.entries.lock();
246        for (name, node) in entries.iter().skip(sink.offset() as usize - 2) {
247            sink.add(
248                node.ino,
249                sink.offset() + 1,
250                DirectoryEntryType::from_mode(node.info().mode),
251                name.as_ref(),
252            )?;
253        }
254        Ok(())
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::testing::spawn_kernel_and_run;
262    use crate::vfs::FsNodeOps;
263    use starnix_uapi::errno;
264
265    #[fuchsia::test]
266    async fn test_default_not_found_handler() {
267        spawn_kernel_and_run(async |locked, current_task| {
268            let dir = SimpleDirectory::new();
269            let node = dir.clone().into_node(&current_task.fs().root().entry.node.fs(), 0o777);
270            let mut locked = locked.cast_locked();
271            let result =
272                FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "nonexistent".into());
273            assert_eq!(result.unwrap_err(), errno!(ENOENT));
274        })
275        .await;
276    }
277
278    #[fuchsia::test]
279    async fn test_custom_not_found_handler() {
280        spawn_kernel_and_run(async |locked, current_task| {
281            let dir = SimpleDirectory::new_with_handler(|name, _entries| {
282                if name == "special" { errno!(EACCES) } else { errno!(ENOENT) }
283            });
284            let node = dir.clone().into_node(&current_task.fs().root().entry.node.fs(), 0o777);
285
286            let mut locked = locked.cast_locked::<FileOpsCore>();
287            let result_special =
288                FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "special".into());
289            assert_eq!(result_special.unwrap_err(), errno!(EACCES));
290
291            let result_other =
292                FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "other".into());
293            assert_eq!(result_other.unwrap_err(), errno!(ENOENT));
294        })
295        .await;
296    }
297
298    #[fuchsia::test]
299    async fn test_simple_directory_lookups() {
300        spawn_kernel_and_run(async |locked, current_task| {
301            let fs = current_task.fs().root().entry.node.fs();
302            let dir = SimpleDirectory::new();
303            let mutator = SimpleDirectoryMutator::new(fs.clone(), dir.clone());
304
305            // Add a symlink
306            mutator.symlink("link".into(), "target".into());
307
308            // Add a subdir
309            mutator.subdir("subdir", 0o755, |sub_mutator| {
310                sub_mutator.symlink("sublink".into(), "subtarget".into());
311            });
312
313            let node = dir.clone().into_node(&fs, 0o777);
314            let mut locked = locked.cast_locked::<FileOpsCore>();
315
316            // Verify that lookup returns the same FsNodeHandle for multiple calls.
317            let node1 = FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "link".into())
318                .expect("lookup link");
319            let node2 = FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "link".into())
320                .expect("lookup link again");
321
322            assert!(Arc::ptr_eq(&node1, &node2));
323            assert!(node1.info().mode.is_lnk());
324
325            // Verify that lookup returns the same FsNodeHandle for subdirectories.
326            let subdir1 =
327                FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "subdir".into())
328                    .expect("lookup subdir");
329            let subdir2 =
330                FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "subdir".into())
331                    .expect("lookup subdir again");
332
333            assert!(Arc::ptr_eq(&subdir1, &subdir2));
334            assert!(subdir1.info().mode.is_dir());
335
336            // Verify that the SimpleDirectory::lookup helper works for nested paths.
337            let sublink = dir.lookup("subdir/sublink".into()).expect("lookup subdir/sublink");
338            assert!(sublink.info().mode.is_lnk());
339
340            // Verify that removing an entry works.
341            mutator.remove("link".into());
342            let result = FsNodeOps::lookup(&dir, &mut locked, &node, &current_task, "link".into());
343            assert_eq!(result.unwrap_err(), errno!(ENOENT));
344        })
345        .await;
346    }
347}