1use crate::raw;
7use crate::sanitize_cstring;
8use enumflags2::{BitFlags, bitflags};
9use std::ffi::CStr;
10use std::marker::PhantomData;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct VfsInterfaceVersion(u32);
14
15impl VfsInterfaceVersion {
16 pub fn new(value: u32) -> Self {
17 Self(value)
18 }
19
20 pub fn get(self) -> u32 {
21 self.0
22 }
23}
24
25#[bitflags]
26#[repr(u32)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum VfsFileAccess {
29 Read = raw::RETRO_VFS_FILE_ACCESS_READ,
30 Write = raw::RETRO_VFS_FILE_ACCESS_WRITE,
31 UpdateExisting = raw::RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING,
32}
33
34pub type VfsFileAccessFlags = BitFlags<VfsFileAccess>;
35
36#[bitflags]
37#[repr(u32)]
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum VfsFileAccessHint {
40 FrequentAccess = raw::RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS,
41}
42
43pub type VfsFileAccessHints = BitFlags<VfsFileAccessHint>;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum VfsSeekPosition {
47 Start,
48 Current,
49 End,
50}
51
52impl VfsSeekPosition {
53 pub(crate) fn as_raw(self) -> i32 {
54 match self {
55 Self::Start => raw::RETRO_VFS_SEEK_POSITION_START,
56 Self::Current => raw::RETRO_VFS_SEEK_POSITION_CURRENT,
57 Self::End => raw::RETRO_VFS_SEEK_POSITION_END,
58 }
59 }
60}
61
62#[bitflags]
63#[repr(u32)]
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum VfsStatFlag {
66 Valid = raw::RETRO_VFS_STAT_IS_VALID,
67 Directory = raw::RETRO_VFS_STAT_IS_DIRECTORY,
68 CharacterSpecial = raw::RETRO_VFS_STAT_IS_CHARACTER_SPECIAL,
69}
70
71pub type VfsStatFlags = BitFlags<VfsStatFlag>;
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub struct VfsMetadata {
75 pub flags: VfsStatFlags,
76 pub size: Option<i32>,
77}
78
79#[derive(Clone, Copy, Debug)]
80pub struct VfsInterface {
81 version: VfsInterfaceVersion,
82 raw: raw::retro_vfs_interface,
83}
84
85impl VfsInterface {
86 pub(crate) fn new(version: VfsInterfaceVersion, raw: raw::retro_vfs_interface) -> Self {
87 Self { version, raw }
88 }
89
90 pub fn version(self) -> VfsInterfaceVersion {
91 self.version
92 }
93
94 pub fn open_file(
95 self,
96 path: &str,
97 access: VfsFileAccessFlags,
98 hints: VfsFileAccessHints,
99 ) -> Option<VfsFile> {
100 let open = self.raw.open?;
101 let path = sanitize_cstring(path);
102 let handle = unsafe { open(path.as_ptr(), access.bits(), hints.bits()) };
104 if handle.is_null() {
105 None
106 } else {
107 Some(VfsFile {
108 handle,
109 raw: self.raw,
110 closed: false,
111 _not_send_sync: PhantomData,
112 })
113 }
114 }
115
116 pub fn remove_file(self, path: &str) -> bool {
117 let Some(remove) = self.raw.remove else {
118 return false;
119 };
120 let path = sanitize_cstring(path);
121 unsafe { remove(path.as_ptr()) == 0 }
123 }
124
125 pub fn rename(self, old_path: &str, new_path: &str) -> bool {
126 let Some(rename) = self.raw.rename else {
127 return false;
128 };
129 let old_path = sanitize_cstring(old_path);
130 let new_path = sanitize_cstring(new_path);
131 unsafe { rename(old_path.as_ptr(), new_path.as_ptr()) == 0 }
133 }
134
135 pub fn stat(self, path: &str) -> Option<VfsMetadata> {
136 let stat = self.raw.stat?;
137 let path = sanitize_cstring(path);
138 let mut size = 0i32;
139 let flags = unsafe { stat(path.as_ptr(), &mut size as *mut i32) };
141 if flags == 0 {
142 None
143 } else {
144 Some(VfsMetadata {
145 flags: VfsStatFlags::from_bits_truncate(flags as u32),
146 size: Some(size),
147 })
148 }
149 }
150
151 pub fn create_dir(self, path: &str) -> bool {
152 let Some(mkdir) = self.raw.mkdir else {
153 return false;
154 };
155 let path = sanitize_cstring(path);
156 unsafe { mkdir(path.as_ptr()) == 0 }
158 }
159
160 pub fn open_dir(self, path: &str, include_hidden: bool) -> Option<VfsDirectory> {
161 let opendir = self.raw.opendir?;
162 let path = sanitize_cstring(path);
163 let handle = unsafe { opendir(path.as_ptr(), include_hidden) };
165 if handle.is_null() {
166 None
167 } else {
168 Some(VfsDirectory {
169 handle,
170 raw: self.raw,
171 closed: false,
172 _not_send_sync: PhantomData,
173 })
174 }
175 }
176}
177
178#[derive(Debug)]
179pub struct VfsFile {
180 handle: *mut raw::retro_vfs_file_handle,
181 raw: raw::retro_vfs_interface,
182 closed: bool,
183 _not_send_sync: PhantomData<*mut ()>,
184}
185
186impl VfsFile {
187 pub fn path(&self) -> Option<String> {
188 let get_path = self.raw.get_path?;
189 let path = unsafe { get_path(self.handle) };
191 if path.is_null() {
192 None
193 } else {
194 Some(
196 unsafe { CStr::from_ptr(path) }
197 .to_string_lossy()
198 .into_owned(),
199 )
200 }
201 }
202
203 pub fn size(&self) -> Option<i64> {
204 let size = self.raw.size?;
205 nonnegative_i64(unsafe { size(self.handle) })
207 }
208
209 pub fn truncate(&mut self, length: i64) -> bool {
210 let Some(truncate) = self.raw.truncate else {
211 return false;
212 };
213 unsafe { truncate(self.handle, length) == 0 }
215 }
216
217 pub fn tell(&self) -> Option<i64> {
218 let tell = self.raw.tell?;
219 nonnegative_i64(unsafe { tell(self.handle) })
221 }
222
223 pub fn seek(&mut self, offset: i64, position: VfsSeekPosition) -> Option<i64> {
224 let seek = self.raw.seek?;
225 nonnegative_i64(unsafe { seek(self.handle, offset, position.as_raw()) })
227 }
228
229 pub fn read(&mut self, buffer: &mut [u8]) -> Option<usize> {
230 let read = self.raw.read?;
231 nonnegative_i64(unsafe {
233 read(
234 self.handle,
235 buffer.as_mut_ptr().cast::<std::ffi::c_void>(),
236 buffer.len() as u64,
237 )
238 })
239 .and_then(|value| usize::try_from(value).ok())
240 }
241
242 pub fn write(&mut self, buffer: &[u8]) -> Option<usize> {
243 let write = self.raw.write?;
244 nonnegative_i64(unsafe {
246 write(
247 self.handle,
248 buffer.as_ptr().cast::<std::ffi::c_void>(),
249 buffer.len() as u64,
250 )
251 })
252 .and_then(|value| usize::try_from(value).ok())
253 }
254
255 pub fn flush(&mut self) -> bool {
256 let Some(flush) = self.raw.flush else {
257 return false;
258 };
259 unsafe { flush(self.handle) == 0 }
261 }
262
263 pub fn close(mut self) -> bool {
264 self.close_inner()
265 }
266
267 fn close_inner(&mut self) -> bool {
268 if self.closed {
269 return true;
270 }
271 self.closed = true;
272 let Some(close) = self.raw.close else {
273 return false;
274 };
275 unsafe { close(self.handle) == 0 }
278 }
279}
280
281impl Drop for VfsFile {
282 fn drop(&mut self) {
283 let _ = self.close_inner();
284 }
285}
286
287#[derive(Debug)]
288pub struct VfsDirectory {
289 handle: *mut raw::retro_vfs_dir_handle,
290 raw: raw::retro_vfs_interface,
291 closed: bool,
292 _not_send_sync: PhantomData<*mut ()>,
293}
294
295impl VfsDirectory {
296 pub fn read_next(&mut self) -> bool {
297 let Some(readdir) = self.raw.readdir else {
298 return false;
299 };
300 unsafe { readdir(self.handle) }
302 }
303
304 pub fn entry_name(&self) -> Option<String> {
305 let get_name = self.raw.dirent_get_name?;
306 let name = unsafe { get_name(self.handle) };
308 if name.is_null() {
309 None
310 } else {
311 Some(
313 unsafe { CStr::from_ptr(name) }
314 .to_string_lossy()
315 .into_owned(),
316 )
317 }
318 }
319
320 pub fn entry_is_dir(&self) -> bool {
321 let Some(is_dir) = self.raw.dirent_is_dir else {
322 return false;
323 };
324 unsafe { is_dir(self.handle) }
326 }
327
328 pub fn close(mut self) -> bool {
329 self.close_inner()
330 }
331
332 fn close_inner(&mut self) -> bool {
333 if self.closed {
334 return true;
335 }
336 self.closed = true;
337 let Some(closedir) = self.raw.closedir else {
338 return false;
339 };
340 unsafe { closedir(self.handle) == 0 }
342 }
343}
344
345impl Drop for VfsDirectory {
346 fn drop(&mut self) {
347 let _ = self.close_inner();
348 }
349}
350
351fn nonnegative_i64(value: i64) -> Option<i64> {
352 (value >= 0).then_some(value)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn vfs_flags_encode_libretro_values() {
361 let access = VfsFileAccessFlags::from(VfsFileAccess::Read)
362 | VfsFileAccess::Write
363 | VfsFileAccess::UpdateExisting;
364 assert_eq!(
365 access.bits(),
366 raw::RETRO_VFS_FILE_ACCESS_READ_WRITE | raw::RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING
367 );
368 assert_eq!(
369 VfsFileAccessHints::from(VfsFileAccessHint::FrequentAccess).bits(),
370 raw::RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS
371 );
372 assert_eq!(
373 VfsSeekPosition::End.as_raw(),
374 raw::RETRO_VFS_SEEK_POSITION_END
375 );
376 assert_eq!(
377 (VfsStatFlags::from(VfsStatFlag::Valid) | VfsStatFlag::Directory).bits(),
378 raw::RETRO_VFS_STAT_IS_VALID | raw::RETRO_VFS_STAT_IS_DIRECTORY
379 );
380 }
381}