playdate_fs/
lib.rs

1#![cfg_attr(not(test), no_std)]
2#![feature(const_trait_impl)]
3
4#[macro_use]
5extern crate sys;
6extern crate alloc;
7
8use core::ffi::c_char;
9use core::ffi::c_int;
10use core::ffi::c_uint;
11use core::ffi::c_void;
12use alloc::string::String;
13use alloc::vec::Vec;
14
15use error::Error;
16use file::AnyFile;
17use options::FileOptionsExt;
18use options::OpenOptions;
19use seek::Whence;
20pub use sys::ffi::FileStat;
21pub use sys::ffi::FileOptions;
22use sys::ffi::CString;
23use sys::ffi::CStr;
24
25use file::File;
26use error::ApiError;
27
28
29pub mod api;
30pub mod file;
31pub mod seek;
32pub mod options;
33pub mod error;
34
35
36pub type Path = str;
37
38
39/// Read the entire contents of a file into a bytes vector.
40/// > Works similarly to [`std::fs::read`].
41pub fn read<P: AsRef<Path>>(path: P, data_dir: bool) -> Result<Vec<u8>, ApiError> {
42	let fs = Fs::Cached();
43	let opts = FileOptions::new().read(true).read_data(data_dir);
44	let mut file = fs.open_with(api::Default, &path, opts)?;
45
46	// determine size of file:
47	let size = fs.metadata(path).map(|m| m.size).ok().unwrap_or(0);
48
49	// prepare prefilled buffer:
50	let mut buf = alloc::vec![0; size as usize];
51
52	fs.read(&mut file, &mut buf, size)?;
53	Ok(buf)
54}
55
56
57/// Read the entire contents of a file into a string.
58/// > Works similarly to [`std::fs::read_to_string`].
59pub fn read_to_string<P: AsRef<Path>>(path: P, data_dir: bool) -> Result<String, ApiError> {
60	let buf = read(path, data_dir)?;
61	alloc::string::String::from_utf8(buf).map_err(Into::into)
62}
63
64
65/// Write a bytes of the entire `contents` of a file.
66///
67/// This function will create a file if it does not exist,
68/// and will entirely replace its contents if it does.
69///
70/// > Works similarly to [`std::fs::write`].
71///
72/// Uses [`sys::ffi::playdate_file::open`] and [`sys::ffi::playdate_file::write`].
73pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), ApiError> {
74	let mut file = File::options().write(true).append(false).open(&path)?;
75	file.write(contents.as_ref())?;
76	Ok(())
77}
78
79
80#[inline(always)]
81/// Removes a file from the filesystem. Directory is a file too.
82///
83/// > Works similarly to [`std::fs::remove_file`] and [`std::fs::remove_dir`].
84///
85/// Uses [`sys::ffi::playdate_file::unlink`].
86pub fn remove<P: AsRef<Path>>(path: P) -> Result<(), ApiError> { Fs::Default().remove(path) }
87
88
89// TODO: metadata
90/// Given a path, query the file system to get information about a file,
91/// directory, etc.
92#[inline(always)]
93pub fn metadata<P: AsRef<Path>>(path: P) -> Result<FileStat, ApiError> { Fs::Default().metadata(path) }
94
95
96/// Renames the file at `from` to `to`.
97///
98/// It will overwrite the file at `to`.
99///
100/// It does not create intermediate folders.
101#[inline(always)]
102pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), ApiError> {
103	Fs::Default().rename(from, to)
104}
105
106/// Creates the given `path` in the `Data/<gameid>` folder.
107///
108/// It does not create intermediate folders.
109#[inline(always)]
110pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<(), ApiError> { Fs::Default().create_dir(path) }
111
112// TODO: create_dir_all
113// pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()>
114
115
116#[inline(always)]
117/// Removes a directory and all of its contents recursively.
118/// > Works similarly to [`std::fs::remove_file`], [`std::fs::remove_dir`] and [`std::fs::remove_dir_all`].
119///
120/// Caution: it also can delete file without any error, if `path` is a file.
121///
122/// Calls [`sys::ffi::playdate_file::unlink`] with `recursive`.
123// XXX: TODO: Should we validate that `path` is a directory?
124pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<(), ApiError> { Fs::Default().remove_dir_all(path) }
125
126// TODO: read_dir -> iter ReadDir
127// pub fn read_dir<P: AsRef<Path>>(path: P) -> Result<ReadDir>
128
129
130/// Playdate File-system API.
131///
132/// Uses inner api end-point for all operations.
133#[derive(Debug, Clone, Copy)]
134pub struct Fs<Api = api::Default>(Api);
135
136impl Fs<api::Default> {
137	/// Creates default [`Fs`] without type parameter requirement.
138	///
139	/// Uses ZST [`api::Default`].
140	#[allow(non_snake_case)]
141	pub fn Default() -> Self { Self(Default::default()) }
142}
143
144impl Fs<api::Cache> {
145	/// Creates [`Fs`] without type parameter requirement.
146	///
147	/// Uses [`api::Cache`].
148	#[allow(non_snake_case)]
149	pub fn Cached() -> Self { Self(Default::default()) }
150}
151
152impl<Api: Default + api::Api> Default for Fs<Api> {
153	fn default() -> Self { Self(Default::default()) }
154}
155
156impl<Api: Default + api::Api> Fs<Api> {
157	pub fn new() -> Self { Self(Default::default()) }
158}
159
160impl<Api: api::Api> Fs<Api> {
161	pub fn new_with(api: Api) -> Self { Self(api) }
162}
163
164
165mod ops {
166	use super::*;
167
168	pub fn open<Api: api::Api, P: AsRef<Path>, Opts: OpenOptions>(api: Api,
169	                                                              path: P,
170	                                                              options: Opts)
171	                                                              -> Result<File<Api>, ApiError> {
172		let path = CString::new(path.as_ref())?;
173		let f = api.open();
174		let ptr = unsafe { f(path.as_ptr() as _, options.into()) };
175		Ok(File(ptr as _, api))
176	}
177
178	pub fn open_with<UApi: api::Api, FApi: api::Api, P: AsRef<Path>, Opts: OpenOptions>(
179		using: UApi,
180		api: FApi,
181		path: P,
182		options: Opts)
183		-> Result<File<FApi>, ApiError> {
184		let path = CString::new(path.as_ref())?;
185		let f = using.open();
186		let ptr = unsafe { f(path.as_ptr() as _, options.into()) };
187		Ok(File(ptr as _, api))
188	}
189
190	pub fn close<Api: api::Api>(mut file: File<Api>) -> Result<(), Error> {
191		let f = file.1.close();
192		let result = unsafe { f(file.0 as _) };
193		file.0 = core::ptr::null_mut();
194		Error::ok_from_code(result)?;
195		Ok(())
196	}
197
198	pub fn close_with<Api: api::Api, FApi: api::Api>(api: Api, mut file: File<FApi>) -> Result<(), Error> {
199		let f = api.close();
200		let result = unsafe { f(file.0) };
201		file.0 = core::ptr::null_mut();
202		Error::ok_from_code(result)?;
203		Ok(())
204	}
205
206	pub fn seek<Api: api::Api>(file: &mut File<Api>, pos: c_int, whence: Whence) -> Result<(), Error> {
207		let f = file.1.seek();
208		let result = unsafe { f(file.0, pos, whence as _) };
209		Error::ok_from_code(result)?;
210		Ok(())
211	}
212
213	pub fn seek_with<Api: api::Api>(api: Api,
214	                                file: &mut impl AnyFile,
215	                                pos: c_int,
216	                                whence: Whence)
217	                                -> Result<(), Error> {
218		let f = api.seek();
219		let result = unsafe { f(file.as_raw(), pos, whence as _) };
220		Error::ok_from_code(result)?;
221		Ok(())
222	}
223
224	pub fn tell<Api: api::Api>(file: &mut File<Api>) -> Result<c_uint, Error> {
225		let f = file.1.tell();
226		let result = unsafe { f(file.0) };
227		Error::ok_from_code(result)
228	}
229
230	pub fn tell_with<Api: api::Api>(api: Api, file: &mut impl AnyFile) -> Result<c_uint, Error> {
231		let f = api.tell();
232		let result = unsafe { f(file.as_raw()) };
233		Error::ok_from_code(result)
234	}
235
236
237	pub fn read<Api: api::Api>(file: &mut File<Api>, to: &mut Vec<u8>, len: c_uint) -> Result<c_uint, Error> {
238		let f = file.1.read();
239		let result = unsafe { f(file.0, to.as_mut_ptr() as *mut _, len) };
240		Error::ok_from_code(result)
241	}
242
243	pub fn write<Api: api::Api>(file: &mut File<Api>, from: &[u8]) -> Result<c_uint, Error> {
244		let f = file.1.write();
245		let result = unsafe { f(file.0, from.as_ptr() as *mut _, from.len() as _) };
246		Error::ok_from_code(result)
247	}
248
249	pub fn flush<Api: api::Api>(file: &mut File<Api>) -> Result<c_uint, Error> {
250		let f = file.1.flush();
251		let result = unsafe { f(file.0) };
252		Error::ok_from_code(result)
253	}
254}
255
256
257impl<Api: api::Api> Fs<Api> {
258	/// Open file for given `options`.
259	///
260	/// Creates new [`File`] instance with copy of inner api end-point.
261	///
262	/// Equivalent to [`sys::ffi::playdate_file::open`]
263	#[doc(alias = "sys::ffi::playdate_file::open")]
264	#[inline(always)]
265	pub fn open<P: AsRef<Path>, Opts: OpenOptions>(&self, path: P, options: Opts) -> Result<File<Api>, ApiError>
266		where Api: Copy {
267		ops::open(self.0, path, options)
268	}
269
270	/// Open file for given `options`.
271	///
272	/// Creates new [`File`] instance with given `api`.
273	///
274	/// Equivalent to [`sys::ffi::playdate_file::open`]
275	#[doc(alias = "sys::ffi::playdate_file::open")]
276	#[inline(always)]
277	pub fn open_with<T: api::Api, P: AsRef<Path>, Opts: OpenOptions>(&self,
278	                                                                 api: T,
279	                                                                 path: P,
280	                                                                 options: Opts)
281	                                                                 -> Result<File<T>, ApiError> {
282		ops::open_with(&self.0, api, path, options)
283	}
284
285	/// Closes the given file.
286	///
287	/// Equivalent to [`sys::ffi::playdate_file::close`]
288	#[doc(alias = "sys::ffi::playdate_file::close")]
289	#[inline(always)]
290	pub fn close<T: api::Api>(&self, file: File<T>) -> Result<(), Error> { ops::close_with(&self.0, file) }
291
292
293	/// Returns the current read/write offset in the given file.
294	///
295	/// Equivalent to [`sys::ffi::playdate_file::tell`]
296	#[doc(alias = "sys::ffi::playdate_file::tell")]
297	#[inline(always)]
298	pub fn tell(&self, file: &mut impl AnyFile) -> Result<c_uint, Error> { crate::ops::tell_with(&self.0, file) }
299
300	/// Sets the read/write offset in the given file to pos, relative to the `whence`.
301	/// - [`Whence::Start`] is relative to the beginning of the file,
302	/// - [`Whence::Current`] is relative to the current position of the file,
303	/// - [`Whence::End`] is relative to the end of the file.
304	///
305	/// Equivalent to [`sys::ffi::playdate_file::seek`]
306	#[doc(alias = "sys::ffi::playdate_file::seek")]
307	#[inline(always)]
308	pub fn seek_raw(&self, file: &mut impl AnyFile, pos: c_int, whence: Whence) -> Result<(), Error> {
309		crate::ops::seek_with(&self.0, file, pos, whence)
310	}
311
312
313	/// Reads up to `len` bytes from the file into the buffer `to`.
314	///
315	/// Returns the number of bytes read (0 indicating end of file).
316	///
317	/// Caution: Vector must be prefilled with `0`s.
318	/// ```no_run
319	/// let mut buf = Vec::<u8>::with_capacity(size);
320	/// buf.resize(size, 0);
321	/// fs.read(&mut file, &mut buf, size)?;
322	/// ```
323	///
324	/// Equivalent to [`sys::ffi::playdate_file::read`]
325	#[doc(alias = "sys::ffi::playdate_file::read")]
326	pub fn read(&self, file: &mut impl AnyFile, to: &mut Vec<u8>, len: c_uint) -> Result<c_uint, Error> {
327		let f = self.0.read();
328		let result = unsafe { f(file.as_raw(), to.as_mut_ptr() as *mut _, len) };
329		Error::ok_from_code(result)
330	}
331
332
333	/// Writes the buffer of bytes buf to the file.
334	///
335	/// Returns the number of bytes written.
336	///
337	/// Equivalent to [`sys::ffi::playdate_file::write`]
338	#[doc(alias = "sys::ffi::playdate_file::write")]
339	pub fn write(&self, file: &mut impl AnyFile, from: &[u8]) -> Result<c_uint, Error> {
340		let f = self.0.write();
341		let result = unsafe { f(file.as_raw(), from.as_ptr() as *mut _, from.len() as _) };
342		Error::ok_from_code(result)
343	}
344
345	/// Flushes the output buffer of file immediately.
346	///
347	/// Returns the number of bytes written.
348	///
349	/// Equivalent to [`sys::ffi::playdate_file::flush`]
350	#[doc(alias = "sys::ffi::playdate_file::flush")]
351	pub fn flush(&self, file: &mut impl AnyFile) -> Result<c_uint, Error> {
352		let f = self.0.flush();
353		let result = unsafe { f(file.as_raw()) };
354		Error::ok_from_code(result)
355	}
356
357
358	/// Populates the [`FileStat`] stat with information about the file at `path`.
359	///
360	/// Equivalent to [`sys::ffi::playdate_file::stat`]
361	#[doc(alias = "sys::ffi::playdate_file::stat")]
362	pub fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<FileStat, ApiError> {
363		let mut stat = FileStat { isdir: 0,
364		                          size: 0,
365		                          m_year: 0,
366		                          m_month: 0,
367		                          m_day: 0,
368		                          m_hour: 0,
369		                          m_minute: 0,
370		                          m_second: 0 };
371		self.metadata_to(path, &mut stat).map(|_| stat)
372	}
373
374	/// Writes into the given `metadata` information about the file at `path`.
375	///
376	/// Equivalent to [`sys::ffi::playdate_file::stat`]
377	#[doc(alias = "sys::ffi::playdate_file::stat")]
378	pub fn metadata_to<P: AsRef<Path>>(&self, path: P, metadata: &mut FileStat) -> Result<(), ApiError> {
379		let path = CString::new(path.as_ref())?;
380		let f = self.0.stat();
381		let result = unsafe { f(path.as_ptr() as _, metadata as *mut _) };
382		Error::ok_from_code(result)?;
383		Ok(())
384	}
385
386
387	// path- fs operations //
388
389	/// Creates the given `path` in the `Data/<gameid>` folder.
390	///
391	/// It does not create intermediate folders.
392	///
393	/// Equivalent to [`sys::ffi::playdate_file::mkdir`]
394	#[doc(alias = "sys::ffi::playdate_file::mkdir")]
395	pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result<(), ApiError> {
396		let path = CString::new(path.as_ref())?;
397		let f = self.0.mkdir();
398		let result = unsafe { f(path.as_ptr() as _) };
399		Error::ok_from_code(result)?;
400		Ok(())
401	}
402
403	/// Deletes the file at path.
404	/// Directory is a file too, definitely.
405	///
406	/// See also [`remove_dir_all`](Fs::remove_dir_all).
407	///
408	/// Equivalent to [`sys::ffi::playdate_file::unlink`]
409	#[doc(alias = "sys::ffi::playdate_file::unlink")]
410	pub fn remove<P: AsRef<Path>>(&self, path: P) -> Result<(), ApiError> {
411		let path = CString::new(path.as_ref())?;
412		let f = self.0.unlink();
413		let result = unsafe { f(path.as_ptr() as _, 0) };
414		Error::ok_from_code(result)?;
415		Ok(())
416	}
417
418	/// Deletes the file at path.
419	///
420	/// If the `path` is a folder, this deletes everything inside the folder
421	/// (including folders, folders inside those, and so on) as well as the folder itself.
422	///
423	/// Equivalent to [`sys::ffi::playdate_file::unlink`]
424	#[doc(alias = "sys::ffi::playdate_file::unlink")]
425	pub fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> Result<(), ApiError> {
426		let path = CString::new(path.as_ref())?;
427		let f = self.0.unlink();
428		let result = unsafe { f(path.as_ptr() as _, 1) };
429		Error::ok_from_code(result)?;
430		Ok(())
431	}
432
433	/// Renames the file at `from` to `to`.
434	///
435	/// It will overwrite the file at `to`.
436	///
437	/// It does not create intermediate folders.
438	///
439	/// Equivalent to [`sys::ffi::playdate_file::rename`]
440	#[doc(alias = "sys::ffi::playdate_file::rename")]
441	pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(&self, from: P, to: Q) -> Result<(), ApiError> {
442		let from = CString::new(from.as_ref())?;
443		let to = CString::new(to.as_ref())?;
444		let f = self.0.rename();
445		let result = unsafe { f(from.as_ptr() as _, to.as_ptr() as _) };
446		Error::ok_from_code(result)?;
447		Ok(())
448	}
449
450
451	// read dir //
452
453	/// Calls the given callback function for every file at `path`.
454	///
455	/// Subfolders are indicated by a trailing slash '/' in filename.
456	///
457	/// This method does not recurse into subfolders.
458	///
459	/// If `include_hidden` is set, files beginning with a period will be included;
460	/// otherwise, they are skipped.
461	///
462	/// Returns error if no folder exists at path or it can’t be opened.
463	///
464	/// Equivalent to [`sys::ffi::playdate_file::listfiles`]
465	#[doc(alias = "sys::ffi::playdate_file::listfiles")]
466	pub fn read_dir<P, Fn>(&self, path: P, mut callback: Fn, include_hidden: bool) -> Result<(), ApiError>
467		where P: AsRef<Path>,
468		      Fn: FnMut(String) {
469		unsafe extern "C" fn proxy<Fn: FnMut(String)>(filename: *const c_char, userdata: *mut c_void) {
470			// TODO: are we really need `into_owned` for storing in the temp vec?
471			let filename = CStr::from_ptr(filename as _).to_string_lossy().into_owned();
472
473			if let Some(callback) = (userdata as *mut _ as *mut Fn).as_mut() {
474				callback(filename);
475			} else {
476				panic!("Fs.read_dir: missed callback");
477			}
478		}
479
480		let path = CString::new(path.as_ref())?;
481
482		// NOTE: that's safe because ref dies after internal listfiles() returns.
483		let callback_ref = (&mut callback) as *mut Fn as *mut _;
484
485		let f = self.0.listfiles();
486		let result = unsafe {
487			f(
488			  path.as_ptr() as _,
489			  Some(proxy::<Fn>),
490			  callback_ref,
491			  include_hidden as _,
492			)
493		};
494		Error::ok_from_code(result)?;
495		Ok(())
496	}
497}
498
499
500pub mod prelude {
501	pub use sys::ffi::FileStat;
502	pub use sys::ffi::FileOptions;
503	pub use crate::error::ApiError as FsApiError;
504	pub use crate::error::Error as FsError;
505	pub use crate::Path;
506	pub use crate::Fs;
507	pub use crate::file::*;
508	pub use crate::options::*;
509	pub use crate::seek::SeekFrom;
510}