bp3d_os/dirs/
mod.rs

1// Copyright (c) 2025, BlockProject 3D
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above copyright notice,
11//       this list of conditions and the following disclaimer in the documentation
12//       and/or other materials provided with the distribution.
13//     * Neither the name of BlockProject 3D nor the names of its contributors
14//       may be used to endorse or promote products derived from this software
15//       without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29//! This module provides cross-platform functions to get various system paths.
30
31use std::path::PathBuf;
32use std::sync::OnceLock;
33
34pub use self::path::AppPath;
35
36mod path;
37pub mod system;
38
39//TODO: Remove once once_cell_try feature is stabilized.
40mod sealing {
41    use std::sync::OnceLock;
42
43    pub trait CellExt<T> {
44        fn get_or_try_set<E>(&self, f: impl Fn() -> Result<T, E>) -> Result<&T, E>;
45    }
46
47    impl<T> CellExt<T> for OnceLock<T> {
48        fn get_or_try_set<E>(&self, f: impl Fn() -> Result<T, E>) -> Result<&T, E> {
49            if let Some(value) = self.get() {
50                Ok(value)
51            } else {
52                let value = f()?;
53                Ok(self.get_or_init(|| value))
54            }
55        }
56    }
57}
58
59use sealing::CellExt;
60
61/// Represents the application's directories.
62///
63/// Main entry point to obtain any directory for your application.
64///
65/// These APIs will fail as last resort. If they fail it usually means the system has a problem.
66/// The system may also include specific configuration to break applications on purpose,
67/// in which case these APIs will also fail.
68///
69/// These APIs do not automatically create the directories, instead they return a matching instance of [AppPath](AppPath).
70pub struct App<'a> {
71    name: &'a str,
72    data: OnceLock<PathBuf>,
73    cache: OnceLock<PathBuf>,
74    docs: OnceLock<PathBuf>,
75    logs: OnceLock<PathBuf>,
76    config: OnceLock<PathBuf>,
77}
78
79impl<'a> App<'a> {
80    /// Creates a new application.
81    ///
82    /// # Arguments
83    ///
84    /// * `name`: the name of the application.
85    ///
86    /// returns: App
87    pub fn new(name: &'a str) -> App<'a> {
88        App {
89            name,
90            data: OnceLock::new(),
91            cache: OnceLock::new(),
92            docs: OnceLock::new(),
93            logs: OnceLock::new(),
94            config: OnceLock::new(),
95        }
96    }
97
98    /// Returns the path to this application's files.
99    ///
100    /// Use this directory to store any information not intended to be user accessible.
101    /// Returns None if this system doesn't have any application writable location; this should
102    /// never occur on any supported system except if such system is broken.
103    pub fn get_data(&self) -> Option<AppPath<'_>> {
104        self.data
105            .get_or_try_set(|| system::get_app_data().ok_or(()).map(|v| v.join(self.name)))
106            .ok()
107            .map(|v| v.as_ref())
108            .map(AppPath::new)
109    }
110
111    /// Returns the path to this application's cache.
112    ///
113    /// Use this directory to store cached files such as downloads, intermediate files, etc.
114    ///
115    /// This function first tries to use [get_app_cache](system::get_app_cache)/{APP} and
116    /// falls back to [get_data](App::get_data)/Cache.
117    pub fn get_cache(&self) -> Option<AppPath<'_>> {
118        self.cache
119            .get_or_try_set(|| {
120                system::get_app_cache()
121                    .map(|v| v.join(self.name))
122                    .or_else(|| self.get_data().map(|v| v.join("Cache")))
123                    .ok_or(())
124            })
125            .ok()
126            .map(|v| v.as_ref())
127            .map(AppPath::new)
128    }
129
130    /// Returns the path to this application's public documents.
131    ///
132    /// Use this directory to store any content the user should see and alter.
133    ///
134    /// This function first tries to use [get_app_documents](system::get_app_documents) and
135    /// falls back to [get_data](App::get_data)/Documents.
136    pub fn get_documents(&self) -> Option<AppPath<'_>> {
137        // If this is OK then we must be running from a sandboxed system
138        // where the app has it's own public documents folder, otherwise
139        // create a "public" Documents directory inside the application's data directory.
140        self.docs
141            .get_or_try_set(|| {
142                system::get_app_documents()
143                    .or_else(|| self.get_data().map(|v| v.join("Documents")))
144                    .ok_or(())
145            })
146            .ok()
147            .map(|v| v.as_ref())
148            .map(AppPath::new)
149    }
150
151    /// Returns the path to this application's logs.
152    ///
153    /// Use this directory to store all logs. The user can view and alter this directory.
154    ///
155    /// This function first tries to use [get_app_logs](system::get_app_logs)/{APP} and
156    /// falls back to [get_documents](App::get_documents)/Logs.
157    pub fn get_logs(&self) -> Option<AppPath<'_>> {
158        // Logs should be public and not contain any sensitive information, so store that in
159        // the app's public documents.
160        self.logs
161            .get_or_try_set(|| {
162                system::get_app_logs()
163                    .map(|v| v.join(self.name))
164                    .or_else(|| self.get_documents().map(|v| v.join("Logs")))
165                    .ok_or(())
166            })
167            .ok()
168            .map(|v| v.as_ref())
169            .map(AppPath::new)
170    }
171
172    /// Returns the path to this application's config.
173    ///
174    /// Use this directory to store all configs for the current user.
175    /// This directory is not intended for direct user access.
176    ///
177    /// This function first tries to use [get_app_config](system::get_app_config)/{APP} and
178    /// falls back to [get_data](App::get_data)/Config.
179    pub fn get_config(&self) -> Option<AppPath<'_>> {
180        self.config
181            .get_or_try_set(|| {
182                system::get_app_config()
183                    .map(|v| v.join(self.name))
184                    .or_else(|| self.get_data().map(|v| v.join("Config")))
185                    .ok_or(())
186            })
187            .ok()
188            .map(|v| v.as_ref())
189            .map(AppPath::new)
190    }
191}
192
193impl<'a> Clone for App<'a> {
194    fn clone(&self) -> Self {
195        App {
196            name: self.name,
197            data: self.data.clone(),
198            cache: self.cache.clone(),
199            docs: self.docs.clone(),
200            logs: self.logs.clone(),
201            config: self.config.clone(),
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use std::path::PathBuf;
209
210    use crate::dirs::App;
211
212    fn assert_sync_send<T: Sync + Send>(x: T) -> T {
213        x
214    }
215
216    #[test]
217    fn test_sync_send() {
218        let obj = App::new("test");
219        let _ = assert_sync_send(obj);
220    }
221
222    #[test]
223    fn api_breakage() {
224        let app = App::new("test");
225        let _: Option<PathBuf> = app
226            .get_logs()
227            .map(|v| v.create())
228            .unwrap()
229            .ok()
230            .map(|v| v.into());
231    }
232}