diff --git a/examples/filesystem.rs b/examples/filesystem.rs new file mode 100644 index 0000000000..dda35a4632 --- /dev/null +++ b/examples/filesystem.rs @@ -0,0 +1,65 @@ +extern crate sdl3; + +use sdl3::filesystem::*; + +pub fn main() -> Result<(), String> { + sdl3::init().ok(); + let base_path = get_base_path().unwrap(); + println!("Base path: {base_path:?}"); + + let path_info = get_path_info(base_path).unwrap(); + println!("Base path info: {path_info:?}"); + + + enumerate_directory(base_path, Box::new(|directory, file| { + println!("Enumerate {directory:?}: {file:?}"); + return EnumerationResult::Continue; + })).ok(); + + if let Ok(results) = glob_directory(base_path, Some("filesystem*"), GlobFlags::NONE) { + for result in results { + println!("Glob: {result:?}"); + } + } + + let user_folder = get_user_folder(Folder::DOCUMENTS).unwrap(); + println!("Documents folder: {user_folder:?}"); + + let test_path = base_path.join("testpath"); + let test_path2 = base_path.join("testpath2"); + match get_path_info(&test_path) { + Ok(info) => println!("Test path info: {info:?}"), + Err(e) => println!("Test path error: {e:?}") + } + create_directory(&test_path).ok(); + match get_path_info(&test_path) { + Ok(info) => println!("Test path info: {info:?}"), + Err(e) => println!("Test path error: {e:?}") + } + + match rename_path(&test_path, &test_path2) { + Ok(()) => println!("Renamed {test_path:?} to {test_path2:?}"), + Err(e) => eprintln!("Failed to rename: {e:?}") + } + + match remove_path(&test_path2) { + Ok(()) => println!("Removed {test_path2:?}"), + Err(e) => eprintln!("Failed to remove: {e:?}") + } + + match get_pref_path("sdl-rs", "filesystem") { + Ok(path) => { + println!("Got pref path for org 'sdl-rs' app 'filesystem' as {path:?}"); + match remove_path(&path) { + Ok(()) => println!("Removed {path:?}"), + Err(e) => eprintln!("Failed to remove: {e:?}") + } + }, + Err(error) => { + eprintln!("Failed to get pref path for org 'sdl-rs' app 'filesystem': {error:?}") + } + } + + + Ok(()) +} diff --git a/src/sdl3/filesystem.rs b/src/sdl3/filesystem.rs index 5eb421a274..072e5e97a8 100644 --- a/src/sdl3/filesystem.rs +++ b/src/sdl3/filesystem.rs @@ -1,26 +1,184 @@ -use crate::get_error; -use libc::c_char; -use libc::c_void; +use libc::{c_char, c_void}; use std::error; use std::ffi::{CStr, CString, NulError}; use std::fmt; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::ptr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use sys::filesystem::{SDL_EnumerationResult, SDL_PathInfo}; +use crate::get_error; use crate::sys; +#[derive(Debug, Clone)] +pub enum FileSystemError { + InvalidPathError(PathBuf), + NulError(NulError), + SdlError(String), +} + +macro_rules! path_cstring { + ($pathref:ident) => { + let Some($pathref) = $pathref.as_ref().to_str() else { + return Err(FileSystemError::InvalidPathError( + $pathref.as_ref().to_owned(), + )); + }; + + let Ok($pathref) = CString::new($pathref) else { + return Err(FileSystemError::InvalidPathError(PathBuf::from($pathref))); + }; + }; +} + +macro_rules! cstring_path { + ($path:ident, $error:expr) => { + let Ok($path) = CStr::from_ptr($path).to_str() else { + $error + }; + let $path = Path::new($path); + }; +} + +#[doc(alias = "SDL_CopyFile")] +pub fn copy_file( + old_path: impl AsRef, + new_path: impl AsRef, +) -> Result<(), FileSystemError> { + path_cstring!(old_path); + path_cstring!(new_path); + unsafe { + if !sys::filesystem::SDL_CopyFile(old_path.as_ptr(), new_path.as_ptr()) { + return Err(FileSystemError::SdlError(get_error())); + } + } + Ok(()) +} + +#[doc(alias = "SDL_CreateDirectory")] +pub fn create_directory(path: impl AsRef) -> Result<(), FileSystemError> { + path_cstring!(path); + unsafe { + if !sys::filesystem::SDL_CreateDirectory(path.as_ptr()) { + return Err(FileSystemError::SdlError(get_error())); + } + } + Ok(()) +} + +pub enum EnumerationResult { + Continue, + Success, + Failure, +} + +pub type EnumerateCallback = Box EnumerationResult>; + +unsafe extern "C" fn c_enumerate_directory( + userdata: *mut c_void, + dirname: *const c_char, + fname: *const c_char, +) -> SDL_EnumerationResult { + let callback_ptr = userdata as *mut EnumerateCallback; + + cstring_path!(dirname, return SDL_EnumerationResult::FAILURE); + cstring_path!(fname, return SDL_EnumerationResult::FAILURE); + + match (*callback_ptr)(dirname, fname) { + EnumerationResult::Continue => return SDL_EnumerationResult::CONTINUE, + EnumerationResult::Success => return SDL_EnumerationResult::SUCCESS, + EnumerationResult::Failure => return SDL_EnumerationResult::FAILURE, + } +} + +#[doc(alias = "SDL_EnumerateDirectory")] +pub fn enumerate_directory( + path: impl AsRef, + callback: EnumerateCallback, +) -> Result<(), FileSystemError> { + path_cstring!(path); + let callback_ptr = Box::into_raw(Box::new(callback)); + unsafe { + if !sys::filesystem::SDL_EnumerateDirectory( + path.as_ptr(), + Some(c_enumerate_directory), + callback_ptr as *mut c_void, + ) { + return Err(FileSystemError::SdlError(get_error())); + } + } + Ok(()) +} + #[doc(alias = "SDL_GetBasePath")] -pub fn base_path() -> Result { - let result = unsafe { - let buf = sys::filesystem::SDL_GetBasePath(); - let s = CStr::from_ptr(buf as *const _).to_str().unwrap().to_owned(); - sys::stdinc::SDL_free(buf as *mut c_void); - s +pub fn get_base_path() -> Result<&'static Path, FileSystemError> { + unsafe { + let path = sys::filesystem::SDL_GetBasePath(); + cstring_path!(path, return Err(FileSystemError::SdlError(get_error()))); + Ok(path) + } +} + +//TODO: Implement SDL_GetCurrentDirectory when sdl3-sys is updated to SDL 3.2.0. + +pub use sys::filesystem::SDL_PathType as PathType; + +pub struct PathInfo { + path_type: PathType, + size: usize, + create_time: SystemTime, + modify_time: SystemTime, + access_time: SystemTime, +} + +impl fmt::Debug for PathInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PathInfo") + .field("path_type", match self.path_type { + PathType::DIRECTORY => &"Directory", + PathType::FILE => &"File", + PathType::NONE => &"None", + _ => &"Other", + }) + .field("size", &self.size) + .field("create_time", &self.create_time) + .field("modify_time", &self.modify_time) + .field("access_time", &self.access_time) + .finish() + } +} + +#[doc(alias = "SDL_GetPathInfo")] +pub fn get_path_info(path: impl AsRef) -> Result { + let mut info = SDL_PathInfo { + r#type: PathType::NONE, + size: 0, + create_time: 0, + modify_time: 0, + access_time: 0, }; + path_cstring!(path); - if result.is_empty() { - Err(get_error()) - } else { - Ok(result) + unsafe { + if !sys::filesystem::SDL_GetPathInfo(path.as_ptr(), &mut info as *mut SDL_PathInfo) { + return Err(FileSystemError::SdlError(get_error())); + } } + + let path_type = info.r#type; + let size = info.size as usize; + let create_time = UNIX_EPOCH + Duration::from_nanos(info.create_time as u64); + let modify_time = UNIX_EPOCH + Duration::from_nanos(info.modify_time as u64); + let access_time = UNIX_EPOCH + Duration::from_nanos(info.access_time as u64); + + Ok(PathInfo { + path_type, + size, + create_time, + modify_time, + access_time, + }) } #[derive(Debug, Clone)] @@ -54,29 +212,151 @@ impl error::Error for PrefPathError { } } -// TODO: Change to OsStr or something? /// Return the preferred directory for the application to write files on this /// system, based on the given organization and application name. #[doc(alias = "SDL_GetPrefPath")] -pub fn pref_path(org_name: &str, app_name: &str) -> Result { - use self::PrefPathError::*; - let result = unsafe { - let org = match CString::new(org_name) { - Ok(s) => s, - Err(err) => return Err(InvalidOrganizationName(err)), - }; - let app = match CString::new(app_name) { - Ok(s) => s, - Err(err) => return Err(InvalidApplicationName(err)), - }; - let buf = - sys::filesystem::SDL_GetPrefPath(org.as_ptr() as *const c_char, app.as_ptr() as *const c_char); - CStr::from_ptr(buf as *const _).to_str().unwrap().to_owned() +pub fn get_pref_path(org_name: &str, app_name: &str) -> Result { + let org = match CString::new(org_name) { + Ok(s) => s, + Err(err) => return Err(PrefPathError::InvalidOrganizationName(err)), + }; + let app = match CString::new(app_name) { + Ok(s) => s, + Err(err) => return Err(PrefPathError::InvalidApplicationName(err)), + }; + + let path = unsafe { + let buf = sys::filesystem::SDL_GetPrefPath( + org.as_ptr() as *const c_char, + app.as_ptr() as *const c_char, + ); + let path = PathBuf::from(CStr::from_ptr(buf).to_str().unwrap()); + sys::stdinc::SDL_free(buf as *mut c_void); + path }; - if result.is_empty() { - Err(SdlError(get_error())) + if path.as_os_str().is_empty() { + Err(PrefPathError::SdlError(get_error())) } else { - Ok(result) + Ok(path) + } +} + +pub use sys::filesystem::SDL_Folder as Folder; + +#[doc(alias = "SDL_GetUserFolder")] +pub fn get_user_folder(folder: Folder) -> Result<&'static Path, FileSystemError> { + unsafe { + let path = sys::filesystem::SDL_GetUserFolder(folder); + cstring_path!(path, return Err(FileSystemError::SdlError(get_error()))); + Ok(path) + } +} + +bitflags! { + pub struct GlobFlags: sys::filesystem::SDL_GlobFlags { + const NONE = 0; + const CASEINSENSITIVE = sys::filesystem::SDL_GLOB_CASEINSENSITIVE; + } +} + +pub struct GlobResults<'a> { + internal: *mut *mut c_char, + count: i32, + index: isize, + phantom: PhantomData<&'a *mut *mut c_char>, +} + +impl<'a> GlobResults<'a> { + fn new(internal: *mut *mut c_char, count: i32) -> Self { + Self { + internal, + count, + index: 0, + phantom: PhantomData, + } } } + +impl<'a> Iterator for GlobResults<'a> { + type Item = &'a Path; + fn next(&mut self) -> Option { + unsafe { + if self.index >= self.count as isize { + return None; + } + let current = *self.internal.offset(self.index); + self.index += 1; + cstring_path!(current, return None); + Some(¤t) + } + } +} + +impl<'a> Drop for GlobResults<'a> { + fn drop(&mut self) { + unsafe { + sys::stdinc::SDL_free(self.internal as *mut c_void); + } + } +} + +#[doc(alias = "SDL_GlobDirectory")] +pub fn glob_directory( + path: impl AsRef, + pattern: Option<&str>, + flags: GlobFlags, +) -> Result { + path_cstring!(path); + let pattern = match pattern { + Some(pattern) => match CString::new(pattern) { + Ok(pattern) => Some(pattern), + Err(error) => return Err(FileSystemError::NulError(error)), + }, + None => None, + }; + let pattern_ptr = pattern.as_ref().map_or(ptr::null(), |pat| pat.as_ptr()); + let mut count = 0; + + let results = unsafe { + let paths = sys::filesystem::SDL_GlobDirectory( + path.as_ptr(), + pattern_ptr, + flags.bits(), + &mut count as *mut i32, + ); + if paths.is_null() { + return Err(FileSystemError::SdlError(get_error())); + } + GlobResults::new(paths, count) + }; + Ok(results) +} + +#[doc(alias = "SDL_RemovePath")] +pub fn remove_path(path: impl AsRef) -> Result<(), FileSystemError> { + path_cstring!(path); + unsafe { + if !sys::filesystem::SDL_RemovePath(path.as_ptr()) { + return Err(FileSystemError::SdlError(get_error())); + } + } + Ok(()) +} + +#[doc(alias = "SDL_RenamePath")] +pub fn rename_path( + old_path: impl AsRef, + new_path: impl AsRef, +) -> Result<(), FileSystemError> { + path_cstring!(old_path); + path_cstring!(new_path); + + unsafe { + if !sys::filesystem::SDL_RenamePath(old_path.as_ptr(), new_path.as_ptr()) { + return Err(FileSystemError::SdlError(get_error())); + } + } + + Ok(()) +}