diff --git a/src/services/libs/jinux-std/src/fs/fs_resolver.rs b/src/services/libs/jinux-std/src/fs/fs_resolver.rs new file mode 100644 index 00000000..0669344f --- /dev/null +++ b/src/services/libs/jinux-std/src/fs/fs_resolver.rs @@ -0,0 +1,380 @@ +use crate::prelude::*; +use alloc::str; +use alloc::string::String; + +use super::file::FileDescripter; +use super::inode_handle::InodeHandle; +use super::ramfs::RamFS; +use super::utils::{ + AccessMode, CreationFlags, Dentry, FileSystem, InodeMode, InodeType, StatusFlags, PATH_MAX, + SYMLINKS_MAX, +}; +use super::vfs_inode::VfsInode; + +lazy_static! { + static ref ROOT_FS: Arc = RamFS::new(); +} + +pub struct FsResolver { + root: Arc, + cwd: Arc, +} + +impl Clone for FsResolver { + fn clone(&self) -> Self { + Self { + root: self.root.clone(), + cwd: self.cwd.clone(), + } + } +} + +impl FsResolver { + pub fn new() -> Result { + let root = { + let root_inode = VfsInode::new(ROOT_FS.root_inode())?; + Dentry::new_root(root_inode) + }; + Ok(Self { + root: root.clone(), + cwd: root, + }) + } + + /// Get the root directory + pub fn root(&self) -> &Arc { + &self.root + } + + /// Get the current working directory + pub fn cwd(&self) -> &Arc { + &self.cwd + } + + /// Set the current working directory. + pub fn set_cwd(&mut self, dentry: Arc) { + self.cwd = dentry; + } + + /// Open or create a file inode handler. + pub fn open(&self, path: &FsPath, flags: u32, mode: u16) -> Result { + let creation_flags = CreationFlags::from_bits_truncate(flags); + let status_flags = StatusFlags::from_bits_truncate(flags); + let access_mode = AccessMode::from_u32(flags)?; + let inode_mode = InodeMode::from_bits_truncate(mode); + + let follow_tail_link = !creation_flags.contains(CreationFlags::O_NOFOLLOW); + let dentry = match self.lookup(path, follow_tail_link) { + Ok(dentry) => { + let inode = dentry.inode().raw_inode(); + if inode.metadata().type_ == InodeType::SymLink + && !status_flags.contains(StatusFlags::O_PATH) + { + return_errno_with_message!(Errno::ELOOP, "file is a symlink"); + } + if creation_flags.contains(CreationFlags::O_CREAT) + && creation_flags.contains(CreationFlags::O_EXCL) + { + return_errno_with_message!(Errno::EEXIST, "file exists"); + } + if creation_flags.contains(CreationFlags::O_DIRECTORY) + && inode.metadata().type_ != InodeType::Dir + { + return_errno_with_message!( + Errno::ENOTDIR, + "O_DIRECTORY is specified but file is not a directory" + ); + } + dentry + } + Err(e) + if e.error() == Errno::ENOENT + && creation_flags.contains(CreationFlags::O_CREAT) => + { + if creation_flags.contains(CreationFlags::O_DIRECTORY) { + return_errno_with_message!(Errno::ENOTDIR, "cannot create directory"); + } + let (dir_dentry, file_name) = self.lookup_dir_and_base_name(path)?; + if file_name.ends_with("/") { + return_errno_with_message!(Errno::EISDIR, "path refers to a directory"); + } + if !dir_dentry.inode().raw_inode().metadata().mode.is_writable() { + return_errno_with_message!(Errno::EPERM, "file cannot be created"); + } + let new_dentry = + dir_dentry.create_child(&file_name, InodeType::File, inode_mode)?; + new_dentry + } + Err(e) => return Err(e), + }; + + let inode_handle = InodeHandle::new(dentry, access_mode, status_flags)?; + Ok(inode_handle) + } + + /// Lookup dentry according to FsPath + pub fn lookup(&self, path: &FsPath, follow_tail_link: bool) -> Result> { + let dentry = match path.inner { + FsPathInner::Absolute(path) => { + self.lookup_from_parent(&self.root, path.trim_start_matches('/'), follow_tail_link)? + } + FsPathInner::CwdRelative(path) => { + self.lookup_from_parent(&self.cwd, path, follow_tail_link)? + } + FsPathInner::Cwd => self.cwd.clone(), + FsPathInner::FdRelative(fd, path) => { + let parent = self.lookup_dentry_from_fd(fd)?; + self.lookup_from_parent(&parent, path, follow_tail_link)? + } + FsPathInner::Fd(fd) => self.lookup_dentry_from_fd(fd)?, + }; + + Ok(dentry) + } + + /// Lookup the dir dentry and base file name of the giving path. + /// + /// If encounters symlinks, should deference them. + fn lookup_dir_and_base_name(&self, path: &FsPath) -> Result<(Arc, String)> { + let (mut dir_dentry, mut base_name) = match path.inner { + FsPathInner::Absolute(path) => { + let (dir, file_name) = split_path(path); + ( + self.lookup_from_parent(&self.root, dir.trim_start_matches('/'), true)?, + String::from(file_name), + ) + } + FsPathInner::CwdRelative(path) => { + let (dir, file_name) = split_path(path); + ( + self.lookup_from_parent(&self.cwd, path, true)?, + String::from(file_name), + ) + } + FsPathInner::FdRelative(fd, path) => { + let (dir, file_name) = split_path(path); + let parent = self.lookup_dentry_from_fd(fd)?; + ( + self.lookup_from_parent(&parent, path, true)?, + String::from(file_name), + ) + } + _ => return_errno!(Errno::ENOENT), + }; + + loop { + match dir_dentry.get(&base_name.trim_end_matches('/')) { + Ok(dentry) if dentry.inode().raw_inode().metadata().type_ == InodeType::SymLink => { + let link = { + let mut link = dentry.inode().raw_inode().read_link()?; + if link.is_empty() { + return_errno_with_message!(Errno::ENOENT, "invalid symlink"); + } + if base_name.ends_with("/") && !link.ends_with("/") { + link += "/"; + } + link + }; + let (dir, file_name) = split_path(&link); + if dir.starts_with("/") { + dir_dentry = + self.lookup_from_parent(&self.root, dir.trim_start_matches('/'), true)?; + base_name = String::from(file_name); + } else { + dir_dentry = self.lookup_from_parent(&dir_dentry, dir, true)?; + base_name = String::from(file_name); + } + } + _ => break, + } + } + + Ok((dir_dentry, base_name)) + } + + /// Lookup dentry from parent + /// + /// The length of `path` cannot exceed PATH_MAX. + /// If `path` ends with `/`, then the returned inode must be a directory inode. + /// + /// While looking up the dentry, symbolic links will be followed for + /// at most `SYMLINKS_MAX` times. + /// + /// If `follow_tail_link` is true and the trailing component is a symlink, + /// it will be followed. + /// Symlinks in earlier components of the path will always be followed. + fn lookup_from_parent( + &self, + parent: &Arc, + relative_path: &str, + follow_tail_link: bool, + ) -> Result> { + debug_assert!(!relative_path.starts_with("/")); + + if relative_path.len() > PATH_MAX { + return_errno_with_message!(Errno::ENAMETOOLONG, "path is too long"); + } + + // To handle symlinks + let mut link_path = String::new(); + let mut follows = 0; + + // Initialize the first dentry and the relative path + let (mut dentry, mut relative_path) = (parent.clone(), relative_path); + + while !relative_path.is_empty() { + let (next_name, path_remain, must_be_dir) = + if let Some((prefix, suffix)) = relative_path.split_once('/') { + let suffix = suffix.trim_start_matches('/'); + (prefix, suffix, true) + } else { + (relative_path, "", false) + }; + + // Iterate next dentry + let next_dentry = dentry.get(next_name)?; + let next_type = next_dentry.inode().raw_inode().metadata().type_; + let next_is_tail = path_remain.is_empty(); + + // If next inode is a symlink, follow symlinks at most `SYMLINKS_MAX` times. + if next_type == InodeType::SymLink && (follow_tail_link || !next_is_tail) { + if follows >= SYMLINKS_MAX { + return_errno_with_message!(Errno::ELOOP, "too many symlinks"); + } + let link_path_remain = { + let mut tmp_link_path = next_dentry.inode().raw_inode().read_link()?; + if tmp_link_path.is_empty() { + return_errno_with_message!(Errno::ENOENT, "empty symlink"); + } + if !path_remain.is_empty() { + tmp_link_path += "/"; + tmp_link_path += path_remain; + } else if must_be_dir { + tmp_link_path += "/"; + } + tmp_link_path + }; + + // Change the dentry and relative path according to symlink + if link_path_remain.starts_with("/") { + dentry = self.root.clone(); + } + link_path.clear(); + link_path.push_str(&link_path_remain.trim_start_matches('/')); + relative_path = &link_path; + follows += 1; + } else { + // If path ends with `/`, the inode must be a directory + if must_be_dir && next_type != InodeType::Dir { + return_errno_with_message!(Errno::ENOTDIR, "inode is not dir"); + } + dentry = next_dentry; + relative_path = path_remain; + } + } + + Ok(dentry) + } + + /// Lookup dentry from the giving fd + fn lookup_dentry_from_fd(&self, fd: FileDescripter) -> Result> { + let current = current!(); + let file_table = current.file_table().lock(); + let inode_handle = file_table + .get_file(fd)? + .as_inode_handle() + .ok_or(Error::with_message(Errno::EBADE, "not inode"))?; + Ok(inode_handle.dentry().clone()) + } +} + +pub const AT_FDCWD: FileDescripter = -100; + +pub struct FsPath<'a> { + inner: FsPathInner<'a>, +} + +#[derive(Debug)] +enum FsPathInner<'a> { + // absolute path + Absolute(&'a str), + // path is relative to Cwd + CwdRelative(&'a str), + // Cwd + Cwd, + // path is relative to DirFd + FdRelative(FileDescripter, &'a str), + // Fd + Fd(FileDescripter), +} + +impl<'a> FsPath<'a> { + pub fn new(dirfd: FileDescripter, path: &'a str) -> Result { + if path.len() > PATH_MAX { + return_errno_with_message!(Errno::ENAMETOOLONG, "path name too long"); + } + + let fs_path_inner = if path.starts_with("/") { + FsPathInner::Absolute(path) + } else if dirfd >= 0 { + if path.is_empty() { + FsPathInner::Fd(dirfd) + } else { + FsPathInner::FdRelative(dirfd, path) + } + } else if dirfd == AT_FDCWD { + if path.is_empty() { + FsPathInner::Cwd + } else { + FsPathInner::CwdRelative(path) + } + } else { + return_errno_with_message!(Errno::EINVAL, "invalid dirfd number"); + }; + + Ok(Self { + inner: fs_path_inner, + }) + } +} + +impl<'a> TryFrom<&'a str> for FsPath<'a> { + type Error = crate::error::Error; + + fn try_from(path: &'a str) -> Result { + if path.is_empty() { + return_errno_with_message!(Errno::ENOENT, "path is an empty string"); + } + FsPath::new(AT_FDCWD, path) + } +} + +/// Split a `path` to (`dir_path`, `file_name`). +/// +/// The `dir_path` must be a directory. +/// +/// The `file_name` is the last component. It can be suffixed by "/". +/// +/// Example: +/// +/// The path "/dir/file/" will be split to ("/dir", "file/"). +fn split_path(path: &str) -> (&str, &str) { + let file_name = path + .split_inclusive('/') + .filter(|&x| x != "/") + .last() + .unwrap_or("."); + + let mut split = path.trim_end_matches('/').rsplitn(2, '/'); + let dir_path = if split.next().unwrap().is_empty() { + "/" + } else { + let mut dir = split.next().unwrap_or(".").trim_end_matches('/'); + if dir.is_empty() { + dir = "/"; + } + dir + }; + + (dir_path, file_name) +} diff --git a/src/services/libs/jinux-std/src/fs/inode_handle/dyn_cap.rs b/src/services/libs/jinux-std/src/fs/inode_handle/dyn_cap.rs index e592a238..b3a18a2d 100644 --- a/src/services/libs/jinux-std/src/fs/inode_handle/dyn_cap.rs +++ b/src/services/libs/jinux-std/src/fs/inode_handle/dyn_cap.rs @@ -6,11 +6,11 @@ use super::*; impl InodeHandle { pub fn new( - inode: VfsInode, + dentry: Arc, access_mode: AccessMode, status_flags: StatusFlags, ) -> Result { - let inode_info = inode.raw_inode().metadata(); + let inode_info = dentry.inode().raw_inode().metadata(); if access_mode.is_readable() && !inode_info.mode.is_readable() { return_errno_with_message!(Errno::EACCES, "File is not readable"); } @@ -21,7 +21,7 @@ impl InodeHandle { return_errno_with_message!(Errno::EISDIR, "Directory cannot open to write"); } let inner = Arc::new(InodeHandle_ { - inode, + dentry, offset: Mutex::new(0), access_mode, status_flags: Mutex::new(status_flags), diff --git a/src/services/libs/jinux-std/src/fs/inode_handle/mod.rs b/src/services/libs/jinux-std/src/fs/inode_handle/mod.rs index c494ebf8..048c4d79 100644 --- a/src/services/libs/jinux-std/src/fs/inode_handle/mod.rs +++ b/src/services/libs/jinux-std/src/fs/inode_handle/mod.rs @@ -4,9 +4,8 @@ mod dyn_cap; mod static_cap; use super::utils::{ - AccessMode, DirentWriter, DirentWriterContext, InodeType, SeekFrom, StatusFlags, + AccessMode, Dentry, DirentWriter, DirentWriterContext, InodeType, SeekFrom, StatusFlags, }; -use super::vfs_inode::VfsInode; use crate::prelude::*; use crate::rights::Rights; use alloc::sync::Arc; @@ -15,7 +14,7 @@ use jinux_frame::vm::VmIo; pub struct InodeHandle(Arc, R); struct InodeHandle_ { - inode: VfsInode, + dentry: Arc, offset: Mutex, access_mode: AccessMode, status_flags: Mutex, @@ -24,15 +23,17 @@ struct InodeHandle_ { impl InodeHandle_ { pub fn read(&self, buf: &mut [u8]) -> Result { let mut offset = self.offset.lock(); - let file_size = self.inode.raw_inode().metadata().size; + let file_size = self.dentry.inode().raw_inode().metadata().size; let start = file_size.min(*offset); let end = file_size.min(*offset + buf.len()); let len = if self.status_flags.lock().contains(StatusFlags::O_DIRECT) { - self.inode + self.dentry + .inode() .raw_inode() .read_at(start, &mut buf[0..end - start])? } else { - self.inode + self.dentry + .inode() .pages() .read_bytes(start, &mut buf[0..end - start])?; end - start @@ -44,21 +45,24 @@ impl InodeHandle_ { pub fn write(&self, buf: &[u8]) -> Result { let mut offset = self.offset.lock(); - let file_size = self.inode.raw_inode().metadata().size; + let file_size = self.dentry.inode().raw_inode().metadata().size; if self.status_flags.lock().contains(StatusFlags::O_APPEND) { *offset = file_size; } let len = if self.status_flags.lock().contains(StatusFlags::O_DIRECT) { - self.inode.raw_inode().write_at(*offset, buf)? + self.dentry.inode().raw_inode().write_at(*offset, buf)? } else { - let pages = self.inode.pages(); + let pages = self.dentry.inode().pages(); let should_expand_size = *offset + buf.len() > file_size; if should_expand_size { pages.resize(*offset + buf.len())?; } pages.write_bytes(*offset, buf)?; if should_expand_size { - self.inode.raw_inode().resize(*offset + buf.len())?; + self.dentry + .inode() + .raw_inode() + .resize(*offset + buf.len())?; } buf.len() }; @@ -77,7 +81,7 @@ impl InodeHandle_ { off as i64 } SeekFrom::End(off /* as i64 */) => { - let file_size = self.inode.raw_inode().metadata().size as i64; + let file_size = self.dentry.inode().raw_inode().metadata().size as i64; assert!(file_size >= 0); file_size .checked_add(off) @@ -124,7 +128,11 @@ impl InodeHandle_ { pub fn readdir(&self, writer: &mut dyn DirentWriter) -> Result { let mut offset = self.offset.lock(); let mut dir_writer_ctx = DirentWriterContext::new(*offset, writer); - let written_size = self.inode.raw_inode().readdir(&mut dir_writer_ctx)?; + let written_size = self + .dentry + .inode() + .raw_inode() + .readdir(&mut dir_writer_ctx)?; *offset = dir_writer_ctx.pos(); Ok(written_size) } @@ -151,4 +159,8 @@ impl InodeHandle { pub fn set_status_flags(&self, new_status_flags: StatusFlags) { self.0.set_status_flags(new_status_flags) } + + pub fn dentry(&self) -> &Arc { + &self.0.dentry + } } diff --git a/src/services/libs/jinux-std/src/fs/mod.rs b/src/services/libs/jinux-std/src/fs/mod.rs index 0338fb68..b52b3a6b 100644 --- a/src/services/libs/jinux-std/src/fs/mod.rs +++ b/src/services/libs/jinux-std/src/fs/mod.rs @@ -3,6 +3,7 @@ pub mod fcntl; pub mod file; pub mod file_handle; pub mod file_table; +pub mod fs_resolver; pub mod inode_handle; pub mod ioctl; pub mod poll; diff --git a/src/services/libs/jinux-std/src/fs/utils/access_mode.rs b/src/services/libs/jinux-std/src/fs/utils/access_mode.rs index 4a1becd0..62c0e3eb 100644 --- a/src/services/libs/jinux-std/src/fs/utils/access_mode.rs +++ b/src/services/libs/jinux-std/src/fs/utils/access_mode.rs @@ -1,3 +1,4 @@ +use crate::prelude::*; use crate::rights::Rights; #[allow(non_camel_case_types)] @@ -28,6 +29,21 @@ impl AccessMode { } } +impl AccessMode { + pub fn from_u32(flags: u32) -> Result { + let bits = (flags & 0b11) as u8; + if bits > Self::O_RDWR as u8 { + return_errno_with_message!(Errno::EINVAL, "invalid bits for access mode"); + } + Ok(match bits { + 0 => Self::O_RDONLY, + 1 => Self::O_WRONLY, + 2 => Self::O_RDWR, + _ => unreachable!(), + }) + } +} + impl From for AccessMode { fn from(rights: Rights) -> AccessMode { if rights.contains(Rights::READ) && rights.contains(Rights::WRITE) { diff --git a/src/services/libs/jinux-std/src/fs/utils/creation_flags.rs b/src/services/libs/jinux-std/src/fs/utils/creation_flags.rs new file mode 100644 index 00000000..dc6bfe1d --- /dev/null +++ b/src/services/libs/jinux-std/src/fs/utils/creation_flags.rs @@ -0,0 +1,23 @@ +use bitflags::bitflags; + +bitflags! { + pub struct CreationFlags: u32 { + /// create file if it does not exist + const O_CREAT = 1 << 6; + /// error if CREATE and the file exists + const O_EXCL = 1 << 7; + /// not become the process's controlling terminal + const O_NOCTTY = 1 << 8; + /// truncate file upon open + const O_TRUNC = 1 << 9; + /// file is a directory + const O_DIRECTORY = 1 << 16; + /// pathname is not a symbolic link + const O_NOFOLLOW = 1 << 17; + /// close on exec + const O_CLOEXEC = 1 << 19; + /// create an unnamed temporary regular file + /// O_TMPFILE is (_O_TMPFILE | O_DIRECTORY) + const _O_TMPFILE = 1 << 22; + } +} diff --git a/src/services/libs/jinux-std/src/fs/utils/dentry_cache.rs b/src/services/libs/jinux-std/src/fs/utils/dentry_cache.rs new file mode 100644 index 00000000..412241a2 --- /dev/null +++ b/src/services/libs/jinux-std/src/fs/utils/dentry_cache.rs @@ -0,0 +1,127 @@ +use crate::prelude::*; +use alloc::string::String; + +use super::{InodeMode, InodeType, NAME_MAX}; +use crate::fs::vfs_inode::VfsInode; + +pub struct Dentry { + inner: RwLock, + inode: VfsInode, +} + +struct Dentry_ { + name: String, + this: Weak, + parent: Option>, + children: BTreeMap>, +} + +impl Dentry_ { + pub fn new(name: &str, parent: Option>) -> Self { + Self { + name: String::from(name), + this: Weak::default(), + parent, + children: BTreeMap::new(), + } + } +} + +impl Dentry { + /// Create a new dentry cache with root inode + pub fn new_root(inode: VfsInode) -> Arc { + let root = Self::new("/", inode, None); + root + } + + /// Internal constructor + fn new(name: &str, inode: VfsInode, parent: Option>) -> Arc { + let dentry = { + let inner = RwLock::new(Dentry_::new(name, parent)); + Arc::new(Self { inner, inode }) + }; + dentry.inner.write().this = Arc::downgrade(&dentry); + dentry + } + + fn name(&self) -> String { + self.inner.read().name.clone() + } + + pub fn inode(&self) -> &VfsInode { + &self.inode + } + + pub fn create_child(&self, name: &str, type_: InodeType, mode: InodeMode) -> Result> { + let mut inner = self.inner.write(); + let child = { + let inode = VfsInode::new(self.inode().raw_inode().mknod(name, type_, mode)?)?; + Dentry::new(name, inode, Some(inner.this.clone())) + }; + inner.children.insert(String::from(name), child.clone()); + Ok(child) + } + + fn this(&self) -> Arc { + self.inner.read().this.upgrade().unwrap() + } + + fn parent(&self) -> Option> { + self.inner + .read() + .parent + .as_ref() + .map(|p| p.upgrade().unwrap()) + } + + pub fn get(&self, name: &str) -> Result> { + if self.inode.raw_inode().metadata().type_ != InodeType::Dir { + return_errno!(Errno::ENOTDIR); + } + if name.len() > NAME_MAX { + return_errno!(Errno::ENAMETOOLONG); + } + + let dentry = match name { + "." => self.this(), + ".." => self.parent().unwrap_or(self.this()), + name => { + let mut inner = self.inner.write(); + if let Some(dentry) = inner.children.get(name) { + dentry.clone() + } else { + let inode = VfsInode::new(self.inode.raw_inode().lookup(name)?)?; + let dentry = Dentry::new(name, inode, Some(inner.this.clone())); + inner.children.insert(String::from(name), dentry.clone()); + dentry + } + } + }; + Ok(dentry) + } + + pub fn abs_path(&self) -> String { + let mut path = self.name(); + let mut dentry = self.this(); + + loop { + match dentry.parent() { + None => break, + Some(parent_dentry) => { + path = { + let parent_name = parent_dentry.name(); + if parent_name != "/" { + parent_name + "/" + &path + } else { + parent_name + &path + } + }; + dentry = parent_dentry; + } + } + } + + debug_assert!(path.starts_with("/")); + path + } +} diff --git a/src/services/libs/jinux-std/src/fs/utils/mod.rs b/src/services/libs/jinux-std/src/fs/utils/mod.rs index c36b2a52..bf6cb42a 100644 --- a/src/services/libs/jinux-std/src/fs/utils/mod.rs +++ b/src/services/libs/jinux-std/src/fs/utils/mod.rs @@ -1,6 +1,8 @@ //! VFS components pub use access_mode::AccessMode; +pub use creation_flags::CreationFlags; +pub use dentry_cache::Dentry; pub use dirent_writer::{DirentWriter, DirentWriterContext}; pub use fs::{FileSystem, SuperBlock}; pub use inode::{Inode, InodeMode, InodeType, Metadata, Timespec}; @@ -8,6 +10,8 @@ pub use page_cache::PageCacheManager; pub use status_flags::StatusFlags; mod access_mode; +mod creation_flags; +mod dentry_cache; mod dirent_writer; mod fs; mod inode; @@ -20,3 +24,12 @@ pub enum SeekFrom { End(i64), Current(i64), } + +/// Maximum bytes in a path +pub const PATH_MAX: usize = 4096; + +/// Maximum bytes in a file name +pub const NAME_MAX: usize = 255; + +/// The upper limit for resolving symbolic links +pub const SYMLINKS_MAX: usize = 40; diff --git a/src/services/libs/jinux-std/src/fs/vfs_inode.rs b/src/services/libs/jinux-std/src/fs/vfs_inode.rs index 28fe4f6e..7618d71e 100644 --- a/src/services/libs/jinux-std/src/fs/vfs_inode.rs +++ b/src/services/libs/jinux-std/src/fs/vfs_inode.rs @@ -4,6 +4,7 @@ use super::utils::{Inode, PageCacheManager}; use crate::rights::Rights; use crate::vm::vmo::{Vmo, VmoFlags, VmoOptions}; +#[derive(Clone)] pub struct VfsInode { raw_inode: Arc, pages: Vmo,