sdl3/
filesystem.rs

1use libc::{c_char, c_void};
2use std::error;
3use std::ffi::{CStr, CString, NulError};
4use std::fmt;
5use std::marker::PhantomData;
6use std::path::{Path, PathBuf};
7use std::ptr;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9use sys::filesystem::SDL_PathInfo;
10
11use crate::get_error;
12use crate::sys;
13use crate::Error;
14
15#[derive(Debug, Clone)]
16pub enum FileSystemError {
17    InvalidPathError(PathBuf),
18    NulError(NulError),
19    SdlError(Error),
20}
21
22/// Turn a AsRef<Path> into a CString so it can be passed to C
23macro_rules! path_cstring {
24    ($pathref:ident) => {
25        let Some($pathref) = $pathref.as_ref().to_str() else {
26            return Err(FileSystemError::InvalidPathError(
27                $pathref.as_ref().to_owned(),
28            ));
29        };
30
31        let Ok($pathref) = CString::new($pathref) else {
32            return Err(FileSystemError::InvalidPathError(PathBuf::from($pathref)));
33        };
34    };
35}
36
37// Turn a CString into a Path for ease of use
38macro_rules! cstring_path {
39    ($path:ident, $error:expr) => {
40        let Ok($path) = CStr::from_ptr($path).to_str() else {
41            $error
42        };
43        let $path = Path::new($path);
44    };
45}
46
47#[doc(alias = "SDL_CopyFile")]
48pub fn copy_file(
49    old_path: impl AsRef<Path>,
50    new_path: impl AsRef<Path>,
51) -> Result<(), FileSystemError> {
52    path_cstring!(old_path);
53    path_cstring!(new_path);
54    unsafe {
55        if !sys::filesystem::SDL_CopyFile(old_path.as_ptr(), new_path.as_ptr()) {
56            return Err(FileSystemError::SdlError(get_error()));
57        }
58    }
59    Ok(())
60}
61
62#[doc(alias = "SDL_CreateDirectory")]
63pub fn create_directory(path: impl AsRef<Path>) -> Result<(), FileSystemError> {
64    path_cstring!(path);
65    unsafe {
66        if !sys::filesystem::SDL_CreateDirectory(path.as_ptr()) {
67            return Err(FileSystemError::SdlError(get_error()));
68        }
69    }
70    Ok(())
71}
72
73pub use sys::filesystem::SDL_EnumerationResult as EnumerationResult;
74
75pub type EnumerateCallback = fn(&Path, &Path) -> EnumerationResult;
76
77unsafe extern "C" fn c_enumerate_directory(
78    userdata: *mut c_void,
79    dirname: *const c_char,
80    fname: *const c_char,
81) -> EnumerationResult {
82    let callback: EnumerateCallback = std::mem::transmute(userdata);
83
84    cstring_path!(dirname, return EnumerationResult::FAILURE);
85    cstring_path!(fname, return EnumerationResult::FAILURE);
86
87    callback(dirname, fname)
88}
89
90#[doc(alias = "SDL_EnumerateDirectory")]
91pub fn enumerate_directory(
92    path: impl AsRef<Path>,
93    callback: EnumerateCallback,
94) -> Result<(), FileSystemError> {
95    path_cstring!(path);
96    unsafe {
97        if !sys::filesystem::SDL_EnumerateDirectory(
98            path.as_ptr(),
99            Some(c_enumerate_directory),
100            callback as *mut c_void,
101        ) {
102            return Err(FileSystemError::SdlError(get_error()));
103        }
104    }
105    Ok(())
106}
107
108#[doc(alias = "SDL_GetBasePath")]
109pub fn get_base_path() -> Result<&'static Path, FileSystemError> {
110    unsafe {
111        let path = sys::filesystem::SDL_GetBasePath();
112        cstring_path!(path, return Err(FileSystemError::SdlError(get_error())));
113        Ok(path)
114    }
115}
116
117//TODO: Implement SDL_GetCurrentDirectory when sdl3-sys is updated to SDL 3.2.0.
118
119pub use sys::filesystem::SDL_PathType as PathType;
120
121pub struct PathInfo {
122    internal: SDL_PathInfo,
123}
124
125impl PathInfo {
126    fn path_type(&self) -> PathType {
127        self.internal.r#type as PathType
128    }
129
130    fn size(&self) -> usize {
131        self.internal.size as usize
132    }
133
134    fn create_time(&self) -> SystemTime {
135        UNIX_EPOCH + Duration::from_nanos(self.internal.create_time as u64)
136    }
137
138    fn modify_time(&self) -> SystemTime {
139        UNIX_EPOCH + Duration::from_nanos(self.internal.modify_time as u64)
140    }
141
142    fn access_time(&self) -> SystemTime {
143        UNIX_EPOCH + Duration::from_nanos(self.internal.access_time as u64)
144    }
145}
146
147impl fmt::Debug for PathInfo {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        f.debug_struct("PathInfo")
150            .field(
151                "path_type",
152                match self.path_type() {
153                    PathType::DIRECTORY => &"Directory",
154                    PathType::FILE => &"File",
155                    PathType::NONE => &"None",
156                    _ => &"Other",
157                },
158            )
159            .field("size", &self.size())
160            .field("create_time", &self.create_time())
161            .field("modify_time", &self.modify_time())
162            .field("access_time", &self.access_time())
163            .finish()
164    }
165}
166
167#[doc(alias = "SDL_GetPathInfo")]
168pub fn get_path_info(path: impl AsRef<Path>) -> Result<PathInfo, FileSystemError> {
169    let mut info = SDL_PathInfo {
170        r#type: PathType::NONE,
171        size: 0,
172        create_time: 0,
173        modify_time: 0,
174        access_time: 0,
175    };
176    path_cstring!(path);
177
178    unsafe {
179        if !sys::filesystem::SDL_GetPathInfo(path.as_ptr(), &mut info as *mut SDL_PathInfo) {
180            return Err(FileSystemError::SdlError(get_error()));
181        }
182    }
183
184    Ok(PathInfo { internal: info })
185}
186
187#[derive(Debug, Clone)]
188pub enum PrefPathError {
189    InvalidOrganizationName(NulError),
190    InvalidApplicationName(NulError),
191    SdlError(Error),
192}
193
194impl fmt::Display for PrefPathError {
195    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
196        use self::PrefPathError::*;
197
198        match *self {
199            InvalidOrganizationName(ref e) => write!(f, "Invalid organization name: {}", e),
200            InvalidApplicationName(ref e) => write!(f, "Invalid application name: {}", e),
201            SdlError(ref e) => write!(f, "SDL error: {}", e),
202        }
203    }
204}
205
206impl error::Error for PrefPathError {
207    fn description(&self) -> &str {
208        use self::PrefPathError::*;
209
210        match *self {
211            InvalidOrganizationName(_) => "invalid organization name",
212            InvalidApplicationName(_) => "invalid application name",
213            SdlError(ref e) => &e.0,
214        }
215    }
216}
217
218/// Return the preferred directory for the application to write files on this
219/// system, based on the given organization and application name.
220#[doc(alias = "SDL_GetPrefPath")]
221pub fn get_pref_path(org_name: &str, app_name: &str) -> Result<PathBuf, PrefPathError> {
222    let org = match CString::new(org_name) {
223        Ok(s) => s,
224        Err(err) => return Err(PrefPathError::InvalidOrganizationName(err)),
225    };
226    let app = match CString::new(app_name) {
227        Ok(s) => s,
228        Err(err) => return Err(PrefPathError::InvalidApplicationName(err)),
229    };
230
231    let path = unsafe {
232        let buf = sys::filesystem::SDL_GetPrefPath(
233            org.as_ptr() as *const c_char,
234            app.as_ptr() as *const c_char,
235        );
236        let path = PathBuf::from(CStr::from_ptr(buf).to_str().unwrap());
237        sys::stdinc::SDL_free(buf as *mut c_void);
238        path
239    };
240
241    if path.as_os_str().is_empty() {
242        Err(PrefPathError::SdlError(get_error()))
243    } else {
244        Ok(path)
245    }
246}
247
248pub use sys::filesystem::SDL_Folder as Folder;
249
250#[doc(alias = "SDL_GetUserFolder")]
251pub fn get_user_folder(folder: Folder) -> Result<&'static Path, FileSystemError> {
252    unsafe {
253        let path = sys::filesystem::SDL_GetUserFolder(folder);
254        cstring_path!(path, return Err(FileSystemError::SdlError(get_error())));
255        Ok(path)
256    }
257}
258
259bitflags! {
260    pub struct GlobFlags: sys::filesystem::SDL_GlobFlags {
261        const NONE = 0;
262        const CASEINSENSITIVE = sys::filesystem::SDL_GLOB_CASEINSENSITIVE;
263    }
264}
265
266pub struct GlobResultsIter<'a> {
267    results: &'a GlobResults<'a>,
268    index: isize,
269}
270
271impl<'a> Iterator for GlobResultsIter<'a> {
272    type Item = &'a Path;
273    fn next(&mut self) -> Option<Self::Item> {
274        let current = self.results.get(self.index);
275        self.index += 1;
276        current
277    }
278}
279
280pub struct GlobResults<'a> {
281    internal: *mut *mut c_char,
282    count: isize,
283    phantom: PhantomData<&'a *mut *mut c_char>,
284}
285
286impl GlobResults<'_> {
287    fn new(internal: *mut *mut c_char, count: isize) -> Self {
288        Self {
289            internal,
290            count,
291            phantom: PhantomData,
292        }
293    }
294
295    fn len(&self) -> usize {
296        self.count as usize
297    }
298
299    fn get<I>(&self, index: I) -> Option<&Path>
300    where
301        I: Into<isize>,
302    {
303        let index = index.into();
304        if index >= self.count {
305            return None;
306        }
307        unsafe {
308            let path = *self.internal.offset(index);
309            cstring_path!(path, return None);
310            Some(path)
311        }
312    }
313}
314
315impl<'a> IntoIterator for &'a GlobResults<'a> {
316    type Item = &'a Path;
317    type IntoIter = GlobResultsIter<'a>;
318    fn into_iter(self) -> Self::IntoIter {
319        Self::IntoIter {
320            results: self,
321            index: 0,
322        }
323    }
324}
325
326impl Drop for GlobResults<'_> {
327    fn drop(&mut self) {
328        unsafe {
329            sys::stdinc::SDL_free(self.internal as *mut c_void);
330        }
331    }
332}
333
334#[doc(alias = "SDL_GlobDirectory")]
335pub fn glob_directory(
336    path: impl AsRef<Path>,
337    pattern: Option<&str>,
338    flags: GlobFlags,
339) -> Result<GlobResults, FileSystemError> {
340    path_cstring!(path);
341    let pattern = match pattern {
342        Some(pattern) => match CString::new(pattern) {
343            Ok(pattern) => Some(pattern),
344            Err(error) => return Err(FileSystemError::NulError(error)),
345        },
346        None => None,
347    };
348    let pattern_ptr = pattern.as_ref().map_or(ptr::null(), |pat| pat.as_ptr());
349    let mut count = 0;
350
351    let results = unsafe {
352        let paths = sys::filesystem::SDL_GlobDirectory(
353            path.as_ptr(),
354            pattern_ptr,
355            flags.bits(),
356            &mut count as *mut i32,
357        );
358        if paths.is_null() {
359            return Err(FileSystemError::SdlError(get_error()));
360        }
361        GlobResults::new(paths, count as isize)
362    };
363    Ok(results)
364}
365
366#[doc(alias = "SDL_RemovePath")]
367pub fn remove_path(path: impl AsRef<Path>) -> Result<(), FileSystemError> {
368    path_cstring!(path);
369    unsafe {
370        if !sys::filesystem::SDL_RemovePath(path.as_ptr()) {
371            return Err(FileSystemError::SdlError(get_error()));
372        }
373    }
374    Ok(())
375}
376
377#[doc(alias = "SDL_RenamePath")]
378pub fn rename_path(
379    old_path: impl AsRef<Path>,
380    new_path: impl AsRef<Path>,
381) -> Result<(), FileSystemError> {
382    path_cstring!(old_path);
383    path_cstring!(new_path);
384
385    unsafe {
386        if !sys::filesystem::SDL_RenamePath(old_path.as_ptr(), new_path.as_ptr()) {
387            return Err(FileSystemError::SdlError(get_error()));
388        }
389    }
390
391    Ok(())
392}