| 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::helpers::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::{ | |
| Dir, 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<SystemTime> { | |
| unsupported() | |
| } | |
| pub fn accessed(&self) -> io::Result<SystemTime> { | |
| unsupported() | |
| } | |
| pub fn created(&self) -> io::Result<SystemTime> { | |
| unsupported() | |
| } | |
| } | |
| impl FilePermissions { | |
| pub fn readonly(&self) -> bool { | |
| false | |
| } | |
| pub fn set_readonly(&mut self, _readonly: bool) { | |
| panic!("Permissions 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<DirEntry>; | |
| fn next(&mut self) -> Option<io::Result<DirEntry>> { | |
| 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<FileAttr> { | |
| stat(&self.path) | |
| } | |
| pub fn file_type(&self) -> io::Result<FileType> { | |
| 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<File> { | |
| 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<FileAttr> { | |
| // `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<usize> { | |
| 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::<c_char>(), 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<usize> { | |
| 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<'_, u8>) -> io::Result<()> { | |
| crate::io::default_read_buf(|b| self.read(b), cursor) | |
| } | |
| pub fn write(&self, buf: &[u8]) -> io::Result<usize> { | |
| 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::<c_char>(), 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<usize> { | |
| 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<u64> { | |
| // 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<io::Result<u64>> { | |
| None | |
| } | |
| pub fn seek(&self, pos: SeekFrom) -> io::Result<u64> { | |
| const SEEK_SET: i32 = 0; | |
| const SEEK_CUR: i32 = 1; | |
| const SEEK_END: i32 = 2; | |
| fn try_convert_offset<T: TryInto<u32>>(offset: T) -> io::Result<u32> { | |
| 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<File> { | |
| 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<ReadDir> { | |
| // 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 set_perm_nofollow(_p: &Path, _perm: FilePermissions) -> io::Result<()> { | |
| unsupported() | |
| } | |
| pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> { | |
| unsupported() | |
| } | |
| pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> { | |
| unsupported() | |
| } | |
| pub fn exists(path: &Path) -> io::Result<bool> { | |
| run_path_with_cstr(path, &|path| Ok(unsafe { vex_sdk::vexFileStatus(path.as_ptr()) } != 0)) | |
| } | |
| pub fn stat(p: &Path) -> io::Result<FileAttr> { | |
| // `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<FileAttr> { | |
| // 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<u64> { | |
| 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::InputOutputError, | |
| "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::TooManyOpenFiles, | |
| "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 | |
| } | |
| } |