vfs: implement hard links

This commit is contained in:
Mark Poliakov 2024-12-21 00:00:00 +02:00
parent ba00c97c66
commit 9b07dd7c6b
9 changed files with 194 additions and 99 deletions
kernel
userspace
init/src
sysutils/src

@ -50,7 +50,7 @@ impl DirectoryFile {
if let Some((name, node)) = children.get(index) =>
{
pos = DirectoryCachePosition::Index(index + 1);
Some((FixedString::from_str(name)?, node.clone()))
Some((FixedString::from_str(name)?, node.flatten_hardlink()?))
}
DirectoryCachePosition::Index(_) => None,
};

@ -312,26 +312,6 @@ impl IoContext {
self._find(at, path, follow_links)
}
// fn _resolve_link(&self, at: &NodeRef) -> Result<NodeRef, Error> {
// let token = self.check_access(at, AccessMode::READ)?;
// // // let _path = link.imp.read_to_string()?;
// // match at.read_symlink_node(token.clone()) {
// // Ok(node) => Ok(node),
// // // Need to read the link data and resolve it manually
// // Err(Error::NotImplemented) => {
// let path = at.read_symlink_path(token)?;
// let target = if path.is_absolute() {
// self._find(self.root.clone(), path.trim_start_separators(), false, true)?
// } else {
// self._find(at.parent(), &path, false, true)?
// };
// // TODO update node cache
// Ok(target)
// // }
// // Err(e) => Err(e),
// // }
// }
fn _resolve(&self, mut at: NodeRef, follow_links: bool) -> Result<NodeRef, Error> {
loop {
if let Some(target) = at.mountpoint_target() {
@ -415,8 +395,6 @@ impl IoContext {
) -> Result<(PathBuf, NodeRef), Error> {
let path = input.as_ref();
log::debug!("Resolve {path:?}");
let (mut buffer, mut node) = if path.is_absolute() {
(PathBuf::from_str("/"), self.root.clone())
} else {
@ -447,18 +425,16 @@ impl IoContext {
name => name,
};
log::debug!("{buffer:?}: visit {name:?}");
if !node.is_directory() {
todo!();
return Err(Error::NotADirectory);
}
let access = self.check_access(&node, AccessMode::EXEC)?;
let child = node.lookup_or_load(name, access).unwrap();
let child = node.lookup_or_load(name, access)?.flatten_hardlink()?;
let follow_links = follow_link || !tail.is_empty();
let (child, resolved) = self.visit(child, follow_links).unwrap();
let (child, resolved) = self.visit(child, follow_links)?;
if resolved.is_empty() {
buffer.push(name);
} else if resolved.is_absolute() {
@ -470,9 +446,7 @@ impl IoContext {
path = tail;
}
log::debug!("-> {buffer:?}");
Ok((buffer, node))
Ok((buffer, node.flatten_hardlink()?))
}
fn _find(&self, mut at: NodeRef, path: &Path, follow_links: bool) -> Result<NodeRef, Error> {
@ -509,11 +483,11 @@ impl IoContext {
}
let access = self.check_access(&at, AccessMode::EXEC)?;
let node = at.lookup_or_load(element, access)?;
let node = at.lookup_or_load(element, access)?.flatten_hardlink()?;
let node = self._resolve(node, follow_links)?;
if rest.is_empty() {
Ok(node)
Ok(node.flatten_hardlink()?)
} else {
self._find(node, rest, follow_links)
}
@ -530,9 +504,9 @@ mod tests {
};
use crate::vfs::{
impls::{const_value_node, fixed_path_symlink, mdir, value_node},
impls::{const_value_node, fixed_hardlink, fixed_path_symlink, mdir, value_node},
node::AccessToken,
Read,
Node, Read,
};
use super::IoContext;
@ -856,6 +830,46 @@ mod tests {
assert_eq!(err, Error::ReadOnly);
}
#[test]
fn hard_links() {
// Hard link to file
let f0 = const_value_node("test");
let h0 = fixed_hardlink(f0.clone()).unwrap();
let d0 = mdir([("f0", f0.clone()), ("h0", h0.clone())]);
let ioctx = IoContext::new(d0.clone());
let node = ioctx.find(None, "/f0", false).unwrap();
let (path, n) = ioctx.normalize("/f0", false).unwrap();
assert_eq!(path.as_str(), "/f0");
assert!(Arc::ptr_eq(&n, &f0));
assert!(Arc::ptr_eq(&node, &f0));
let node = ioctx.find(None, "/h0", false).unwrap();
let (path, n) = ioctx.normalize("/h0", false).unwrap();
assert_eq!(path.as_str(), "/h0");
assert!(Arc::ptr_eq(&n, &f0));
assert!(Arc::ptr_eq(&node, &f0));
// Hard link to directory
let d1f0 = const_value_node("test");
let d1 = mdir([("f0", d1f0.clone())]);
let h0 = fixed_hardlink(d1f0.clone()).unwrap();
let d0 = mdir([("d1", d1.clone()), ("h0", h0.clone())]);
let ioctx = IoContext::new(d0.clone());
let node = ioctx.find(None, "/d1/f0", false).unwrap();
let (path, n) = ioctx.normalize("/d1/f0", false).unwrap();
assert_eq!(path.as_str(), "/d1/f0");
assert!(Arc::ptr_eq(&n, &d1f0));
assert!(Arc::ptr_eq(&node, &d1f0));
let node = ioctx.find(None, "/h0", false).unwrap();
let (path, n) = ioctx.normalize("/h0", false).unwrap();
assert_eq!(path.as_str(), "/h0");
assert!(Arc::ptr_eq(&n, &d1f0));
assert!(Arc::ptr_eq(&node, &d1f0));
}
#[test]
fn find_tests() {
let root_d1_f1 = const_value_node("dir1/file1");

@ -10,12 +10,15 @@ use alloc::{
use libk_util::sync::IrqSafeSpinlock;
use yggdrasil_abi::{
error::Error,
io::{FileMode, OpenOptions},
io::{FileMode, FileType, OpenOptions},
};
use crate::vfs::{DirectoryOpenPosition, InstanceData};
use super::{CommonImpl, DirectoryImpl, Node, NodeFlags, NodeRef, RegularImpl, SymlinkImpl};
use super::{
traits::HardlinkImpl, CommonImpl, DirectoryImpl, Node, NodeFlags, NodeRef, RegularImpl,
SymlinkImpl,
};
trait SliceRead {
fn read_slice(&self, offset: usize, buf: &mut [u8]) -> usize;
@ -100,6 +103,15 @@ pub struct FixedPathSymlink {
target: String,
}
/// In-memory hard link with a fixed target
pub struct FixedHardlink {
target: NodeRef,
}
/// In-memory hard link with a function-based target
pub struct FnHardlink<R: Fn() -> Result<NodeRef, Error>> {
read: R,
}
impl<T, R> ReadOnlyFnValueNode<T, R>
where
T: ToString + Send + Sync + 'static,
@ -114,10 +126,7 @@ where
}
pub fn new_node(read: R) -> NodeRef {
Node::regular(
Self::new(read),
NodeFlags::IN_MEMORY_PROPS | NodeFlags::IN_MEMORY_SIZE,
)
Node::regular(Self::new(read), NodeFlags::IN_MEMORY_PROPS)
}
}
@ -216,10 +225,7 @@ where
}
pub fn new_node(read: R, write: W) -> NodeRef {
Node::regular(
Self::new(read, write),
NodeFlags::IN_MEMORY_PROPS | NodeFlags::IN_MEMORY_SIZE,
)
Node::regular(Self::new(read, write), NodeFlags::IN_MEMORY_PROPS)
}
}
@ -388,7 +394,19 @@ impl SymlinkImpl for FixedPathSymlink {
}
}
// In-memory functional symlink
// In-memory fixed hard link
impl HardlinkImpl for FixedHardlink {
fn target(&self, _node: &NodeRef) -> Result<NodeRef, Error> {
Ok(self.target.clone())
}
}
impl<R: Fn() -> Result<NodeRef, Error> + Sync + Send> HardlinkImpl for FnHardlink<R> {
fn target(&self, _node: &NodeRef) -> Result<NodeRef, Error> {
(self.read)()
}
}
/// Creates a read-only value node with given `value`
pub fn const_value_node<T>(value: T) -> NodeRef
@ -454,6 +472,17 @@ pub fn fixed_path_symlink(target: impl Into<String>) -> NodeRef {
)
}
pub fn fixed_hardlink(target: NodeRef) -> Result<NodeRef, Error> {
if target.is_hard_link() || target.ty() != FileType::File {
return Err(Error::InvalidOperation);
}
Ok(Node::hardlink(FixedHardlink { target }))
}
pub fn fn_hardlink<R: Fn() -> Result<NodeRef, Error> + Send + Sync + 'static>(read: R) -> NodeRef {
Node::hardlink(FnHardlink { read })
}
#[cfg(test)]
mod tests {
use yggdrasil_abi::io::OpenOptions;

@ -10,6 +10,7 @@ use libk_util::{
ext::OptionExt,
sync::{spin_rwlock::IrqSafeRwLock, IrqSafeSpinlock},
};
use traits::HardlinkImpl;
use yggdrasil_abi::{
bitflags,
error::Error,
@ -79,12 +80,17 @@ pub(crate) struct SymlinkData {
pub(crate) imp: Box<dyn SymlinkImpl>,
}
pub(crate) struct HardlinkData {
pub(crate) imp: Box<dyn HardlinkImpl>,
}
enum NodeImpl {
Regular(Box<dyn RegularImpl>),
Directory(DirectoryData),
Block(BlockDeviceFile),
Char(CharDeviceFile),
Symlink(SymlinkData),
Hardlink(HardlinkData),
// These map transparently to other types of nodes
// TODO: implement open operation on these
#[allow(unused)]
@ -175,6 +181,9 @@ impl Node {
Metadata::default_file(),
);
master.props.lock().size = Some(0);
slave.props.lock().size = Some(0);
(master, slave)
}
@ -261,6 +270,33 @@ impl Node {
)
}
/// Creates a new hard link node pointing to another [Node]
pub fn hardlink<T: HardlinkImpl + 'static>(data: T) -> NodeRef {
Self::new(
NodeImpl::Hardlink(HardlinkData {
imp: Box::new(data),
}),
NodeFlags::IN_MEMORY_SIZE,
Metadata::default_file(),
)
}
/// Unwraps a hard link chain to its final target
pub fn flatten_hardlink(self: &Arc<Self>) -> Result<Arc<Self>, Error> {
let mut node = self.clone();
while let Some(target) = node.read_hardlink()? {
node = target;
}
Ok(node)
}
pub fn read_hardlink(self: &Arc<Self>) -> Result<Option<NodeRef>, Error> {
match &self.data {
NodeImpl::Hardlink(data) => data.target(self).map(Some),
_ => Ok(None),
}
}
/// Returns a [Filesystem] this node belongs to.
pub fn filesystem(&self) -> Option<&dyn Filesystem> {
self.data_as_common().filesystem()
@ -274,6 +310,7 @@ impl Node {
NodeImpl::Block(w) => w.as_any(),
NodeImpl::Char(w) => w.as_any(),
NodeImpl::Symlink(w) => w.imp.as_any(),
NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => todo!(),
}
}
@ -286,6 +323,7 @@ impl Node {
NodeImpl::Block(w) => w,
NodeImpl::Char(w) => w,
NodeImpl::Symlink(w) => w.imp.as_ref(),
NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => todo!(),
}
}
@ -303,6 +341,7 @@ impl Node {
NodeImpl::Block(_) => FileType::Block,
NodeImpl::Char(_) => FileType::Char,
NodeImpl::Symlink(_) => FileType::Symlink,
NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => FileType::Char,
}
}
@ -312,6 +351,11 @@ impl Node {
matches!(&self.data, NodeImpl::Directory(_))
}
/// Returns `true` if the node represents a hard link to another node
pub fn is_hard_link(&self) -> bool {
matches!(&self.data, NodeImpl::Hardlink(_))
}
/// Returns `true` if the node represents a character device and the character device is a
/// terminal
pub fn is_terminal(&self) -> bool {
@ -390,6 +434,19 @@ impl Node {
}
}
impl HardlinkData {
fn target(&self, link: &NodeRef) -> Result<NodeRef, Error> {
let target = self.imp.target(link)?;
// Not allowed to point to other links or directories
debug_assert!(!target.is_hard_link());
debug_assert!(!matches!(
target.ty(),
FileType::Directory | FileType::Symlink
));
Ok(target)
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.data {
@ -398,6 +455,7 @@ impl fmt::Debug for Node {
NodeImpl::Char(_) => f.debug_struct("CharNode").finish_non_exhaustive(),
NodeImpl::Block(_) => f.debug_struct("BlockNode").finish_non_exhaustive(),
NodeImpl::Symlink(_) => f.debug_struct("SymlinkNode").finish_non_exhaustive(),
NodeImpl::Hardlink(_) => f.debug_struct("HardlinkNode").finish_non_exhaustive(),
NodeImpl::PseudoTerminalSlave(_) => f
.debug_struct("PseudoTerminalSlave")
.finish_non_exhaustive(),

@ -36,6 +36,7 @@ impl Node {
}
NodeImpl::Block(dev) => File::block(dev.clone(), self.clone(), opts),
NodeImpl::Char(dev) => File::char(dev.clone(), self.clone(), opts),
NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
// TODO: maybe merge open_directory and open?
NodeImpl::Directory(_) => Err(Error::IsADirectory),
NodeImpl::Symlink(_) => todo!(),
@ -213,7 +214,10 @@ impl Node {
return Ok(dir.children.lock().len() as _);
}
log::warn!("Possible bug: node has IN_MEMORY_SIZE, but is not a directory");
log::warn!(
"Possible bug: node has IN_MEMORY_SIZE, but is not a directory: {:?}",
self.ty()
);
Ok(0)
// Err(Error::NotImplemented)
} else {

@ -151,3 +151,11 @@ pub trait SymlinkImpl: CommonImpl {
Err(Error::NotImplemented)
}
}
/// Hard link interface
pub trait HardlinkImpl: Sync + Send {
fn target(&self, node: &NodeRef) -> Result<NodeRef, Error> {
let _ = node;
Err(Error::NotImplemented)
}
}

@ -3,9 +3,10 @@
use abi::error::Error;
use libk::{
device::display::console,
fs::devfs,
random,
task::{binary::LoadOptions, process::Process, runtime},
vfs::{IoContext, NodeRef},
task::{binary::LoadOptions, process::Process, runtime, thread::Thread},
vfs::{impls::fn_hardlink, IoContext, NodeRef},
};
use memfs::MemoryFilesystem;
@ -32,7 +33,17 @@ pub fn kinit() -> Result<(), Error> {
runtime::spawn(ygg_driver_usb::bus::bus_handler())?;
runtime::spawn(console::flush_consoles_task()).ok();
// TODO bring /dev/tty back
devfs::root()
.add_child(
"tty",
fn_hardlink(|| {
let thread = Thread::current();
let process = thread.process();
process.session_terminal().ok_or(Error::InvalidOperation)
}),
)
.ok();
ygg_driver_net_loopback::init();
ygg_driver_net_core::start_network_tasks()?;

@ -52,10 +52,14 @@ fn exec_script<P: AsRef<Path>>(path: P, arg: &str) -> Result<(), Error> {
}
fn get_scripts<P: AsRef<Path>>(dir: P) -> Result<Vec<String>, Error> {
let mut items = read_dir(dir)?
.map(|item| item.map(|item| item.file_name().to_str().unwrap().to_owned()))
.collect::<Result<Vec<_>, _>>()?;
items.sort();
let mut items = vec![];
for item in read_dir(dir)? {
let item = item?;
if !item.file_type()?.is_file() {
continue;
}
items.push(item.file_name().to_str().unwrap().to_owned());
}
Ok(items)
}

@ -2,11 +2,7 @@
#![feature(let_chains, decl_macro)]
use std::{
fmt,
fs::{read_dir, FileType, Metadata},
io,
path::{Path, PathBuf},
process::ExitCode,
cmp::Ordering, fmt, fs::{read_dir, FileType, Metadata}, io, path::{Path, PathBuf}, process::ExitCode
};
#[cfg(unix)]
@ -213,45 +209,6 @@ impl DisplayBit for Option<Metadata> {
Ok(())
}
}
// #[cfg(target_os = "yggdrasil")]
// impl DisplayBit for Option<Metadata> {
// fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// let Some(attrs) = self else {
// write!(f, "--------- {:<12}", "???")?;
// if opts.inodes {
// write!(f, " {:<8}", "---")?;
// }
//
// return Ok(());
// };
//
// let mode = attrs.mode_ext();
// write!(f, "{} {:>12}", mode, attrs.len().display_size_with(opts))?;
// if opts.inodes {
// if let Some(ino) = attrs.inode() {
// write!(f, " {:>8}", ino)?;
// } else {
// write!(f, " {:>8}", "---")?;
// }
// }
//
// Ok(())
// }
// }
//
// #[cfg(unix)]
// impl DisplayBit for Option<Metadata> {
// fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// use sysutils::unix::FileMode;
//
// let Some(attrs) = self else {
// return write!(f, "--------- {:<8}", "???");
// };
//
// let mode = FileMode::from(attrs.mode());
// write!(f, "{} {:>8}", mode, attrs.len().display_size_with(opts))
// }
// }
impl DisplayBit for Entry {
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -296,6 +253,16 @@ impl Entry {
}
}
fn sort_dirs_first(a: &Entry, b: &Entry) -> Ordering {
let a_is_dir = a.ty.map(|t| t.is_dir()).unwrap_or(false);
let b_is_dir = b.ty.map(|t| t.is_dir()).unwrap_or(false);
let by_type = Ord::cmp(&(b_is_dir as usize), &(a_is_dir as usize));
let by_name = Ord::cmp(&a.name, &b.name);
by_type.then(by_name)
}
fn list_directory(path: &Path) -> io::Result<Vec<Entry>> {
let mut entries = vec![];
for entry in read_dir(path)? {
@ -327,7 +294,7 @@ fn list_directory(path: &Path) -> io::Result<Vec<Entry>> {
});
}
entries.sort_by(|a, b| Ord::cmp(&a.name, &b.name));
entries.sort_by(sort_dirs_first);
Ok(entries)
}