playdate_rs/
fs.rs

1use alloc::ffi::CString;
2use alloc::string::String;
3use alloc::vec::Vec;
4
5pub use sys::{FileOptions, FileStat, SEEK_CUR, SEEK_END, SEEK_SET};
6
7use no_std_io::io::{self};
8
9pub use no_std_io::io::{Read, Seek, Write};
10
11pub struct PlaydateFileSystem {
12    handle: *const sys::playdate_file,
13}
14
15impl PlaydateFileSystem {
16    pub(crate) fn new(handle: *const sys::playdate_file) -> Self {
17        Self { handle }
18    }
19
20    /// Returns human-readable text describing the most recent error (usually indicated by a -1 return from a filesystem function).
21    pub fn get_error(&self) -> Option<io::Error> {
22        let c_string = unsafe { (*self.handle).geterr.unwrap()() };
23        if c_string.is_null() {
24            None
25        } else {
26            let c_str = unsafe { ::core::ffi::CStr::from_ptr(c_string) };
27            Some(io::Error::new(
28                io::ErrorKind::Other,
29                c_str.to_str().unwrap(),
30            ))
31        }
32    }
33
34    /// Calls the given callback function for every file at path. Subfolders are indicated by a trailing slash '/' in filename. listfiles() does not recurse into subfolders. If showhidden is set, files beginning with a period will be included; otherwise, they are skipped. Returns 0 on success, -1 if no folder exists at path or it can’t be opened.
35    pub fn list_files(
36        &self,
37        path: impl AsRef<str>,
38        show_hidden: bool,
39        mut callback: impl FnMut(&str),
40    ) -> Result<(), io::Error> {
41        let c_string = CString::new(path.as_ref()).unwrap();
42        extern "C" fn callback_wrapper(filename: *const i8, callback: *mut c_void) {
43            let callback = callback as *mut *mut dyn FnMut(&str);
44            let callback = unsafe { &mut **callback };
45            let filename = unsafe { ::core::ffi::CStr::from_ptr(filename) };
46            callback(filename.to_str().unwrap());
47        }
48        let mut callback_dyn: *mut dyn FnMut(&str) = &mut callback;
49        let callback_dyn_ptr: *mut *mut dyn FnMut(&str) = &mut callback_dyn;
50        let result = unsafe {
51            (*self.handle).listfiles.unwrap()(
52                c_string.as_ptr(),
53                Some(callback_wrapper),
54                callback_dyn_ptr as *mut _,
55                show_hidden as i32,
56            )
57        };
58        if result != 0 {
59            Ok(())
60        } else {
61            Err(self.get_error().unwrap())
62        }
63    }
64
65    /// Populates the FileStat stat with information about the file at path. Returns 0 on success, or -1 in case of error.
66    pub fn stat(&self, path: impl AsRef<str>) -> io::Result<FileStat> {
67        let c_string = CString::new(path.as_ref()).unwrap();
68        let mut stat = FileStat::default();
69        let result = unsafe { (*self.handle).stat.unwrap()(c_string.as_ptr(), &mut stat) };
70        if result != 0 {
71            Ok(stat)
72        } else {
73            Err(self.get_error().unwrap())
74        }
75    }
76
77    /// Creates the given path in the Data/&lt;gameid&gt; folder. It does not create intermediate folders. Returns 0 on success, or -1 in case of error.
78    pub fn mkdir(&self, path: impl AsRef<str>) -> io::Result<()> {
79        let c_string = CString::new(path.as_ref()).unwrap();
80        let result = unsafe { (*self.handle).mkdir.unwrap()(c_string.as_ptr()) };
81        if result != 0 {
82            Ok(())
83        } else {
84            Err(self.get_error().unwrap())
85        }
86    }
87
88    /// Deletes the file at path. Returns 0 on success, or -1 in case of error. If recursive is 1 and the target path is a folder, this deletes everything inside the folder (including folders, folders inside those, and so on) as well as the folder itself.
89    pub fn unlink(&self, name: impl AsRef<str>, recursive: bool) -> io::Result<()> {
90        let c_string = CString::new(name.as_ref()).unwrap();
91        let result = unsafe { (*self.handle).unlink.unwrap()(c_string.as_ptr(), recursive as i32) };
92        if result != 0 {
93            Ok(())
94        } else {
95            Err(self.get_error().unwrap())
96        }
97    }
98
99    /// Renames the file at from to to. It will overwrite the file at to without confirmation. It does not create intermediate folders. Returns 0 on success, or -1 in case of error.
100    pub fn rename(&self, from: impl AsRef<str>, to: impl AsRef<str>) -> io::Result<()> {
101        let from_c_string = CString::new(from.as_ref()).unwrap();
102        let to_c_string = CString::new(to.as_ref()).unwrap();
103        let result =
104            unsafe { (*self.handle).rename.unwrap()(from_c_string.as_ptr(), to_c_string.as_ptr()) };
105        if result != 0 {
106            Ok(())
107        } else {
108            Err(self.get_error().unwrap())
109        }
110    }
111
112    /// Opens a handle for the file at path. The kFileRead mode opens a file in the game pdx, while kFileReadData searches the game’s data folder; to search the data folder first then fall back on the game pdx, use the bitwise combination kFileRead|kFileReadData.kFileWrite and kFileAppend always write to the data folder. The function returns NULL if a file at path cannot be opened, and playdate->file->geterr() will describe the error. The filesystem has a limit of 64 simultaneous open files.
113    pub fn open(&self, name: impl AsRef<str>, mode: FileOptions) -> io::Result<File> {
114        let c_string = CString::new(name.as_ref()).unwrap();
115        let file = unsafe { (*self.handle).open.unwrap()(c_string.as_ptr(), mode) };
116        if file.is_null() {
117            Err(self.get_error().unwrap())
118        } else {
119            Ok(File::new(file))
120        }
121    }
122
123    /// Closes the given file handle. Returns 0 on success, or -1 in case of error.
124    pub(crate) fn close(&self, file: *mut sys::SDFile) -> io::Result<()> {
125        let result = unsafe { (*self.handle).close.unwrap()(file) };
126        if result == 0 {
127            Ok(())
128        } else {
129            Err(self.get_error().unwrap())
130        }
131    }
132
133    /// Reads up to len bytes from the file into the buffer buf. Returns the number of bytes read (0 indicating end of file), or -1 in case of error.
134    pub(crate) fn read(&self, file: *mut sys::SDFile, buf: &mut [u8]) -> io::Result<usize> {
135        let result = unsafe {
136            (*self.handle).read.unwrap()(file, buf.as_mut_ptr() as *mut _, buf.len() as u32)
137        };
138        if result >= 0 {
139            Ok(result as usize)
140        } else {
141            Err(self.get_error().unwrap())
142        }
143    }
144
145    /// Writes the buffer of bytes buf to the file. Returns the number of bytes written, or -1 in case of error.
146    pub(crate) fn write(&self, file: *mut sys::SDFile, buf: &[u8]) -> io::Result<usize> {
147        let result = unsafe {
148            (*self.handle).write.unwrap()(file, buf.as_ptr() as *const _, buf.len() as u32)
149        };
150        if result >= 0 {
151            Ok(result as usize)
152        } else {
153            Err(self.get_error().unwrap())
154        }
155    }
156
157    /// Flushes the output buffer of file immediately. Returns the number of bytes written, or -1 in case of error.
158    pub(crate) fn flush(&self, file: *mut sys::SDFile) -> io::Result<()> {
159        let result = unsafe { (*self.handle).flush.unwrap()(file) };
160        if result != 0 {
161            Ok(())
162        } else {
163            Err(self.get_error().unwrap())
164        }
165    }
166
167    /// Returns the current read/write offset in the given file handle, or -1 on error.
168    pub(crate) fn tell(&self, file: *mut sys::SDFile) -> io::Result<usize> {
169        let result = unsafe { (*self.handle).tell.unwrap()(file) };
170        if result >= 0 {
171            Ok(result as usize)
172        } else {
173            Err(self.get_error().unwrap())
174        }
175    }
176
177    /// Sets the read/write offset in the given file handle to pos, relative to the whence macro. SEEK_SET is relative to the beginning of the file, SEEK_CUR is relative to the current position of the file pointer, and SEEK_END is relative to the end of the file. Returns 0 on success, -1 on error.
178    pub(crate) fn seek(&self, file: *mut sys::SDFile, pos: usize, whence: i32) -> io::Result<()> {
179        let result = unsafe { (*self.handle).seek.unwrap()(file, pos as i32, whence) };
180        if result != 0 {
181            Ok(())
182        } else {
183            Err(self.get_error().unwrap())
184        }
185    }
186}
187
188use core::ffi::c_void;
189
190use crate::PLAYDATE;
191
192pub struct File {
193    handle: *mut sys::SDFile,
194}
195
196impl File {
197    pub(crate) fn new(handle: *mut sys::SDFile) -> Self {
198        Self { handle }
199    }
200
201    /// Returns the current read/write offset in the given file handle, or -1 on error.
202    pub fn tell(&self) -> io::Result<usize> {
203        PLAYDATE.file.tell(self.handle)
204    }
205
206    /// Open a new file
207    pub fn open(name: impl AsRef<str>, mode: FileOptions) -> io::Result<Self> {
208        PLAYDATE.file.open(name, mode)
209    }
210
211    /// Read the entire content to a string
212    pub fn read_to_string(&mut self) -> io::Result<String> {
213        let mut buf = Vec::new();
214        self.read_to_end(&mut buf)?;
215        Ok(String::from_utf8(buf).unwrap())
216    }
217}
218
219impl Read for File {
220    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
221        let Ok(size) = PLAYDATE.file.read(self.handle, buf) else {
222            return Err(io::Error::new(io::ErrorKind::Other, "file read error"));
223        };
224        Ok(size)
225    }
226}
227
228impl Write for File {
229    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
230        let Ok(size) = PLAYDATE.file.write(self.handle, buf) else {
231            return Err(io::Error::new(io::ErrorKind::Other, "file write error"));
232        };
233        Ok(size)
234    }
235
236    fn flush(&mut self) -> io::Result<()> {
237        if PLAYDATE.file.flush(self.handle).is_err() {
238            Err(io::Error::new(io::ErrorKind::Other, "file flush error"))
239        } else {
240            Ok(())
241        }
242    }
243}
244
245impl Seek for File {
246    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
247        let whence = match pos {
248            io::SeekFrom::Start(_) => SEEK_SET,
249            io::SeekFrom::End(_) => SEEK_END,
250            io::SeekFrom::Current(_) => SEEK_CUR,
251        };
252        let pos = match pos {
253            io::SeekFrom::Start(pos) => pos as usize,
254            io::SeekFrom::End(pos) => pos as usize,
255            io::SeekFrom::Current(pos) => pos as usize,
256        };
257        if PLAYDATE.file.seek(self.handle, pos, whence as _).is_err() {
258            Err(io::Error::new(io::ErrorKind::Other, "file seek error"))
259        } else {
260            Ok(pos as u64)
261        }
262    }
263}
264
265impl Drop for File {
266    fn drop(&mut self) {
267        PLAYDATE.file.close(self.handle).unwrap();
268    }
269}