From c35a61fb7f5a29ed8b5f5972ca913cb120fa4369 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Wed, 5 Mar 2025 13:10:51 +0200 Subject: [PATCH] vfs/user: implement chmod utility --- kernel/libk/src/vfs/node/ops.rs | 33 +++++- kernel/src/syscall/imp/sys_io.rs | 16 ++- lib/abi/src/io/file.rs | 19 ++-- userspace/Cargo.lock | 1 + userspace/Cargo.toml | 1 + userspace/lib/cross/Cargo.toml | 1 + userspace/lib/cross/src/fs.rs | 46 ++++++++ userspace/lib/cross/src/lib.rs | 3 +- userspace/lib/cross/src/sys/yggdrasil/fs.rs | 37 +++++++ userspace/lib/cross/src/sys/yggdrasil/mod.rs | 1 + userspace/sysutils/src/chmod.rs | 108 ++++++++++++++++--- userspace/tools/shell/src/syntax/lex.rs | 2 +- 12 files changed, 235 insertions(+), 33 deletions(-) create mode 100644 userspace/lib/cross/src/fs.rs create mode 100644 userspace/lib/cross/src/sys/yggdrasil/fs.rs diff --git a/kernel/libk/src/vfs/node/ops.rs b/kernel/libk/src/vfs/node/ops.rs index 8e55073a..238de115 100644 --- a/kernel/libk/src/vfs/node/ops.rs +++ b/kernel/libk/src/vfs/node/ops.rs @@ -3,7 +3,10 @@ use core::mem::MaybeUninit; use libk_util::ext::OptionExt; use yggdrasil_abi::{ error::Error, - io::{DirectoryEntry, FileMode, GroupId, OpenOptions, UserId}, + io::{ + DirectoryEntry, FileMetadataUpdate, FileMetadataUpdateMode, FileMode, GroupId, OpenOptions, + UserId, + }, }; use crate::vfs::{ @@ -263,6 +266,34 @@ impl Node { Ok(*metadata) } + pub fn update_metadata(self: &NodeRef, update: &FileMetadataUpdate) -> Result<(), Error> { + let FileMetadataUpdate::Permissions(mode) = update else { + return Err(Error::NotImplemented); + }; + + let mut cache = self.props.lock(); + let common = self.data_as_common(); + + let metadata = cache + .metadata + .get_or_try_insert_with(|| common.metadata(self))?; + + match *mode { + FileMetadataUpdateMode::Set(value) => metadata.mode |= value, + FileMetadataUpdateMode::Modify { set, clear } => { + metadata.mode &= !clear; + metadata.mode |= set; + } + } + + if !self.flags.contains(NodeFlags::IN_MEMORY_PROPS) { + // Update permissions in the real node + common.set_metadata(self, metadata)?; + } + + Ok(()) + } + // TODO clarify directory size /// Returns the size in bytes of the node pub fn size(self: &NodeRef) -> Result { diff --git a/kernel/src/syscall/imp/sys_io.rs b/kernel/src/syscall/imp/sys_io.rs index 5faea49b..67b8de32 100644 --- a/kernel/src/syscall/imp/sys_io.rs +++ b/kernel/src/syscall/imp/sys_io.rs @@ -235,14 +235,22 @@ pub(crate) fn clone_fd(source_fd: RawFd, target_fd: Option) -> Result, - _path: &str, - _update: &FileMetadataUpdate, + path: &str, + update: &FileMetadataUpdate, ) -> Result<(), Error> { let thread = Thread::current(); let process = thread.process(); - run_with_io_at(&process, at, |_at, _io| { - todo!(); + run_with_io_at(&process, at, |at, mut io| { + let node = if path.is_empty() { + at + } else { + io.ioctx_mut().find(Some(at), path, false)? + }; + + node.update_metadata(update)?; + + Ok(()) }) } diff --git a/lib/abi/src/io/file.rs b/lib/abi/src/io/file.rs index 6e081323..22ba476a 100644 --- a/lib/abi/src/io/file.rs +++ b/lib/abi/src/io/file.rs @@ -9,14 +9,15 @@ use super::FileMode; /// Describes an operation to perform when updating certain file metadata elements #[derive(Debug, Clone)] pub enum FileMetadataUpdateMode { - /// value = new - Set, - /// value = old | new - Or, - /// value = old & new - And, - /// value = old & !new - AndNot, + /// Assign a new file mode + Set(FileMode), + /// Set and/or clear existing file mode bits + Modify { + /// Bits to set + set: FileMode, + /// Bits to clear + clear: FileMode, + }, } /// Describes how file times are updated @@ -34,7 +35,7 @@ pub struct FileTimesUpdate { #[derive(Debug, Clone)] pub enum FileMetadataUpdate { /// Changes file permissions - Permissions(FileMode, FileMetadataUpdateMode), + Permissions(FileMetadataUpdateMode), /// Changes file mtime/atime/ctime Times(FileTimesUpdate), } diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index 0e6423f4..86c12034 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -536,6 +536,7 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" name = "cross" version = "0.1.0" dependencies = [ + "bitflags 2.8.0", "libc", "runtime", "tempfile", diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml index c8b354e8..c34e07ab 100644 --- a/userspace/Cargo.toml +++ b/userspace/Cargo.toml @@ -31,6 +31,7 @@ clap = { version = "4.5.20", features = ["std", "derive", "help", "usage"], defa clap-num = "1.1.1" serde_json = "1.0.132" serde = { version = "1.0.214", features = ["derive"] } +bitflags = "2.6.0" bytemuck = "1.19.0" thiserror = "1.0.64" env_logger = "0.11.5" diff --git a/userspace/lib/cross/Cargo.toml b/userspace/lib/cross/Cargo.toml index 9aa488b0..f7d37d2c 100644 --- a/userspace/lib/cross/Cargo.toml +++ b/userspace/lib/cross/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +bitflags.workspace = true [target.'cfg(target_os = "yggdrasil")'.dependencies] yggdrasil-rt.workspace = true diff --git a/userspace/lib/cross/src/fs.rs b/userspace/lib/cross/src/fs.rs new file mode 100644 index 00000000..7c1a5c57 --- /dev/null +++ b/userspace/lib/cross/src/fs.rs @@ -0,0 +1,46 @@ +use std::{fmt, io, os::fd::RawFd, path::Path}; + +use bitflags::bitflags; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileModeUpdate { + Set(FileMode), + Modify { set: FileMode, clear: FileMode }, +} + +bitflags! { + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct FileMode: u32 { + const OTHER_EXEC = 1 << 0; + const OTHER_WRITE = 1 << 1; + const OTHER_READ = 1 << 2; + const GROUP_EXEC = 1 << 3; + const GROUP_WRITE = 1 << 4; + const GROUP_READ = 1 << 5; + const USER_EXEC = 1 << 6; + const USER_WRITE = 1 << 7; + const USER_READ = 1 << 8; + } +} + +impl FileMode { + const MASK: u32 = 0o777; +} + +impl From for FileMode { + fn from(value: u32) -> Self { + Self::from_bits_retain(value & Self::MASK) + } +} + +pub use crate::sys::fs::update_file_mode; + +pub fn set_file_mode>(at: Option, path: P, mode: FileMode) -> io::Result<()> { + update_file_mode(at, path, FileModeUpdate::Set(mode)) +} + +impl fmt::Debug for FileMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#03o}", self.bits()) + } +} diff --git a/userspace/lib/cross/src/lib.rs b/userspace/lib/cross/src/lib.rs index 8186fa50..b734c14e 100644 --- a/userspace/lib/cross/src/lib.rs +++ b/userspace/lib/cross/src/lib.rs @@ -3,7 +3,8 @@ pub(crate) mod sys; +pub mod fs; pub mod io; -pub mod net; pub mod mem; +pub mod net; pub mod signal; diff --git a/userspace/lib/cross/src/sys/yggdrasil/fs.rs b/userspace/lib/cross/src/sys/yggdrasil/fs.rs new file mode 100644 index 00000000..ba2cd933 --- /dev/null +++ b/userspace/lib/cross/src/sys/yggdrasil/fs.rs @@ -0,0 +1,37 @@ +use std::{io, os::fd::RawFd, path::Path}; + +use runtime::rt as yggdrasil_rt; +use yggdrasil_rt::io::{FileMetadataUpdate, FileMetadataUpdateMode, FileMode as RawFileMode}; + +use crate::fs::FileModeUpdate; + +pub fn update_file_mode>( + at: Option, + path: P, + mode: FileModeUpdate, +) -> io::Result<()> { + // TODO make a soft error? + let path_str = path.as_ref().to_str().expect("Path is not a valid string"); + let raw_update_mode = match mode { + FileModeUpdate::Set(mode) => { + let raw_file_mode = unsafe { RawFileMode::from_raw(mode.bits()) }; + FileMetadataUpdateMode::Set(raw_file_mode) + } + FileModeUpdate::Modify { set, clear } => { + let raw_set_mode = unsafe { RawFileMode::from_raw(set.bits()) }; + let raw_clear_mode = unsafe { RawFileMode::from_raw(clear.bits()) }; + FileMetadataUpdateMode::Modify { + set: raw_set_mode, + clear: raw_clear_mode, + } + } + }; + unsafe { + yggdrasil_rt::sys::update_metadata( + at, + path_str, + &FileMetadataUpdate::Permissions(raw_update_mode), + ) + .map_err(io::Error::from) + } +} diff --git a/userspace/lib/cross/src/sys/yggdrasil/mod.rs b/userspace/lib/cross/src/sys/yggdrasil/mod.rs index ada4edc8..8fbf4081 100644 --- a/userspace/lib/cross/src/sys/yggdrasil/mod.rs +++ b/userspace/lib/cross/src/sys/yggdrasil/mod.rs @@ -1,3 +1,4 @@ +pub mod fs; pub mod mem; pub mod pid; pub mod pipe; diff --git a/userspace/sysutils/src/chmod.rs b/userspace/sysutils/src/chmod.rs index 54269b2d..9b79d271 100644 --- a/userspace/sysutils/src/chmod.rs +++ b/userspace/sysutils/src/chmod.rs @@ -1,22 +1,96 @@ -#![feature(rustc_private, yggdrasil_os)] -use std::{ - env, - os::yggdrasil::io::{self, FileMetadataUpdate, FileMetadataUpdateMode, RawFileMode}, -}; +#![feature(iter_chain)] +use std::{env, iter, process::ExitCode}; -fn main() { - let args: Vec<_> = env::args().collect(); - if args.len() != 3 { - panic!(); - } +use cross::fs::{update_file_mode, FileMode, FileModeUpdate}; - let mode_str = &args[1]; - let filename = &args[2]; +fn parse_delta_bit( + bit: &str, + set: &mut FileMode, + clear: &mut FileMode, +) -> Result<(), &'static str> { + let chars = bit.as_bytes(); + let (target, plus_or_minus, bit) = match chars.len() { + 2 => (b'a', chars[0], chars[1]), + 3 => (chars[0], chars[1], chars[2]), + _ => return Err("Expected mode to be in format X[+-]Y or [+-]Y"), + }; + let (add_in, remove_in) = match plus_or_minus { + b'+' => (set, clear), + b'-' => (clear, set), + _ => return Err("Expected +/- sign"), + }; + let mask = match (target, bit) { + (b'a', b'r') => FileMode::USER_READ | FileMode::GROUP_READ | FileMode::OTHER_READ, + (b'a', b'w') => FileMode::USER_WRITE | FileMode::GROUP_WRITE | FileMode::OTHER_WRITE, + (b'a', b'x') => FileMode::USER_EXEC | FileMode::GROUP_EXEC | FileMode::OTHER_EXEC, + (b'u', b'r') => FileMode::USER_READ, + (b'u', b'w') => FileMode::USER_WRITE, + (b'u', b'x') => FileMode::USER_EXEC, + (b'g', b'r') => FileMode::GROUP_READ, + (b'g', b'w') => FileMode::GROUP_WRITE, + (b'g', b'x') => FileMode::GROUP_EXEC, + (b'o', b'r') => FileMode::OTHER_READ, + (b'o', b'w') => FileMode::OTHER_WRITE, + (b'o', b'x') => FileMode::OTHER_EXEC, + (_, _) => return Err("Invalid mode value"), + }; - let mode_oct = u32::from_str_radix(mode_str, 8).unwrap(); + *add_in |= mask; + *remove_in &= !mask; - let update = - FileMetadataUpdate::Permissions(RawFileMode::new(mode_oct), FileMetadataUpdateMode::Set); - - io::update_metadata(filename, &update).unwrap(); + Ok(()) +} + +fn parse_mode_update(text: &str) -> Result { + if let Some(digits) = text.strip_prefix("0") { + // Decode as absolute octal + let bits = u32::from_str_radix(digits, 8).map_err(|_| "Expected octal permission mask")?; + Ok(FileModeUpdate::Set(FileMode::from(bits))) + } else { + let mut set = FileMode::empty(); + let mut clear = FileMode::empty(); + // Try to decode as a sequence of +x,-y,+z... flags + for bit in text.split(',') { + parse_delta_bit(bit.trim_matches(',').trim(), &mut set, &mut clear)?; + } + + Ok(FileModeUpdate::Modify { set, clear }) + } +} + +fn usage(invocation: &str) { + eprintln!("Usage: {invocation} OCT-MODE FILENAME [FILENAME...]"); + eprintln!(" OR {invocation} [ugoa][+-][rwx] FILENAME [FILENAME...]"); +} + +fn run>(mut args: I) -> Result { + let mode_str = args.next().ok_or("Too few arguments")?; + let first = args.next().ok_or("No filenames provided")?; + let args = iter::chain(iter::once(first), args); + let mode = parse_mode_update(&mode_str)?; + + let mut code = ExitCode::SUCCESS; + for arg in args { + match update_file_mode(None, &arg, mode) { + Ok(()) => (), + Err(error) => { + eprintln!("{arg}: {error}"); + code = ExitCode::FAILURE; + } + } + } + Ok(code) +} + +fn main() -> ExitCode { + let mut args = env::args(); + let invocation = args.next().unwrap(); + match run(args) { + Ok(code) => code, + Err(usage_error) => { + eprintln!("{invocation}: {usage_error}"); + usage(&invocation); + ExitCode::FAILURE + } + } } diff --git a/userspace/tools/shell/src/syntax/lex.rs b/userspace/tools/shell/src/syntax/lex.rs index 6c18e539..77bf537f 100644 --- a/userspace/tools/shell/src/syntax/lex.rs +++ b/userspace/tools/shell/src/syntax/lex.rs @@ -141,7 +141,7 @@ fn lex_identifier(i: &str) -> IResult<&str, &str> { recognize(many1_count(alt((alphanumeric1, is_a("-_")))))(i) } fn lex_filename(i: &str) -> IResult<&str, &str> { - recognize(many1_count(alt((alphanumeric1, is_a("./-_:+")))))(i) + recognize(many1_count(alt((alphanumeric1, is_a("./-_:+,")))))(i) } fn lex_braced_var(i: &str) -> IResult<&str, &str> {