blob: f642e7cb074ec84d55e3ccec7d00c0037872784a [file] [log] [blame]
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<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!("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<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<'_>) -> 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 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::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
}
}