Skip to main content

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 futures::lock::Mutex as AsyncMutex;
9use std::sync::Arc;
10use vfs::directory::entry::{
11    DirectoryEntry, DirectoryEntryAsync, EntryInfo, GetEntryInfo, OpenRequest,
12};
13use zx_status::Status;
14
15pub trait ToPseudoDirectory: Send + 'static {
16    /// Constructs the `PseudoDirectory` the first time a request for the directory is received.
17    ///
18    /// The returned directory must not have an inode number.
19    fn to_pseudo_directory(self) -> Arc<PseudoDirectory>;
20}
21
22pub trait ToPseudoDirectoryAsync: Send + 'static {
23    /// Constructs the `PseudoDirectory` the first time a request for the directory is received.
24    ///
25    /// The returned directory must not have an inode number.
26    fn to_pseudo_directory(self) -> impl Future<Output = Arc<PseudoDirectory>> + Send;
27}
28
29/// A pseudo directory that delays constructing a `PseudoDirectory` until a request is received.
30///
31/// The intended purpose of `LazyPseudoDirectory` is to save memory when presenting data in a
32/// filesystem structure for debug purposes. The directory should not be accessed during regular
33/// system usage otherwise no memory savings will occur.
34pub struct LazyPseudoDirectory<T>(Mutex<Inner<T>>);
35
36impl<T: ToPseudoDirectory> LazyPseudoDirectory<T> {
37    pub fn new(data: T) -> Arc<Self> {
38        Arc::new(Self(Mutex::new(Inner::Data(data))))
39    }
40
41    /// Retrieves either the backing data or the directory depending on whether the directory has
42    /// been accessed yet.
43    pub fn state(&self) -> LazyPseudoDirectoryState<'_, T> {
44        let inner = self.0.lock();
45        match &*inner {
46            Inner::Data(_) => LazyPseudoDirectoryState::Data(MutexGuard::map(inner, |inner| {
47                let Inner::Data(data) = inner else { unreachable!() };
48                data
49            })),
50            Inner::Directory(dir) => LazyPseudoDirectoryState::Directory(dir.clone()),
51            Inner::Intermediate => unreachable!(),
52        }
53    }
54}
55
56pub enum LazyPseudoDirectoryState<'a, T> {
57    Data(MappedMutexGuard<'a, T>),
58    Directory(Arc<PseudoDirectory>),
59}
60
61impl<T> LazyPseudoDirectoryState<'_, T> {
62    pub fn is_data(&self) -> bool {
63        match self {
64            Self::Data(_) => true,
65            _ => false,
66        }
67    }
68
69    pub fn is_directory(&self) -> bool {
70        match self {
71            Self::Directory(_) => true,
72            _ => false,
73        }
74    }
75}
76
77enum Inner<T> {
78    Data(T),
79    Directory(Arc<PseudoDirectory>),
80
81    /// An intermediate state used when converting from `Data` to `Directory`. A lock on `Inner` is
82    /// held during the transition so this state is never be externally observable.
83    Intermediate,
84}
85
86impl<T: ToPseudoDirectory> Inner<T> {
87    fn get_or_init_directory(&mut self) -> Arc<PseudoDirectory> {
88        if let Self::Directory(dir) = self {
89            return dir.clone();
90        }
91
92        let Self::Data(data) = std::mem::replace(self, Self::Intermediate) else {
93            unreachable!();
94        };
95        let dir = data.to_pseudo_directory();
96        *self = Self::Directory(dir.clone());
97
98        // Requiring that the directory does not have an inode number avoids creating the directory
99        // when responding to `GetEntryInfo::entry_info` requests.
100        debug_assert!(
101            dir.entry_info().inode() == fio::INO_UNKNOWN,
102            "The directory must not have an inode number"
103        );
104        dir
105    }
106}
107
108impl<T> GetEntryInfo for LazyPseudoDirectory<T> {
109    fn entry_info(&self) -> EntryInfo {
110        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
111    }
112}
113
114impl<T: ToPseudoDirectory> DirectoryEntry for LazyPseudoDirectory<T> {
115    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
116        let mut this = self.0.lock();
117        if let Inner::Directory(dir) = &*this {
118            return dir.clone().open_entry(request);
119        }
120        if request.requires_event() || !request.path().is_empty() {
121            this.get_or_init_directory().open_entry(request)
122        } else {
123            std::mem::drop(this);
124            request.spawn(self);
125            Ok(())
126        }
127    }
128}
129
130impl<T: ToPseudoDirectory> DirectoryEntryAsync for LazyPseudoDirectory<T> {
131    async fn open_entry_async(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
132        if !request.wait_till_ready().await {
133            // The channel was closed before any request was received.
134            return Ok(());
135        }
136        let mut this = self.0.lock();
137        this.get_or_init_directory().open_entry(request)
138    }
139}
140
141/// A pseudo directory that delays constructing a `PseudoDirectory` until a request is received.
142///
143/// The intended purpose of `LazyPseudoDirectoryAsync` is to save memory when presenting data in a
144/// filesystem structure for debug purposes. The directory should not be accessed during regular
145/// system usage otherwise no memory savings will occur.
146///
147/// This is functionally identical to [`LazyPseudoDirectory`], except it is compatible with
148/// [`ToPseudoDirectoryAsync`] (i.e. directories with asynchronous initializers).  As a tradeoff,
149/// any time the contents are accessed, an async lock must be acquired, which will be less
150/// performant.  As such, this should only be used when needed.
151pub struct LazyPseudoDirectoryAsync<T>(AsyncMutex<InnerAsync<T>>);
152
153enum InnerAsync<T> {
154    Data(Option<T>),
155    Directory(Arc<PseudoDirectory>),
156}
157
158impl<T: ToPseudoDirectoryAsync> LazyPseudoDirectoryAsync<T> {
159    pub fn new(data: T) -> Arc<Self> {
160        Arc::new(Self(AsyncMutex::new(InnerAsync::Data(Some(data)))))
161    }
162
163    pub async fn is_data(&self) -> bool {
164        let inner = self.0.lock().await;
165        matches!(&*inner, InnerAsync::Data(_))
166    }
167
168    pub async fn is_directory(&self) -> bool {
169        let inner = self.0.lock().await;
170        matches!(&*inner, InnerAsync::Directory(_))
171    }
172}
173
174impl<T> GetEntryInfo for LazyPseudoDirectoryAsync<T> {
175    fn entry_info(&self) -> EntryInfo {
176        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
177    }
178}
179
180impl<T: ToPseudoDirectoryAsync> DirectoryEntry for LazyPseudoDirectoryAsync<T> {
181    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
182        if let Some(this) = self.0.try_lock() {
183            if let InnerAsync::Directory(dir) = &*this {
184                return dir.clone().open_entry(request);
185            }
186        }
187        request.spawn(self);
188        Ok(())
189    }
190}
191
192impl<T: ToPseudoDirectoryAsync> DirectoryEntryAsync for LazyPseudoDirectoryAsync<T> {
193    async fn open_entry_async(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
194        if !request.wait_till_ready().await {
195            return Ok(());
196        }
197
198        let mut this = self.0.lock().await;
199
200        let dir = match &mut *this {
201            InnerAsync::Directory(dir) => dir.clone(),
202            InnerAsync::Data(opt) => {
203                let data = opt.take().expect("Data already taken");
204                let dir = data.to_pseudo_directory().await;
205                debug_assert!(
206                    dir.entry_info().inode() == fio::INO_UNKNOWN,
207                    "The directory must not have an inode number"
208                );
209                *this = InnerAsync::Directory(dir.clone());
210                dir
211            }
212        };
213
214        dir.open_entry(request)
215    }
216}
217
218#[cfg(all(test))]
219mod tests {
220    use super::*;
221    use crate::PseudoFile;
222    use fidl::endpoints::create_proxy;
223    use vfs::directory::helper::DirectlyMutable;
224    use vfs::{ExecutionScope, Path, ToObjectRequest};
225
226    #[cfg(target_os = "fuchsia")]
227    use fuchsia_async::TestExecutor;
228
229    struct MockData;
230
231    fn open(
232        dir: Arc<LazyPseudoDirectory<MockData>>,
233        flags: fio::Flags,
234        path: Path,
235    ) -> fio::DirectoryProxy {
236        let (client, server) = create_proxy::<fio::DirectoryMarker>();
237        flags
238            .to_object_request(server)
239            .handle(|object_request| {
240                dir.open_entry(OpenRequest::new(
241                    ExecutionScope::new(),
242                    flags,
243                    path,
244                    object_request,
245                ))
246                .unwrap();
247                Ok(())
248            })
249            .unwrap();
250        client
251    }
252
253    #[cfg(target_os = "fuchsia")]
254    fn run_ready_tasks(executor: &mut TestExecutor) {
255        let _ = executor.run_until_stalled(&mut std::future::pending::<()>());
256    }
257
258    impl ToPseudoDirectory for MockData {
259        fn to_pseudo_directory(self) -> Arc<PseudoDirectory> {
260            let inner = PseudoDirectory::new();
261            inner.add_entry("file", PseudoFile::from_data("1234")).unwrap();
262            let dir = PseudoDirectory::new();
263            dir.add_entry("inner", inner).unwrap();
264            dir
265        }
266    }
267
268    #[cfg(target_os = "fuchsia")]
269    #[fuchsia::test]
270    fn test_open_entry_with_no_request_does_not_create_directory() {
271        let mut exec = TestExecutor::new();
272        let lazy_dir = LazyPseudoDirectory::new(MockData);
273        let _client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
274        run_ready_tasks(&mut exec);
275        assert!(lazy_dir.state().is_data());
276    }
277
278    #[cfg(target_os = "fuchsia")]
279    #[fuchsia::test]
280    fn test_open_entry_with_representation_creates_directory() {
281        let mut exec = TestExecutor::new();
282        let lazy_dir = LazyPseudoDirectory::new(MockData);
283        let _client = open(
284            lazy_dir.clone(),
285            fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
286            Path::dot(),
287        );
288        run_ready_tasks(&mut exec);
289        assert!(lazy_dir.state().is_directory());
290    }
291
292    #[cfg(target_os = "fuchsia")]
293    #[fuchsia::test]
294    fn test_open_entry_with_path_creates_directory() {
295        let mut exec = TestExecutor::new();
296        let lazy_dir = LazyPseudoDirectory::new(MockData);
297        let _client = open(lazy_dir.clone(), fio::PERM_READABLE, "inner".try_into().unwrap());
298        run_ready_tasks(&mut exec);
299        assert!(lazy_dir.state().is_directory());
300    }
301
302    #[fuchsia::test]
303    async fn test_create_directory_on_request() {
304        let lazy_dir = LazyPseudoDirectory::new(MockData);
305        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
306        assert!(lazy_dir.state().is_data());
307        client.get_flags().await.unwrap().unwrap();
308        assert!(lazy_dir.state().is_directory());
309    }
310
311    #[cfg(target_os = "fuchsia")]
312    #[fuchsia::test]
313    fn test_peer_closed_does_not_create_directory() {
314        let mut exec = TestExecutor::new();
315        let lazy_dir = LazyPseudoDirectory::new(MockData);
316        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
317        assert!(lazy_dir.state().is_data());
318
319        // Close the channel and wait for the peer closed signal to be received.
320        std::mem::drop(client);
321        run_ready_tasks(&mut exec);
322
323        assert!(lazy_dir.state().is_data());
324    }
325
326    #[fuchsia::test]
327    async fn test_read_inner_file() {
328        let lazy_dir = LazyPseudoDirectory::new(MockData);
329        let client = open(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
330        assert_eq!(
331            fuchsia_fs::directory::read_file_to_string(&client, "inner/file")
332                .await
333                .expect("failed to read file"),
334            "1234"
335        );
336    }
337    impl ToPseudoDirectoryAsync for MockData {
338        async fn to_pseudo_directory(self) -> Arc<PseudoDirectory> {
339            let inner = PseudoDirectory::new();
340            inner.add_entry("file", PseudoFile::from_data("1234")).unwrap();
341            let dir = PseudoDirectory::new();
342            dir.add_entry("inner", inner).unwrap();
343            dir
344        }
345    }
346
347    fn open_async_dir(
348        dir: Arc<LazyPseudoDirectoryAsync<MockData>>,
349        flags: fio::Flags,
350        path: Path,
351    ) -> fio::DirectoryProxy {
352        let (client, server) = create_proxy::<fio::DirectoryMarker>();
353        flags
354            .to_object_request(server)
355            .handle(|object_request| {
356                dir.open_entry(OpenRequest::new(
357                    ExecutionScope::new(),
358                    flags,
359                    path,
360                    object_request,
361                ))
362                .unwrap();
363                Ok(())
364            })
365            .unwrap();
366        client
367    }
368
369    #[fuchsia::test]
370    async fn test_async_create_directory_on_request() {
371        let lazy_dir = LazyPseudoDirectoryAsync::new(MockData);
372        let client = open_async_dir(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
373        assert!(lazy_dir.is_data().await);
374        client.get_flags().await.unwrap().unwrap();
375        assert!(lazy_dir.is_directory().await);
376    }
377
378    #[fuchsia::test]
379    async fn test_async_read_inner_file() {
380        let lazy_dir = LazyPseudoDirectoryAsync::new(MockData);
381        let client = open_async_dir(lazy_dir.clone(), fio::PERM_READABLE, Path::dot());
382        assert_eq!(
383            fuchsia_fs::directory::read_file_to_string(&client, "inner/file")
384                .await
385                .expect("failed to read file"),
386            "1234"
387        );
388    }
389}