Skip to main content

libretro_core/
vfs.rs

1//! Virtual file system service-interface wrappers.
2//!
3//! `VfsInterface`, `VfsFile`, and `VfsDirectory` expose frontend-mediated file
4//! access with typed modes, hints, seek positions, stat flags, and RAII handles.
5
6use 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        // SAFETY: `path` is a valid C string for the duration of the call.
103        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        // SAFETY: `path` is a valid C string for the duration of the call.
122        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        // SAFETY: Both paths are valid C strings for the duration of the call.
132        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        // SAFETY: `path` is a valid C string and `size` is a valid out-param.
140        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        // SAFETY: `path` is a valid C string for the duration of the call.
157        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        // SAFETY: `path` is a valid C string for the duration of the call.
164        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        // SAFETY: `self.handle` is live while `self` is not closed.
190        let path = unsafe { get_path(self.handle) };
191        if path.is_null() {
192            None
193        } else {
194            // SAFETY: Frontend returns a NUL-terminated path owned by the handle.
195            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        // SAFETY: `self.handle` is live while `self` is not closed.
206        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        // SAFETY: `self.handle` is live while `self` is not closed.
214        unsafe { truncate(self.handle, length) == 0 }
215    }
216
217    pub fn tell(&self) -> Option<i64> {
218        let tell = self.raw.tell?;
219        // SAFETY: `self.handle` is live while `self` is not closed.
220        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        // SAFETY: `self.handle` is live while `self` is not closed.
226        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        // SAFETY: `buffer` is valid for writes of its length.
232        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        // SAFETY: `buffer` is valid for reads of its length.
245        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        // SAFETY: `self.handle` is live while `self` is not closed.
260        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        // SAFETY: We mark the handle closed before calling the frontend so Drop
276        // cannot close it twice even if the frontend reports failure.
277        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        // SAFETY: `self.handle` is live while `self` is not closed.
301        unsafe { readdir(self.handle) }
302    }
303
304    pub fn entry_name(&self) -> Option<String> {
305        let get_name = self.raw.dirent_get_name?;
306        // SAFETY: `self.handle` is live while `self` is not closed.
307        let name = unsafe { get_name(self.handle) };
308        if name.is_null() {
309            None
310        } else {
311            // SAFETY: The returned name is valid until the next read/close.
312            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        // SAFETY: `self.handle` is live while `self` is not closed.
325        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        // SAFETY: We mark closed before the frontend invalidates the handle.
341        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}