pseudo_fs/
lazy_pseudo_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::PseudoDirectory;
6use fidl_fuchsia_io as fio;
7use fuchsia_sync::{MappedMutexGuard, Mutex, MutexGuard};
8use std::sync::Arc;
9use vfs::directory::entry::{
10    DirectoryEntry, DirectoryEntryAsync, EntryInfo, GetEntryInfo, OpenRequest,
11};
12use zx_status::Status;
13
14pub trait ToPseudoDirectory: Send + 'static {
15    /// Constructs the `PseudoDirectory` the first time a request for the directory is received.
16    ///
17    /// The returned directory must not have an inode number.
18    fn to_pseudo_directory(self) -> Arc<PseudoDirectory>;
19}
20
21/// A pseudo directory that delays constructing a `PseudoDirectory` until a request is received.
22///
23/// The intended purpose of `LazyPseudoDirectory` is to save memory when presenting data in a
24/// filesystem structure for debug purposes. The directory should not be accessed during regular
25/// system usage otherwise no memory savings will occur.
26pub struct LazyPseudoDirectory<T>(Mutex<Inner<T>>);
27
28impl<T: ToPseudoDirectory> LazyPseudoDirectory<T> {
29    pub fn new(data: T) -> Arc<Self> {
30        Arc::new(Self(Mutex::new(Inner::Data(data))))
31    }
32
33    /// Retrieves either the backing data or the directory depending on whether the directory has
34    /// been accessed yet.
35    pub fn state(&self) -> LazyPseudoDirectoryState<'_, T> {
36        let inner = self.0.lock();
37        match &*inner {
38            Inner::Data(_) => LazyPseudoDirectoryState::Data(MutexGuard::map(inner, |inner| {
39                let Inner::Data(data) = inner else { unreachable!() };
40                data
41            })),
42            Inner::Directory(dir) => LazyPseudoDirectoryState::Directory(dir.clone()),
43            Inner::Intermediate => unreachable!(),
44        }
45    }
46}
47
48pub enum LazyPseudoDirectoryState<'a, T> {
49    Data(MappedMutexGuard<'a, T>),
50    Directory(Arc<PseudoDirectory>),
51}
52
53impl<T> LazyPseudoDirectoryState<'_, T> {
54    pub fn is_data(&self) -> bool {
55        match self {
56            Self::Data(_) => true,
57            _ => false,
58        }
59    }
60
61    pub fn is_directory(&self) -> bool {
62        match self {
63            Self::Directory(_) => true,
64            _ => false,
65        }
66    }
67}
68
69enum Inner<T> {
70    Data(T),
71    Directory(Arc<PseudoDirectory>),
72
73    /// An intermediate state used when converting from `Data` to `Directory`. A lock on `Inner` is
74    /// held during the transition so this state is never be externally observable.
75    Intermediate,
76}
77
78impl<T: ToPseudoDirectory> Inner<T> {
79    fn get_or_init_directory(&mut self) -> Arc<PseudoDirectory> {
80        if let Self::Directory(dir) = self {
81            return dir.clone();
82        }
83
84        let Self::Data(data) = std::mem::replace(self, Self::Intermediate) else {
85            unreachable!();
86        };
87        let dir = data.to_pseudo_directory();
88        *self = Self::Directory(dir.clone());
89
90        // Requiring that the directory does not have an inode number avoids creating the directory
91        // when responding to `GetEntryInfo::entry_info` requests.
92        debug_assert!(
93            dir.entry_info().inode() == fio::INO_UNKNOWN,
94            "The directory must not have an inode number"
95        );
96        dir
97    }
98}
99
100impl<T> GetEntryInfo for LazyPseudoDirectory<T> {
101    fn entry_info(&self) -> EntryInfo {
102        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
103    }
104}
105
106impl<T: ToPseudoDirectory> DirectoryEntry for LazyPseudoDirectory<T> {
107    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
108        let mut this = self.0.lock();
109        if let Inner::Directory(dir) = &*this {
110            return dir.clone().open_entry(request);
111        }
112        if request.requires_event() || !request.path().is_empty() {
113            this.get_or_init_directory().open_entry(request)
114        } else {
115            std::mem::drop(this);
116            request.spawn(self);
117            Ok(())
118        }
119    }
120}
121
122impl<T: ToPseudoDirectory> DirectoryEntryAsync for LazyPseudoDirectory<T> {
123    async fn open_entry_async(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
124        if !request.wait_till_ready().await {
125            // The channel was closed before any request was received.
126            return Ok(());
127        }
128        let mut this = self.0.lock();
129        this.get_or_init_directory().open_entry(request)
130    }
131}
132
133#[cfg(all(test))]
134mod tests {
135    use super::*;
136    use crate::PseudoFile;
137    use fidl::endpoints::create_proxy;
138    use vfs::directory::helper::DirectlyMutable;
139    use vfs::{ExecutionScope, Path, ToObjectRequest};
140
141    #[cfg(target_os = "fuchsia")]
142    use fuchsia_async::TestExecutor;
143
144    struct MockData;
145
146    fn open(
147        dir: Arc<LazyPseudoDirectory<MockData>>,
148        flags: fio::Flags,
149        path: Path,
150    ) -> fio::DirectoryProxy {
151        let (client, server) = create_proxy::<fio::DirectoryMarker>();
152        flags
153            .to_object_request(server)
154            .handle(|object_request| {
155                dir.open_entry(OpenRequest::new(
156                    ExecutionScope::new(),
157                    flags,
158                    path,
159                    object_request,
160                ))
161                .unwrap();
162                Ok(())
163            })
164            .unwrap();
165        client
166    }
167
168    #[cfg(target_os = "fuchsia")]
169    fn run_ready_tasks(executor: &mut TestExecutor) {
170        let _ = executor.run_until_stalled(&mut std::future::pending::<()>());
171    }
172
173    impl ToPseudoDirectory for MockData {
174        fn to_pseudo_directory(self) -> Arc<PseudoDirectory> {
175            let inner = PseudoDirectory::new();
176            inner.add_entry("file", PseudoFile::from_data("1234")).unwrap();
177            let dir = PseudoDirectory::new();
178            dir.add_entry("inner", inner).unwrap();
179            dir
180        }
181    }
182
183    #[cfg(target_os = "fuchsia")]
184    #[fuchsia::test]
185    fn test_open_entry_with_no_request_does_not_create_directory() {
186        let mut exec = TestExecutor::new();
187        let lazy_dir = LazyPseudoDirectory::new(MockData);
188        let _client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
189        run_ready_tasks(&mut exec);
190        assert!(lazy_dir.state().is_data());
191    }
192
193    #[cfg(target_os = "fuchsia")]
194    #[fuchsia::test]
195    fn test_open_entry_with_representation_creates_directory() {
196        let mut exec = TestExecutor::new();
197        let lazy_dir = LazyPseudoDirectory::new(MockData);
198        let _client = open(
199            lazy_dir.clone(),
200            fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
201            Path::dot(),
202        );
203        run_ready_tasks(&mut exec);
204        assert!(lazy_dir.state().is_directory());
205    }
206
207    #[cfg(target_os = "fuchsia")]
208    #[fuchsia::test]
209    fn test_open_entry_with_path_creates_directory() {
210        let mut exec = TestExecutor::new();
211        let lazy_dir = LazyPseudoDirectory::new(MockData);
212        let _client = open(lazy_dir.clone(), fio::PERM_READABLE, "inner".try_into().unwrap());
213        run_ready_tasks(&mut exec);
214        assert!(lazy_dir.state().is_directory());
215    }
216
217    #[fuchsia::test]
218    async fn test_create_directory_on_request() {
219        let lazy_dir = LazyPseudoDirectory::new(MockData);
220        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
221        assert!(lazy_dir.state().is_data());
222        client.get_flags().await.unwrap().unwrap();
223        assert!(lazy_dir.state().is_directory());
224    }
225
226    #[cfg(target_os = "fuchsia")]
227    #[fuchsia::test]
228    fn test_peer_closed_does_not_create_directory() {
229        let mut exec = TestExecutor::new();
230        let lazy_dir = LazyPseudoDirectory::new(MockData);
231        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
232        assert!(lazy_dir.state().is_data());
233
234        // Close the channel and wait for the peer closed signal to be received.
235        std::mem::drop(client);
236        run_ready_tasks(&mut exec);
237
238        assert!(lazy_dir.state().is_data());
239    }
240
241    #[fuchsia::test]
242    async fn test_read_inner_file() {
243        let lazy_dir = LazyPseudoDirectory::new(MockData);
244        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
245        assert_eq!(
246            fuchsia_fs::directory::read_file_to_string(&client, "inner/file")
247                .await
248                .expect("failed to read file"),
249            "1234"
250        );
251    }
252}