From e0d20d7b0ea67a2fba7a767aefc339d2440aad97 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 10 Apr 2026 13:41:54 +0200 Subject: [PATCH 1/3] implement set_times function in VirtualFileSystem for updating access and modification times --- .../src/file_system/mod.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/modules/virtual_file_system/src/file_system/mod.rs b/modules/virtual_file_system/src/file_system/mod.rs index cfcca582..726d0f8d 100644 --- a/modules/virtual_file_system/src/file_system/mod.rs +++ b/modules/virtual_file_system/src/file_system/mod.rs @@ -833,4 +833,43 @@ impl VirtualFileSystem { let attributes = Attributes::new().set_permissions(permissions); Self::set_attributes(file_system.file_system, relative_path, &attributes).await } + + pub async fn set_times( + &self, + task: TaskIdentifier, + path: impl AsRef, + access: bool, + modification: bool, + ) -> Result<()> { + if !access && !modification { + return Ok(()); + } + + let path = path.as_ref(); + let file_systems = self.file_systems.read().await; + + let (file_system, relative_path, _) = + Self::get_file_system_from_path(&file_systems, &path)?; + let (time, current_user, _) = self.get_time_user_group(task).await?; + + Self::check_permissions( + file_system.file_system, + relative_path, + Permission::Write, + current_user, + ) + .await?; + + let mut attributes = Attributes::new(); + + if access { + attributes = attributes.set_access(time); + } + + if modification { + attributes = attributes.set_modification(time); + } + + Self::set_attributes(file_system.file_system, relative_path, &attributes).await + } } From a288610051d92f8b0a8e6bdfe616cca932cd2294 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Fri, 10 Apr 2026 13:42:00 +0200 Subject: [PATCH 2/3] implement touch command in command line shell --- .../shell/command_line/src/commands/mod.rs | 9 + .../shell/command_line/src/commands/touch.rs | 214 ++++++++++++++++++ executables/shell/command_line/src/error.rs | 4 + 3 files changed, 227 insertions(+) create mode 100644 executables/shell/command_line/src/commands/touch.rs diff --git a/executables/shell/command_line/src/commands/mod.rs b/executables/shell/command_line/src/commands/mod.rs index 21794262..6723d707 100644 --- a/executables/shell/command_line/src/commands/mod.rs +++ b/executables/shell/command_line/src/commands/mod.rs @@ -15,6 +15,7 @@ mod ping; mod print_working_directory; mod statistics; mod tail; +mod touch; mod web_request; mod which; mod word_count; @@ -44,6 +45,7 @@ use self::{ print_working_directory::PrintWorkingDirectoryCommand, statistics::StatisticsCommand, tail::TailCommand, + touch::TouchCommand, web_request::WebRequestCommand, which::WhichCommand, word_count::WordCountCommand, @@ -99,6 +101,7 @@ pub enum UserCommandKind { WordCount, Head, Tail, + Touch, } pub fn resolve_user_command(name: &str) -> Option { @@ -125,6 +128,7 @@ pub fn resolve_user_command(name: &str) -> Option { "wc" => Some(UserCommandKind::WordCount), "head" => Some(UserCommandKind::Head), "tail" => Some(UserCommandKind::Tail), + "touch" => Some(UserCommandKind::Touch), _ => None, } } @@ -190,6 +194,7 @@ where UserCommandKind::WordCount => WordCountCommand.execute(context, options, paths).await, UserCommandKind::Head => HeadCommand.execute(context, options, paths).await, UserCommandKind::Tail => TailCommand.execute(context, options, paths).await, + UserCommandKind::Touch => TouchCommand.execute(context, options, paths).await, } } @@ -298,6 +303,10 @@ mod tests { resolve_user_command("tail"), Some(UserCommandKind::Tail) )); + assert!(matches!( + resolve_user_command("touch"), + Some(UserCommandKind::Touch) + )); assert!(resolve_user_command("unknown").is_none()); } } diff --git a/executables/shell/command_line/src/commands/touch.rs b/executables/shell/command_line/src/commands/touch.rs new file mode 100644 index 00000000..5dc28127 --- /dev/null +++ b/executables/shell/command_line/src/commands/touch.rs @@ -0,0 +1,214 @@ +use crate::{Error, Result}; +use alloc::{borrow::ToOwned, vec::Vec}; +use getargs::{Arg, Options}; +use xila::{ + file_system::{AccessFlags, CreateFlags, Error as FileSystemError, Flags, Path}, + virtual_file_system::{self, File}, +}; + +use super::{CommandContext, UserCommand}; + +pub struct TouchCommand; + +impl UserCommand for TouchCommand { + async fn execute<'a, I, C>( + &self, + context: &mut C, + options: &mut Options<&'a str, I>, + _paths: &[&Path], + ) -> Result<()> + where + I: Iterator, + C: CommandContext, + { + execute_touch(context, options).await + } +} + +struct TouchParameters<'a> { + no_create: bool, + access: bool, + modification: bool, + paths: Vec<&'a str>, +} + +fn resolve_path( + context: &C, + path: &str, +) -> Result { + let path = Path::from_str(path); + + if path.is_absolute() { + Ok(path.to_owned()) + } else { + context + .current_directory_owned() + .join(path) + .ok_or(Error::FailedToJoinPath) + } +} + +fn parse_touch_parameters<'a, I>(options: &mut Options<&'a str, I>) -> Result> +where + I: Iterator, +{ + let mut no_create = false; + let mut access = false; + let mut modification = false; + let mut paths = Vec::new(); + + while let Some(argument) = options.next_arg()? { + match argument { + Arg::Long("no-create") | Arg::Short('c') => { + no_create = true; + } + Arg::Long("access") | Arg::Short('a') => { + access = true; + } + Arg::Long("modification") | Arg::Short('m') => { + modification = true; + } + Arg::Positional(path) => { + paths.push(path); + } + _ => { + return Err(Error::InvalidOption); + } + } + } + + if paths.is_empty() { + return Err(Error::MissingPositionalArgument("path")); + } + + Ok(TouchParameters { + no_create, + access, + modification, + paths, + }) +} + +fn resolve_update_mask(access: bool, modification: bool) -> (bool, bool) { + if !access && !modification { + (true, true) + } else { + (access, modification) + } +} + +async fn touch_one_path( + context: &C, + path: &Path, + no_create: bool, + update_access: bool, + update_modification: bool, +) -> Result<()> { + let virtual_file_system = virtual_file_system::get_instance(); + let exists = match virtual_file_system.get_statistics(&path).await { + Ok(_) => true, + Err(xila::virtual_file_system::Error::FileSystem(FileSystemError::NotFound)) => false, + Err(error) => return Err(Error::FailedToGetMetadata(error)), + }; + + if !exists { + if no_create { + return Ok(()); + } + + let _file = File::open( + virtual_file_system, + context.task_id(), + path, + Flags::new(AccessFlags::Write, Some(CreateFlags::Create), None), + ) + .await + .map_err(Error::FailedToOpenFile)?; + } + + virtual_file_system + .set_times(context.task_id(), path, update_access, update_modification) + .await + .map_err(Error::FailedToSetMetadata) +} + +async fn execute_touch<'a, I, C>(context: &mut C, options: &mut Options<&'a str, I>) -> Result<()> +where + I: Iterator, + C: CommandContext, +{ + let parameters = parse_touch_parameters(options)?; + let (update_access, update_modification) = + resolve_update_mask(parameters.access, parameters.modification); + + let mut first_error: Option = None; + + for path in parameters.paths { + let path = resolve_path(context, path)?; + let result = touch_one_path( + context, + &path, + parameters.no_create, + update_access, + update_modification, + ) + .await; + + if let Err(error) = result { + if first_error.is_none() { + first_error = Some(error); + } + } + } + + if let Some(error) = first_error { + return Err(error); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use getargs::Options; + + use super::{parse_touch_parameters, resolve_update_mask}; + + #[test] + fn resolves_default_mask_to_access_and_modification() { + assert_eq!(resolve_update_mask(false, false), (true, true)); + } + + #[test] + fn resolves_access_only_mask() { + assert_eq!(resolve_update_mask(true, false), (true, false)); + } + + #[test] + fn resolves_modification_only_mask() { + assert_eq!(resolve_update_mask(false, true), (false, true)); + } + + #[test] + fn parses_flags_and_multiple_paths() { + let input = ["-c", "-a", "first.txt", "second.txt"]; + let mut options = Options::new(input.into_iter()); + + let parsed = parse_touch_parameters(&mut options).unwrap(); + + assert!(parsed.no_create); + assert!(parsed.access); + assert!(!parsed.modification); + assert_eq!(parsed.paths, ["first.txt", "second.txt"]); + } + + #[test] + fn fails_when_no_path_is_provided() { + let input = ["-m"]; + let mut options = Options::new(input.into_iter()); + + let parsed = parse_touch_parameters(&mut options); + + assert!(parsed.is_err()); + } +} diff --git a/executables/shell/command_line/src/error.rs b/executables/shell/command_line/src/error.rs index b9633aee..51cb85bb 100644 --- a/executables/shell/command_line/src/error.rs +++ b/executables/shell/command_line/src/error.rs @@ -43,6 +43,7 @@ pub enum Error { FailedToResolve(network::Error), FailedToCreateSocket(network::Error), Format, + FailedToSetMetadata(virtual_file_system::Error), } impl From> for Error { @@ -168,6 +169,9 @@ impl Display for Error { Error::FailedToGetMetadata(error) => { write!(formatter, translate!("Failed to get metadata: {}"), error) } + Error::FailedToSetMetadata(error) => { + write!(formatter, translate!("Failed to set metadata: {}"), error) + } Error::FailedToSetCurrentDirectory(error) => { write!( formatter, From 8388b1c5ac2b9c23d1a327cbcf0a6cc4bfe36acc Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Sat, 11 Apr 2026 11:48:43 +0200 Subject: [PATCH 3/3] add: support for setting metadata in command line shell --- executables/shell/command_line/locales/en.json | 1 + executables/shell/command_line/locales/fr.json | 1 + 2 files changed, 2 insertions(+) diff --git a/executables/shell/command_line/locales/en.json b/executables/shell/command_line/locales/en.json index 6885a3bc..db8ccbd9 100644 --- a/executables/shell/command_line/locales/en.json +++ b/executables/shell/command_line/locales/en.json @@ -24,6 +24,7 @@ "Failed to resolve domain: {}": "Failed to resolve domain: {}", "Failed to set current directory: {}": "Failed to set current directory: {}", "Failed to set environment variable: {}": "Failed to set environment variable: {}", + "Failed to set metadata: {}": "Failed to set metadata: {}", "Failed to set task user: {}": "Failed to set task user: {}", "Failed to tokenize command line": "Failed to tokenize command line", "Format error": "Format error", diff --git a/executables/shell/command_line/locales/fr.json b/executables/shell/command_line/locales/fr.json index 6dc6faa5..5ce8641e 100644 --- a/executables/shell/command_line/locales/fr.json +++ b/executables/shell/command_line/locales/fr.json @@ -24,6 +24,7 @@ "Failed to resolve domain: {}": "Échec de la résolution du domaine: {}", "Failed to set current directory: {}": "Échec de la définition du répertoire courant: {}", "Failed to set environment variable: {}": "Échec de la définition de la variable d'environnement: {}", + "Failed to set metadata: {}": "Échec de la définition des métadonnées : {}", "Failed to set task user: {}": "Échec de la définition de l'utilisateur de la tâche: {}", "Failed to tokenize command line": "Échec de la tokenisation de la ligne de commande", "Format error": "Erreur de formatage",