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}