use alloc::{borrow::Cow, boxed::Box, string::ToString, sync::Arc};
use core::{
ffi::c_int,
hint::likely,
sync::atomic::{AtomicBool, Ordering},
};
use ax_fs_ng::vfs::{FileBackend, FileFlags, FsContext};
use ax_io::{Seek, SeekFrom};
use axfs_ng_vfs::{DirectoryCursor, DirectoryReadState, Location, Metadata, NodeFlags, VfsResult};
use axpoll::{IoEvents, Pollable};
use linux_raw_sys::{
general::{AT_EMPTY_PATH, AT_FDCWD, AT_SYMLINK_NOFOLLOW, O_APPEND, O_EXCL},
ioctl::TIOCSCTTY,
};
use super::{FileLike, Kstat, get_file_like};
use crate::{
StarryError, StarryResult,
file::{IoDst, IoSrc},
mm::VmPtr,
pseudofs::Device,
sync::Mutex,
task::{
current_user_task,
future::{block_on_user, poll_io},
},
};
const DFS_IOCTL_ATOMIC_WRITE_SET: u32 = 0x4004_9502;
pub fn with_fs<R>(
dirfd: c_int,
f: impl FnOnce(&mut FsContext) -> StarryResult<R>,
) -> StarryResult<R> {
let fs_context = ax_fs_ng::vfs::current_fs_context();
let mut fs = fs_context.lock();
if dirfd == AT_FDCWD {
f(&mut fs)
} else {
let dir = Directory::from_fd(dirfd)?.inner.clone();
f(&mut fs.with_current_dir(dir)?)
}
}
pub enum ResolveAtResult {
File(Location),
Other(Arc<dyn FileLike>),
}
impl ResolveAtResult {
pub fn into_file(self) -> Option<Location> {
match self {
Self::File(file) => Some(file),
Self::Other(_) => None,
}
}
pub fn stat(&self) -> StarryResult<Kstat> {
match self {
Self::File(file) => Ok(metadata_to_kstat(&file.metadata()?)),
Self::Other(file_like) => file_like.stat(),
}
}
}
pub fn resolve_fd(fd: c_int) -> StarryResult<ResolveAtResult> {
let file_like = get_file_like(fd)?;
let f = file_like.clone();
Ok(if let Some(file) = f.downcast_ref::<File>() {
ResolveAtResult::File(file.inner().location().clone())
} else if let Some(dir) = f.downcast_ref::<Directory>() {
ResolveAtResult::File(dir.inner().clone())
} else {
ResolveAtResult::Other(file_like)
})
}
pub fn resolve_at(dirfd: c_int, path: Option<&str>, flags: u32) -> StarryResult<ResolveAtResult> {
resolve_at_with_search(dirfd, path, flags, None)
}
pub fn resolve_at_checked(
dirfd: c_int,
path: Option<&str>,
flags: u32,
check_search: impl Fn(&Location) -> VfsResult<()>,
) -> StarryResult<ResolveAtResult> {
resolve_at_with_search(dirfd, path, flags, Some(&check_search))
}
type SearchCheck<'a> = Option<&'a dyn Fn(&Location) -> VfsResult<()>>;
fn resolve_at_with_search(
dirfd: c_int,
path: Option<&str>,
flags: u32,
search: SearchCheck<'_>,
) -> StarryResult<ResolveAtResult> {
match path {
Some("") | None => {
if flags & AT_EMPTY_PATH == 0 {
return Err(StarryError::NotFound);
}
if dirfd == AT_FDCWD {
return with_fs(dirfd, |fs| {
Ok(ResolveAtResult::File(fs.current_dir().clone()))
});
}
resolve_fd(dirfd)
}
Some(path) => {
let dirfd = if path.starts_with('/') {
AT_FDCWD
} else {
dirfd
};
with_fs(dirfd, |fs| {
let location = match (search, flags & AT_SYMLINK_NOFOLLOW != 0) {
(Some(check), true) => fs.resolve_no_follow_checked(path, check),
(Some(check), false) => fs.resolve_checked(path, check),
(None, true) => fs.resolve_no_follow(path),
(None, false) => fs.resolve(path),
}?;
Ok(ResolveAtResult::File(location))
})
}
}
}
pub fn metadata_to_kstat(metadata: &Metadata) -> Kstat {
let ty = metadata.node_type as u8;
let perm = metadata.mode.bits() as u32;
let mode = ((ty as u32) << 12) | perm;
Kstat {
dev: metadata.device,
ino: metadata.inode,
mode,
nlink: metadata.nlink as _,
uid: metadata.uid,
gid: metadata.gid,
size: metadata.size,
blksize: metadata.block_size as _,
blocks: metadata.blocks,
rdev: metadata.rdev,
atime: metadata.atime,
mtime: metadata.mtime,
ctime: metadata.ctime,
}
}
pub struct File {
inner: ax_fs_ng::File,
open_flags: u32,
nonblock: AtomicBool,
append: AtomicBool,
}
impl File {
pub fn new(inner: ax_fs_ng::File, open_flags: u32) -> Self {
Self {
inner,
open_flags,
nonblock: AtomicBool::new(false),
append: AtomicBool::new(open_flags & O_APPEND != 0),
}
}
pub fn inner(&self) -> &ax_fs_ng::File {
&self.inner
}
}
impl Drop for File {
fn drop(&mut self) {
if self.open_flags & linux_raw_sys::general::O_PATH == 0
&& let Ok(device) = self.inner.location().entry().downcast::<Device>()
{
device.inner().close(self.open_flags & O_EXCL != 0);
}
}
}
impl File {
fn is_blocking(&self) -> bool {
self.inner.location().flags().contains(NodeFlags::BLOCKING)
}
}
fn path_for(loc: &Location) -> Cow<'static, str> {
loc.absolute_path()
.map_or_else(|_| "<error>".into(), |f| Cow::Owned(f.to_string()))
}
impl FileLike for File {
fn read(&self, dst: &mut IoDst) -> StarryResult<usize> {
let inner = self.inner();
if likely(self.is_blocking()) {
Ok(inner.read(dst)?)
} else {
let task = current_user_task();
block_on_user(
&task,
poll_io(self, IoEvents::IN, self.nonblocking(), || {
Ok(inner.read(&mut *dst)?)
}),
)
.into_result()?
}
}
fn write(&self, src: &mut IoSrc) -> StarryResult<usize> {
let mut inner = self.inner();
if self.append() {
inner.seek(SeekFrom::End(0))?;
}
let result: StarryResult<usize> = if likely(self.is_blocking()) {
Ok(inner.write(src)?)
} else {
let task = current_user_task();
block_on_user(
&task,
poll_io(self, IoEvents::OUT, self.nonblocking(), || {
Ok(inner.write(&mut *src)?)
}),
)
.into_result()?
};
if let Ok(bytes) = result
&& bytes > 0
{
let path = path_for(inner.location()).into_owned();
crate::file::inotify::notify_modify_path(&path);
}
result
}
fn stat(&self) -> StarryResult<Kstat> {
Ok(metadata_to_kstat(&self.inner().location().metadata()?))
}
fn inode_key(&self) -> Option<(u64, u64)> {
let m = self.inner().location().metadata().ok()?;
Some((m.device, m.inode))
}
fn ioctl(
&self,
current: &crate::task::UserTaskRef,
cmd: u32,
arg: usize,
) -> StarryResult<usize> {
let loc = self.inner().backend()?.location();
if cmd == TIOCSCTTY
&& let Some(result) = crate::pseudofs::dev::tty::bind_pty_at_location(loc.clone())
{
return result;
}
match cmd {
DFS_IOCTL_ATOMIC_WRITE_SET => {
let _enabled: u32 = (arg as *const u32).vm_read(current)?;
Ok(0)
}
_ => {
if let Ok(device) = loc.entry().downcast::<Device>() {
Ok(device.ioctl_for_task(current, cmd, arg)?)
} else {
Ok(loc.ioctl(cmd, arg)?)
}
}
}
}
fn file_mmap(&self) -> StarryResult<(FileBackend, FileFlags)> {
Ok((self.inner().backend()?.clone(), self.inner().flags()))
}
fn set_nonblocking(&self, flag: bool) -> StarryResult {
self.nonblock.store(flag, Ordering::Release);
Ok(())
}
fn nonblocking(&self) -> bool {
self.nonblock.load(Ordering::Acquire)
}
fn append(&self) -> bool {
self.append.load(Ordering::Acquire)
}
fn set_append(&self, flag: bool) -> StarryResult {
self.append.store(flag, Ordering::Release);
self.inner().set_flag(FileFlags::APPEND, flag);
Ok(())
}
fn open_flags(&self) -> u32 {
self.open_flags
}
fn path(&self) -> Cow<'_, str> {
path_for(self.inner.location())
}
fn from_fd(fd: c_int) -> StarryResult<Arc<Self>>
where
Self: Sized + 'static,
{
let any = get_file_like(fd)?;
if let Ok(file) = any.clone().downcast_arc::<File>() {
return Ok(file);
}
if let Ok(memfd) = any.clone().downcast_arc::<crate::file::memfd::Memfd>() {
return Ok(memfd.inner().clone());
}
if let Ok(mount_table) = any.clone().downcast_arc::<crate::file::MountTableFile>() {
return Ok(mount_table.inner().clone());
}
Err(if any.is::<Directory>() {
StarryError::IsADirectory
} else {
StarryError::InvalidInput
})
}
}
impl Pollable for File {
fn poll(&self) -> IoEvents {
self.inner().location().poll()
}
unsafe fn register_shared(
&self,
sink: &mut dyn axpoll::SharedRegistrationSink,
events: IoEvents,
) {
unsafe { self.inner().location().register_shared(sink, events) };
}
unsafe fn register_exclusive(
&self,
sink: &mut dyn axpoll::ExclusiveRegistrationSink,
events: IoEvents,
) {
unsafe { self.inner().location().register_exclusive(sink, events) };
}
}
pub struct Directory {
inner: Location,
pub(crate) position: Mutex<DirectoryPosition>,
open_flags: u32,
detached_mount_handle: bool,
}
pub(crate) struct DirectoryPosition {
pub(crate) cursor: DirectoryCursor,
pub(crate) read_state: Option<Box<dyn DirectoryReadState>>,
}
impl Directory {
pub fn new(inner: Location, open_flags: u32) -> Self {
Self {
inner,
position: Mutex::new(DirectoryPosition {
cursor: DirectoryCursor::START,
read_state: None,
}),
open_flags,
detached_mount_handle: false,
}
}
pub(crate) fn new_detached_mount(inner: Location, open_flags: u32) -> Self {
Self {
inner,
position: Mutex::new(DirectoryPosition {
cursor: DirectoryCursor::START,
read_state: None,
}),
open_flags,
detached_mount_handle: true,
}
}
pub fn inner(&self) -> &Location {
&self.inner
}
pub(crate) fn is_detached_mount_handle(&self) -> bool {
self.detached_mount_handle
}
}
impl FileLike for Directory {
fn supports_epoll(&self) -> bool {
false
}
fn read(&self, _dst: &mut IoDst) -> StarryResult<usize> {
Err(StarryError::IsADirectory)
}
fn write(&self, _src: &mut IoSrc) -> StarryResult<usize> {
Err(StarryError::BadFileDescriptor)
}
fn stat(&self) -> StarryResult<Kstat> {
Ok(metadata_to_kstat(&self.inner.metadata()?))
}
fn inode_key(&self) -> Option<(u64, u64)> {
let m = self.inner.metadata().ok()?;
Some((m.device, m.inode))
}
fn open_flags(&self) -> u32 {
self.open_flags
}
fn path(&self) -> Cow<'_, str> {
path_for(&self.inner)
}
fn from_fd(fd: c_int) -> StarryResult<Arc<Self>> {
get_file_like(fd)?
.downcast_arc()
.map_err(|_| StarryError::NotADirectory)
}
}
impl Pollable for Directory {
fn poll(&self) -> IoEvents {
IoEvents::IN | IoEvents::OUT
}
unsafe fn register_shared(
&self,
_sink: &mut dyn axpoll::SharedRegistrationSink,
_events: IoEvents,
) {
}
}
#[cfg(all(test, not(axtest)))]
fn metadata_to_kstat_conversion_rules_hold_for_test() -> bool {
use core::time::Duration;
use axfs_ng_vfs::{DeviceId, Metadata};
let meta = Metadata {
device: 42,
inode: 100,
nlink: 3,
mode: axfs_ng_vfs::NodePermission::from_bits_truncate(0o644),
node_type: axfs_ng_vfs::NodeType::RegularFile,
uid: 1000,
gid: 1000,
size: 4096,
block_size: 512,
blocks: 8,
rdev: DeviceId::default(),
atime: Duration::from_secs(1000),
mtime: Duration::from_millis(2000500),
ctime: Duration::from_nanos(3000999999000),
};
let kstat = metadata_to_kstat(&meta);
kstat.dev == 42
&& kstat.ino == 100
&& kstat.nlink == 3
&& kstat.uid == 1000
&& kstat.gid == 1000
&& kstat.size == 4096
&& kstat.blksize == 512
&& kstat.blocks == 8
&& (kstat.mode >> 12) == (axfs_ng_vfs::NodeType::RegularFile as u32)
}
#[cfg(all(test, not(axtest)))]
mod tests {
#[test]
fn metadata_to_kstat_conversion_rules_hold() {
assert!(super::metadata_to_kstat_conversion_rules_hold_for_test());
}
}