use crate::ffi::{OsString, c_char}; use crate::fmt; use crate::fs::TryLockError; use crate::hash::Hash; use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom}; use crate::path::{Path, PathBuf}; use crate::sys::common::small_c_string::run_path_with_cstr; use crate::sys::time::SystemTime; use crate::sys::{unsupported, unsupported_err}; #[expect(dead_code)] #[path = "unsupported.rs"] mod unsupported_fs; pub use unsupported_fs::{ DirBuilder, FileTimes, canonicalize, link, readlink, remove_dir_all, rename, rmdir, symlink, unlink, }; /// VEXos file descriptor. /// /// This stores an opaque pointer to a [FatFs file object structure] managed by VEXos /// representing an open file on disk. /// /// [FatFs file object structure]: https://github.com/Xilinx/embeddedsw/blob/master/lib/sw_services/xilffs/src/include/ff.h?rgh-link-date=2025-09-23T20%3A03%3A43Z#L215 /// /// # Safety /// /// Since this platform uses a pointer to to an internal filesystem structure with a lifetime /// associated with it (rather than a UNIX-style file descriptor table), care must be taken to /// ensure that the pointer held by `FileDesc` is valid for as long as it exists. #[derive(Debug)] struct FileDesc(*mut vex_sdk::FIL); // SAFETY: VEXos's FDs can be used on a thread other than the one they were created on. unsafe impl Send for FileDesc {} // SAFETY: We assume an environment without threads (i.e. no RTOS). // (If there were threads, it is possible that a mutex would be required.) unsafe impl Sync for FileDesc {} pub struct File { fd: FileDesc, } #[derive(Clone)] pub enum FileAttr { Dir, File { size: u64 }, } pub struct ReadDir(!); pub struct DirEntry { path: PathBuf, } #[derive(Clone, Debug)] pub struct OpenOptions { read: bool, write: bool, append: bool, truncate: bool, create: bool, create_new: bool, } #[derive(Clone, PartialEq, Eq, Debug)] pub struct FilePermissions {} #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct FileType { is_dir: bool, } impl FileAttr { pub fn size(&self) -> u64 { match self { Self::File { size } => *size, Self::Dir => 0, } } pub fn perm(&self) -> FilePermissions { FilePermissions {} } pub fn file_type(&self) -> FileType { FileType { is_dir: matches!(self, FileAttr::Dir) } } pub fn modified(&self) -> io::Result { unsupported() } pub fn accessed(&self) -> io::Result { unsupported() } pub fn created(&self) -> io::Result { unsupported() } } impl FilePermissions { pub fn readonly(&self) -> bool { false } pub fn set_readonly(&mut self, _readonly: bool) { panic!("Perimissions do not exist") } } impl FileType { pub fn is_dir(&self) -> bool { self.is_dir } pub fn is_file(&self) -> bool { !self.is_dir } pub fn is_symlink(&self) -> bool { // No symlinks in VEXos - entries are either files or directories. false } } impl fmt::Debug for ReadDir { fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0 } } impl Iterator for ReadDir { type Item = io::Result; fn next(&mut self) -> Option> { self.0 } } impl DirEntry { pub fn path(&self) -> PathBuf { self.path.clone() } pub fn file_name(&self) -> OsString { self.path.file_name().unwrap_or_default().into() } pub fn metadata(&self) -> io::Result { stat(&self.path) } pub fn file_type(&self) -> io::Result { Ok(self.metadata()?.file_type()) } } impl OpenOptions { pub fn new() -> OpenOptions { OpenOptions { read: false, write: false, append: false, truncate: false, create: false, create_new: false, } } pub fn read(&mut self, read: bool) { self.read = read; } pub fn write(&mut self, write: bool) { self.write = write; } pub fn append(&mut self, append: bool) { self.append = append; } pub fn truncate(&mut self, truncate: bool) { self.truncate = truncate; } pub fn create(&mut self, create: bool) { self.create = create; } pub fn create_new(&mut self, create_new: bool) { self.create_new = create_new; } } impl File { pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { run_path_with_cstr(path, &|path| { // Enforce the invariants of `create_new`/`create`. // // Since VEXos doesn't have anything akin to POSIX's `oflags`, we need to enforce // the requirements that `create_new` can't have an existing file and `!create` // doesn't create a file ourselves. if !opts.read && (opts.write || opts.append) && (opts.create_new || !opts.create) { let status = unsafe { vex_sdk::vexFileStatus(path.as_ptr()) }; if opts.create_new && status != 0 { return Err(io::const_error!(io::ErrorKind::AlreadyExists, "file exists",)); } else if !opts.create && status == 0 { return Err(io::const_error!( io::ErrorKind::NotFound, "no such file or directory", )); } } let file = match opts { // read + write - unsupported OpenOptions { read: true, write: true, .. } => { return Err(io::const_error!( io::ErrorKind::InvalidInput, "opening files with read and write access is unsupported on this target", )); } // read OpenOptions { read: true, write: false, append: _, truncate: false, create: false, create_new: false, } => unsafe { vex_sdk::vexFileOpen(path.as_ptr(), c"".as_ptr()) }, // append OpenOptions { read: false, write: _, append: true, truncate: false, create: _, create_new: _, } => unsafe { vex_sdk::vexFileOpenWrite(path.as_ptr()) }, // write OpenOptions { read: false, write: true, append: false, truncate, create: _, create_new: _, } => unsafe { if *truncate { vex_sdk::vexFileOpenCreate(path.as_ptr()) } else { // Open in append, but jump to the start of the file. let fd = vex_sdk::vexFileOpenWrite(path.as_ptr()); vex_sdk::vexFileSeek(fd, 0, 0); fd } }, _ => { return Err(io::const_error!(io::ErrorKind::InvalidInput, "invalid argument")); } }; if file.is_null() { Err(io::const_error!(io::ErrorKind::NotFound, "could not open file")) } else { Ok(Self { fd: FileDesc(file) }) } }) } pub fn file_attr(&self) -> io::Result { // `vexFileSize` returns -1 upon error, so u64::try_from will fail on error. if let Ok(size) = u64::try_from(unsafe { // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. vex_sdk::vexFileSize(self.fd.0) }) { Ok(FileAttr::File { size }) } else { Err(io::const_error!(io::ErrorKind::InvalidData, "failed to get file size")) } } pub fn fsync(&self) -> io::Result<()> { self.flush() } pub fn datasync(&self) -> io::Result<()> { self.flush() } pub fn lock(&self) -> io::Result<()> { unsupported() } pub fn lock_shared(&self) -> io::Result<()> { unsupported() } pub fn try_lock(&self) -> Result<(), TryLockError> { Err(TryLockError::Error(unsupported_err())) } pub fn try_lock_shared(&self) -> Result<(), TryLockError> { Err(TryLockError::Error(unsupported_err())) } pub fn unlock(&self) -> io::Result<()> { unsupported() } pub fn truncate(&self, _size: u64) -> io::Result<()> { unsupported() } pub fn read(&self, buf: &mut [u8]) -> io::Result { let len = buf.len() as u32; let buf_ptr = buf.as_mut_ptr(); let read = unsafe { // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. vex_sdk::vexFileRead(buf_ptr.cast::(), 1, len, self.fd.0) }; if read < 0 { Err(io::const_error!(io::ErrorKind::Other, "could not read from file")) } else { Ok(read as usize) } } pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { crate::io::default_read_vectored(|b| self.read(b), bufs) } #[inline] pub fn is_read_vectored(&self) -> bool { false } pub fn read_buf(&self, cursor: BorrowedCursor<'_>) -> io::Result<()> { crate::io::default_read_buf(|b| self.read(b), cursor) } pub fn write(&self, buf: &[u8]) -> io::Result { let len = buf.len() as u32; let buf_ptr = buf.as_ptr(); let written = unsafe { // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. vex_sdk::vexFileWrite(buf_ptr.cast_mut().cast::(), 1, len, self.fd.0) }; if written < 0 { Err(io::const_error!(io::ErrorKind::Other, "could not write to file")) } else { Ok(written as usize) } } pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { crate::io::default_write_vectored(|b| self.write(b), bufs) } #[inline] pub fn is_write_vectored(&self) -> bool { false } pub fn flush(&self) -> io::Result<()> { unsafe { // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. vex_sdk::vexFileSync(self.fd.0); } Ok(()) } pub fn tell(&self) -> io::Result { // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. let position = unsafe { vex_sdk::vexFileTell(self.fd.0) }; position.try_into().map_err(|_| { io::const_error!(io::ErrorKind::InvalidData, "failed to get current location in file") }) } pub fn size(&self) -> Option> { None } pub fn seek(&self, pos: SeekFrom) -> io::Result { const SEEK_SET: i32 = 0; const SEEK_CUR: i32 = 1; const SEEK_END: i32 = 2; fn try_convert_offset>(offset: T) -> io::Result { offset.try_into().map_err(|_| { io::const_error!( io::ErrorKind::InvalidInput, "cannot seek to an offset too large to fit in a 32 bit integer", ) }) } // SAFETY: `self.fd` contains a valid pointer to `FIL` for this struct's lifetime. match pos { SeekFrom::Start(offset) => unsafe { map_fresult(vex_sdk::vexFileSeek(self.fd.0, try_convert_offset(offset)?, SEEK_SET))? }, SeekFrom::End(offset) => unsafe { if offset >= 0 { map_fresult(vex_sdk::vexFileSeek( self.fd.0, try_convert_offset(offset)?, SEEK_END, ))? } else { // `vexFileSeek` does not support seeking with negative offset, meaning // we have to calculate the offset from the end of the file ourselves. // Seek to the end of the file to get the end position in the open buffer. map_fresult(vex_sdk::vexFileSeek(self.fd.0, 0, SEEK_END))?; let end_position = self.tell()?; map_fresult(vex_sdk::vexFileSeek( self.fd.0, // NOTE: Files internally use a 32-bit representation for stream // position, so `end_position as i64` should never overflow. try_convert_offset(end_position as i64 + offset)?, SEEK_SET, ))? } }, SeekFrom::Current(offset) => unsafe { if offset >= 0 { map_fresult(vex_sdk::vexFileSeek( self.fd.0, try_convert_offset(offset)?, SEEK_CUR, ))? } else { // `vexFileSeek` does not support seeking with negative offset, meaning // we have to calculate the offset from the stream position ourselves. map_fresult(vex_sdk::vexFileSeek( self.fd.0, try_convert_offset((self.tell()? as i64) + offset)?, SEEK_SET, ))? } }, } Ok(self.tell()?) } pub fn duplicate(&self) -> io::Result { unsupported() } pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> { unsupported() } pub fn set_times(&self, _times: FileTimes) -> io::Result<()> { unsupported() } } impl fmt::Debug for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("File").field("fd", &self.fd.0).finish() } } impl Drop for File { fn drop(&mut self) { unsafe { vex_sdk::vexFileClose(self.fd.0) }; } } pub fn readdir(_p: &Path) -> io::Result { // While there *is* a userspace function for reading file directories, // the necessary implementation cannot currently be done cleanly, as // VEXos does not expose directory length to user programs. // // This means that we would need to create a large fixed-length buffer // and hope that the folder's contents didn't exceed that buffer's length, // which obviously isn't behavior we want to rely on in the standard library. unsupported() } pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> { unsupported() } pub fn exists(path: &Path) -> io::Result { run_path_with_cstr(path, &|path| Ok(unsafe { vex_sdk::vexFileStatus(path.as_ptr()) } != 0)) } pub fn stat(p: &Path) -> io::Result { // `vexFileStatus` returns 3 if the given path is a directory, 1 if the path is a // file, or 0 if no such path exists. const FILE_STATUS_DIR: u32 = 3; run_path_with_cstr(p, &|c_path| { let file_type = unsafe { vex_sdk::vexFileStatus(c_path.as_ptr()) }; // We can't get the size if its a directory because we cant open it as a file if file_type == FILE_STATUS_DIR { Ok(FileAttr::Dir) } else { let mut opts = OpenOptions::new(); opts.read(true); let file = File::open(p, &opts)?; file.file_attr() } }) } pub fn lstat(p: &Path) -> io::Result { // Symlinks aren't supported in this filesystem stat(p) } // Cannot use `copy` from `common` here, since `File::set_permissions` is unsupported on this target. pub fn copy(from: &Path, to: &Path) -> io::Result { use crate::fs::File; // NOTE: If `from` is a directory, this call should fail due to vexFileOpen* returning null. let mut reader = File::open(from)?; let mut writer = File::create(to)?; io::copy(&mut reader, &mut writer) } fn map_fresult(fresult: vex_sdk::FRESULT) -> io::Result<()> { // VEX uses a derivative of FatFs (Xilinx's xilffs library) for filesystem operations. match fresult { vex_sdk::FRESULT::FR_OK => Ok(()), vex_sdk::FRESULT::FR_DISK_ERR => Err(io::const_error!( io::ErrorKind::Uncategorized, "internal function reported an unrecoverable hard error", )), vex_sdk::FRESULT::FR_INT_ERR => Err(io::const_error!( io::ErrorKind::Uncategorized, "internal error in filesystem runtime", )), vex_sdk::FRESULT::FR_NOT_READY => Err(io::const_error!( io::ErrorKind::Uncategorized, "the storage device could not be prepared to work", )), vex_sdk::FRESULT::FR_NO_FILE => Err(io::const_error!( io::ErrorKind::NotFound, "could not find the file in the directory" )), vex_sdk::FRESULT::FR_NO_PATH => Err(io::const_error!( io::ErrorKind::NotFound, "a directory in the path name could not be found", )), vex_sdk::FRESULT::FR_INVALID_NAME => Err(io::const_error!( io::ErrorKind::InvalidInput, "the given string is invalid as a path name", )), vex_sdk::FRESULT::FR_DENIED => Err(io::const_error!( io::ErrorKind::PermissionDenied, "the required access for this operation was denied", )), vex_sdk::FRESULT::FR_EXIST => Err(io::const_error!( io::ErrorKind::AlreadyExists, "an object with the same name already exists in the directory", )), vex_sdk::FRESULT::FR_INVALID_OBJECT => Err(io::const_error!( io::ErrorKind::Uncategorized, "invalid or null file/directory object", )), vex_sdk::FRESULT::FR_WRITE_PROTECTED => Err(io::const_error!( io::ErrorKind::PermissionDenied, "a write operation was performed on write-protected media", )), vex_sdk::FRESULT::FR_INVALID_DRIVE => Err(io::const_error!( io::ErrorKind::InvalidInput, "an invalid drive number was specified in the path name", )), vex_sdk::FRESULT::FR_NOT_ENABLED => Err(io::const_error!( io::ErrorKind::Uncategorized, "work area for the logical drive has not been registered", )), vex_sdk::FRESULT::FR_NO_FILESYSTEM => Err(io::const_error!( io::ErrorKind::Uncategorized, "valid FAT volume could not be found on the drive", )), vex_sdk::FRESULT::FR_MKFS_ABORTED => Err(io::const_error!( io::ErrorKind::Uncategorized, "failed to create filesystem volume" )), vex_sdk::FRESULT::FR_TIMEOUT => Err(io::const_error!( io::ErrorKind::TimedOut, "the function was canceled due to a timeout of thread-safe control", )), vex_sdk::FRESULT::FR_LOCKED => Err(io::const_error!( io::ErrorKind::Uncategorized, "the operation to the object was rejected by file sharing control", )), vex_sdk::FRESULT::FR_NOT_ENOUGH_CORE => { Err(io::const_error!(io::ErrorKind::OutOfMemory, "not enough memory for the operation")) } vex_sdk::FRESULT::FR_TOO_MANY_OPEN_FILES => Err(io::const_error!( io::ErrorKind::Uncategorized, "maximum number of open files has been reached", )), vex_sdk::FRESULT::FR_INVALID_PARAMETER => { Err(io::const_error!(io::ErrorKind::InvalidInput, "a given parameter was invalid")) } _ => unreachable!(), // C-style enum } }