physfs_rs/
lib.rs

1//!This crate aims at safely wrapping PhysicsFs, a library that implements a virtual file system
2//!mainly for moddable videogames. Still WIP.
3
4use physfs_sys;
5use std::fmt;
6use std::sync::Mutex;
7
8use lazy_static::lazy_static;
9
10lazy_static! {
11    static ref INSTANCED: Mutex<bool> = Mutex::new(false);
12}
13
14///Represents an error returned by PhysicsFs
15pub struct PhysFsError {
16    code: physfs_sys::PHYSFS_ErrorCode,
17}
18
19impl PhysFsError {
20    pub(crate) fn new(code: physfs_sys::PHYSFS_ErrorCode) -> Self {
21        Self { code }
22    }
23
24    pub fn get_text(&self) -> String {
25        let v = unsafe {
26            std::ffi::CStr::from_ptr(physfs_sys::PHYSFS_getErrorByCode(self.code))
27                .to_bytes()
28                .to_vec()
29        };
30
31        String::from_utf8(v).unwrap()
32    }
33}
34
35impl fmt::Debug for PhysFsError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_struct("PhysFsError")
38            .field("code", &self.code)
39            .field("text", &self.get_text())
40            .finish()
41    }
42}
43
44impl fmt::Display for PhysFsError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}", self.get_text())
47    }
48}
49
50use std::error::Error;
51
52impl Error for PhysFsError {
53    fn source(&self) -> Option<&(dyn Error + 'static)> {
54        None
55    }
56}
57
58///A handle to a file opend through PhysicsFs
59pub struct PhysFsHandle {
60    handle: *mut physfs_sys::PHYSFS_File,
61}
62
63impl PhysFsHandle {
64    pub(crate) fn new(handle: *mut physfs_sys::PHYSFS_File) -> Self {
65        Self { handle }
66    }
67
68    pub fn file_length(&self) -> u64 {
69        unsafe { physfs_sys::PHYSFS_fileLength(self.handle) as u64 }
70    }
71
72    pub fn read_to_vec(&mut self) -> std::io::Result<Vec<u8>> {
73        let len = self.file_length() as usize;
74        let mut v = Vec::with_capacity(len);
75        v.resize(len, 0);
76        std::io::Read::read(self, v.as_mut_slice())?;
77        Ok(v)
78    }
79
80    pub fn close(self) {}
81}
82
83//PhysFs is supposed to manage concurrent access on its own
84//through a global mutex so it should be thread safe
85unsafe impl Send for PhysFsHandle {}
86unsafe impl Sync for PhysFsHandle {}
87
88impl std::io::Read for PhysFsHandle {
89    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
90        let read_bytes = unsafe {
91            physfs_sys::PHYSFS_readBytes(
92                self.handle,
93                buf.as_mut_ptr() as *mut std::ffi::c_void,
94                buf.len() as u64,
95            )
96        };
97        if read_bytes == -1 {
98            Err(std::io::Error::new(
99                std::io::ErrorKind::Other,
100                get_last_error().err().unwrap(),
101            ))
102        } else {
103            Ok(read_bytes as usize)
104        }
105    }
106}
107
108impl std::io::Write for PhysFsHandle {
109    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
110        Ok(unsafe {
111            physfs_sys::PHYSFS_writeBytes(
112                self.handle,
113                buf.as_ptr() as *const std::ffi::c_void,
114                buf.len() as u64,
115            ) as usize
116        })
117    }
118
119    fn flush(&mut self) -> std::io::Result<()> {
120        unsafe {
121            let ret = physfs_sys::PHYSFS_flush(self.handle);
122            if ret != 0 {
123                return Err(std::io::Error::new(
124                    std::io::ErrorKind::Other,
125                    get_last_error().err().unwrap(),
126                ));
127            }
128        }
129        Ok(())
130    }
131}
132
133use std::io::SeekFrom;
134impl std::io::Seek for PhysFsHandle {
135    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
136        match pos {
137            SeekFrom::Start(pos) => unsafe {
138                let ret = physfs_sys::PHYSFS_seek(self.handle, pos);
139                if ret == 0 {
140                    Err(std::io::Error::new(
141                        std::io::ErrorKind::Other,
142                        get_last_error().err().unwrap(),
143                    ))
144                } else {
145                    Ok(physfs_sys::PHYSFS_tell(self.handle) as u64)
146                }
147            },
148            SeekFrom::Current(pos) => unsafe {
149                let cpos = physfs_sys::PHYSFS_tell(self.handle);
150                self.seek(SeekFrom::Start((cpos + pos) as u64))
151            },
152            SeekFrom::End(pos) => unsafe {
153                let cpos = physfs_sys::PHYSFS_fileLength(self.handle);
154                self.seek(SeekFrom::Start((cpos + pos) as u64))
155            },
156        }
157    }
158}
159
160impl Drop for PhysFsHandle {
161    fn drop(&mut self) {
162        unsafe {
163            let _res = physfs_sys::PHYSFS_close(self.handle);
164            //TODO check error code?
165        }
166    }
167}
168
169fn get_last_error() -> Result<(), PhysFsError> {
170    Err(unsafe { PhysFsError::new(physfs_sys::PHYSFS_getLastErrorCode()) })
171}
172
173///This struct doesn't store any state, his sole purpose is to ensure that all PhysicsFs calls
174///are done after calling his init function. Only ine instance of it can exist at any given time.
175///This is because PhysicsFs has a global state so this struct makes sure rust borrowing rules are
176///enforced
177pub struct PhysFs {}
178
179fn create_ctsr(s: impl AsRef<str>) -> std::ffi::CString {
180    std::ffi::CString::new(s.as_ref().as_bytes()).unwrap()
181}
182
183macro_rules! to_cstr {
184    ($s:expr) => {
185        create_ctsr($s).as_bytes().as_ptr() as *const i8
186    };
187}
188
189impl PhysFs {
190    fn new() -> Self {
191        unsafe {
192            physfs_sys::PHYSFS_init(std::ptr::null());
193        }
194
195        Self {}
196    }
197
198    ///When calling this method for the first time an instance of Self will be returned, if called
199    ///again it will return None ensuring that no more than one instance exists
200    pub fn get() -> Option<Self> {
201        let mut instanced_lock = INSTANCED.lock().unwrap();
202        let ret = if !*instanced_lock { Some(Self::new()) } else { None };
203        *instanced_lock = true;
204
205        ret
206    }
207
208    ///Mounts `dir` to `mount_point`.
209    ///`dir` can either be a path to an archive of a supported format or to a directory
210    pub fn mount(
211        &mut self,
212        dir: impl AsRef<str>,
213        mount_point: impl AsRef<str>,
214        append: bool,
215    ) -> Result<(), PhysFsError> {
216        unsafe {
217            if 0 == physfs_sys::PHYSFS_mount(
218                to_cstr!(dir),
219                to_cstr!(mount_point),
220                if append { 1 } else { 0 },
221            ) {
222                get_last_error()?;
223            }
224        }
225
226        Ok(())
227    }
228
229    unsafe fn make_handle(
230        &self,
231        handle: *mut physfs_sys::PHYSFS_File,
232    ) -> Result<PhysFsHandle, PhysFsError> {
233        if handle == std::ptr::null_mut() {
234            get_last_error()?;
235        }
236
237        Ok(PhysFsHandle::new(handle))
238    }
239
240    ///Open file inside virtual fs for reading
241    pub fn open_read(&self, path: impl AsRef<str>) -> Result<PhysFsHandle, PhysFsError> {
242        unsafe { self.make_handle(physfs_sys::PHYSFS_openRead(to_cstr!(path))) }
243    }
244
245    ///Open file inside virtual fs for writing. If the file already exists it will be truncated
246    pub fn open_write(&self, path: impl AsRef<str>) -> Result<PhysFsHandle, PhysFsError> {
247        unsafe { self.make_handle(physfs_sys::PHYSFS_openWrite(to_cstr!(path))) }
248    }
249
250    ///Open file inside virtual fs for writing. If the file already exists new writes will be
251    ///appended to the existing contents
252    pub fn open_append(&self, path: impl AsRef<str>) -> Result<PhysFsHandle, PhysFsError> {
253        unsafe { self.make_handle(physfs_sys::PHYSFS_openAppend(to_cstr!(path))) }
254    }
255
256    ///Enumerates the files in a given searchpath directory. None if erros occur
257    pub fn enumerate_files(&self, path: impl AsRef<str>) -> Option<Vec<String>> {
258        let mut list = unsafe {physfs_sys::PHYSFS_enumerateFiles(to_cstr!(path))};
259        if list == std::ptr::null_mut() {
260            return None;
261        }
262
263        let mut res = vec![];
264        while unsafe{*list} != std::ptr::null_mut() {
265            unsafe {
266                let filename = std::ffi::CStr::from_ptr(*list).to_str().unwrap();
267                res.push(filename.to_owned());
268            }
269            list = ((list as usize) + std::mem::size_of_val(&list)) as _;
270        }
271
272        return Some(res);
273    }
274
275    pub fn is_directory(&self, path: impl AsRef<str>) -> bool {
276        unsafe{
277            physfs_sys::PHYSFS_isDirectory(to_cstr!(path)) == 1
278        }
279    }
280}
281
282impl Drop for PhysFs {
283    fn drop(&mut self) {
284        let mut instanced_lock = INSTANCED.lock().unwrap();
285        *instanced_lock = false;
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use std::io::*;
293
294    //tests seem to run in parallel threads, wait for the instance to be available
295    fn wait_instance() -> PhysFs {
296        let mut instance = None;
297        while instance.is_none() {
298            instance = PhysFs::get();
299        }
300        instance.unwrap()
301    }
302
303    const TEST_TEXT: &'static str = "testing\n";
304    #[test]
305    fn it_works() {
306        let mut fs = wait_instance();
307        fs.mount("alol.zip", "", true).err().unwrap(); //muest give none
308        fs.mount("test.zip", "", true).unwrap();
309        fs.mount("./", "", true).unwrap();
310        let mut handle = fs.open_read("test").unwrap();
311
312        assert_eq!(handle.file_length(), 8);
313        let mut buf = [0; 8];
314        handle.read(&mut buf).unwrap();
315        assert_eq!(String::from_utf8_lossy(&buf), TEST_TEXT);
316        handle.close();
317
318        let mut handle = fs.open_read("test").unwrap();
319        assert_eq!(handle.read_to_vec().unwrap(), TEST_TEXT.as_bytes().to_vec());
320
321        assert!(PhysFs::get().is_none());
322    }
323
324    #[test]
325    fn files_enumeration() {
326        let mut fs = wait_instance();
327        fs.mount("test.zip", "", true).unwrap();
328
329        let files = fs.enumerate_files("/").unwrap();
330        assert_eq!(vec!["test"], files);
331        assert!(!fs.is_directory("test"));
332        fs.mount("./", "/", true);
333        assert!(fs.is_directory("src"));
334    }
335}