1use std::{
6 path::{Component, Display, Path, PathBuf},
7 str::FromStr,
8};
9
10use crate::Runtime;
11
12use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14
15pub(crate) mod plugin;
16
17use crate::error::*;
18
19#[cfg(target_os = "android")]
20mod android;
21#[cfg(not(target_os = "android"))]
22mod desktop;
23
24#[cfg(target_os = "android")]
25pub use android::PathResolver;
26#[cfg(not(target_os = "android"))]
27pub use desktop::PathResolver;
28
29#[derive(Clone, Debug, Serialize)]
42pub struct SafePathBuf(PathBuf);
43
44impl SafePathBuf {
45 pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
47 if path.components().any(|x| matches!(x, Component::ParentDir)) {
48 Err("cannot traverse directory, rewrite the path without the use of `../`")
49 } else {
50 Ok(Self(path))
51 }
52 }
53
54 pub fn display(&self) -> Display<'_> {
58 self.0.display()
59 }
60}
61
62impl AsRef<Path> for SafePathBuf {
63 fn as_ref(&self) -> &Path {
64 self.0.as_ref()
65 }
66}
67
68impl FromStr for SafePathBuf {
69 type Err = &'static str;
70
71 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
72 Self::new(s.into())
73 }
74}
75
76impl From<SafePathBuf> for PathBuf {
77 fn from(path: SafePathBuf) -> Self {
78 path.0
79 }
80}
81
82impl<'de> Deserialize<'de> for SafePathBuf {
83 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
84 where
85 D: Deserializer<'de>,
86 {
87 let path = PathBuf::deserialize(deserializer)?;
88 SafePathBuf::new(path).map_err(DeError::custom)
89 }
90}
91
92#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
99#[repr(u16)]
100#[non_exhaustive]
101pub enum BaseDirectory {
102 Audio = 1,
105 Cache = 2,
108 Config = 3,
111 Data = 4,
114 LocalData = 5,
117 Document = 6,
120 Download = 7,
123 Picture = 8,
126 Public = 9,
129 Video = 10,
132 Resource = 11,
135 Temp = 12,
138 AppConfig = 13,
141 AppData = 14,
144 AppLocalData = 15,
147 AppCache = 16,
150 AppLog = 17,
154 #[cfg(not(target_os = "android"))]
157 Desktop = 18,
158 #[cfg(not(target_os = "android"))]
161 Executable = 19,
162 #[cfg(not(target_os = "android"))]
165 Font = 20,
166 Home = 21,
169 #[cfg(not(target_os = "android"))]
172 Runtime = 22,
173 #[cfg(not(target_os = "android"))]
176 Template = 23,
177}
178
179impl BaseDirectory {
180 pub fn variable(self) -> &'static str {
182 match self {
183 Self::Audio => "$AUDIO",
184 Self::Cache => "$CACHE",
185 Self::Config => "$CONFIG",
186 Self::Data => "$DATA",
187 Self::LocalData => "$LOCALDATA",
188 Self::Document => "$DOCUMENT",
189 Self::Download => "$DOWNLOAD",
190 Self::Picture => "$PICTURE",
191 Self::Public => "$PUBLIC",
192 Self::Video => "$VIDEO",
193 Self::Resource => "$RESOURCE",
194 Self::Temp => "$TEMP",
195 Self::AppConfig => "$APPCONFIG",
196 Self::AppData => "$APPDATA",
197 Self::AppLocalData => "$APPLOCALDATA",
198 Self::AppCache => "$APPCACHE",
199 Self::AppLog => "$APPLOG",
200 Self::Home => "$HOME",
201
202 #[cfg(not(target_os = "android"))]
203 Self::Desktop => "$DESKTOP",
204 #[cfg(not(target_os = "android"))]
205 Self::Executable => "$EXE",
206 #[cfg(not(target_os = "android"))]
207 Self::Font => "$FONT",
208 #[cfg(not(target_os = "android"))]
209 Self::Runtime => "$RUNTIME",
210 #[cfg(not(target_os = "android"))]
211 Self::Template => "$TEMPLATE",
212 }
213 }
214
215 pub fn from_variable(variable: &str) -> Option<Self> {
217 let res = match variable {
218 "$AUDIO" => Self::Audio,
219 "$CACHE" => Self::Cache,
220 "$CONFIG" => Self::Config,
221 "$DATA" => Self::Data,
222 "$LOCALDATA" => Self::LocalData,
223 "$DOCUMENT" => Self::Document,
224 "$DOWNLOAD" => Self::Download,
225
226 "$PICTURE" => Self::Picture,
227 "$PUBLIC" => Self::Public,
228 "$VIDEO" => Self::Video,
229 "$RESOURCE" => Self::Resource,
230 "$TEMP" => Self::Temp,
231 "$APPCONFIG" => Self::AppConfig,
232 "$APPDATA" => Self::AppData,
233 "$APPLOCALDATA" => Self::AppLocalData,
234 "$APPCACHE" => Self::AppCache,
235 "$APPLOG" => Self::AppLog,
236 "$HOME" => Self::Home,
237
238 #[cfg(not(target_os = "android"))]
239 "$DESKTOP" => Self::Desktop,
240 #[cfg(not(target_os = "android"))]
241 "$EXE" => Self::Executable,
242 #[cfg(not(target_os = "android"))]
243 "$FONT" => Self::Font,
244 #[cfg(not(target_os = "android"))]
245 "$RUNTIME" => Self::Runtime,
246 #[cfg(not(target_os = "android"))]
247 "$TEMPLATE" => Self::Template,
248
249 _ => return None,
250 };
251 Some(res)
252 }
253}
254
255impl<R: Runtime> PathResolver<R> {
256 pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
270 resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
271 }
272
273 pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
287 let mut p = PathBuf::new();
288 let mut components = path.as_ref().components();
289 match components.next() {
290 Some(Component::Normal(str)) => {
291 if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
292 p.push(resolve_path::<R>(self, base_directory, None)?);
293 } else {
294 p.push(str);
295 }
296 }
297 Some(component) => p.push(component),
298 None => (),
299 }
300
301 for component in components {
302 if let Component::ParentDir = component {
303 continue;
304 }
305 p.push(component);
306 }
307
308 Ok(p)
309 }
310}
311
312fn resolve_path<R: Runtime>(
313 resolver: &PathResolver<R>,
314 directory: BaseDirectory,
315 path: Option<PathBuf>,
316) -> Result<PathBuf> {
317 let resolve_resource = matches!(directory, BaseDirectory::Resource);
318 let mut base_dir_path = match directory {
319 BaseDirectory::Audio => resolver.audio_dir(),
320 BaseDirectory::Cache => resolver.cache_dir(),
321 BaseDirectory::Config => resolver.config_dir(),
322 BaseDirectory::Data => resolver.data_dir(),
323 BaseDirectory::LocalData => resolver.local_data_dir(),
324 BaseDirectory::Document => resolver.document_dir(),
325 BaseDirectory::Download => resolver.download_dir(),
326 BaseDirectory::Picture => resolver.picture_dir(),
327 BaseDirectory::Public => resolver.public_dir(),
328 BaseDirectory::Video => resolver.video_dir(),
329 BaseDirectory::Resource => resolver.resource_dir(),
330 BaseDirectory::Temp => resolver.temp_dir(),
331 BaseDirectory::AppConfig => resolver.app_config_dir(),
332 BaseDirectory::AppData => resolver.app_data_dir(),
333 BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
334 BaseDirectory::AppCache => resolver.app_cache_dir(),
335 BaseDirectory::AppLog => resolver.app_log_dir(),
336 BaseDirectory::Home => resolver.home_dir(),
337 #[cfg(not(target_os = "android"))]
338 BaseDirectory::Desktop => resolver.desktop_dir(),
339 #[cfg(not(target_os = "android"))]
340 BaseDirectory::Executable => resolver.executable_dir(),
341 #[cfg(not(target_os = "android"))]
342 BaseDirectory::Font => resolver.font_dir(),
343 #[cfg(not(target_os = "android"))]
344 BaseDirectory::Runtime => resolver.runtime_dir(),
345 #[cfg(not(target_os = "android"))]
346 BaseDirectory::Template => resolver.template_dir(),
347 }?;
348
349 if let Some(path) = path {
350 if resolve_resource {
352 let mut resource_path = PathBuf::new();
353 for component in path.components() {
354 match component {
355 Component::Prefix(_) => {}
356 Component::RootDir => resource_path.push("_root_"),
357 Component::CurDir => {}
358 Component::ParentDir => resource_path.push("_up_"),
359 Component::Normal(p) => resource_path.push(p),
360 }
361 }
362 base_dir_path.push(resource_path);
363 } else {
364 base_dir_path.push(path);
365 }
366 }
367
368 Ok(base_dir_path)
369}
370
371#[cfg(test)]
372mod test {
373 use super::SafePathBuf;
374 use quickcheck::{Arbitrary, Gen};
375
376 use std::path::PathBuf;
377
378 impl Arbitrary for SafePathBuf {
379 fn arbitrary(g: &mut Gen) -> Self {
380 Self(PathBuf::arbitrary(g))
381 }
382
383 fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
384 Box::new(self.0.shrink().map(SafePathBuf))
385 }
386 }
387}