From 4509d5e6ed2754d87515e807dd971a5ff7b35d62 Mon Sep 17 00:00:00 2001 From: jutty Date: Wed, 25 Mar 2026 01:15:41 -0300 Subject: [PATCH] Rust: Finalize implementation requirements --- rust/Cargo.toml | 5 ++ rust/src/conf.rs | 21 ++++-- rust/src/main.rs | 2 +- rust/src/run.rs | 191 ++++++++++++++++++++++++++++++++--------------- 4 files changed, 150 insertions(+), 69 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 35e7360..0041d50 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,3 +10,8 @@ documentation = "https://tori.jutty.dev/docs/" edition = "2024" rust-version = "1.94.0" + +[lints.clippy] +zombie_processes = "deny" +suspicious_command_arg_space = "warn" + diff --git a/rust/src/conf.rs b/rust/src/conf.rs index fe8dff6..e699898 100644 --- a/rust/src/conf.rs +++ b/rust/src/conf.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fs::{self, DirEntry}, path::PathBuf}; -use crate::log::{self, elog}; +use crate::{run::Command, log::{self, elog}}; pub fn load() -> Configuration { log::elog("Loading configuration"); @@ -14,7 +14,10 @@ pub fn load() -> Configuration { // provided or obtained from $PATH and filesystem permission to execute let mut conf = Configuration { - su_command: vec!["su".into(), "-c".into(), "{% command %}".into()], + su_command: Command { + base: "su".into(), + args: vec!["-c".into(), "{% command %}".into()], + } }; let root = get_root(); @@ -38,13 +41,13 @@ pub fn load() -> Configuration { elog(&format!("{lines:#?}")); if let Some(su_command) = map.get("su_command") { - let split = su_command.split(' ') + let split: Vec = su_command.split(' ') .filter(|s| !s.is_empty()) .map(|s| s.to_string()).collect(); - if let Some(first) = conf.su_command.first() && let Ok(path_match) = resolve_from_path(first) { - elog(&format!("Succesfully resolved 'su_command' configuration value {su_command} to {}", path_match.to_string_lossy())); - conf.su_command = split; + if let Some((base, args)) = split.split_first() && let Ok(resolved_path) = resolve_from_path(base) { + elog(&format!("Succesfully resolved 'su_command' configuration value {su_command} through PATH to {resolved_path:?}, with base {base} and args {args:?}")); + conf.su_command = Command { base: base.clone(), args: args.to_vec() } } else { eprintln!("Failed validation of 'su_command' configuration value"); } @@ -119,7 +122,9 @@ fn resolve_from_path(command: &str) -> Result { Err("{command} not found in any of the directories in PATH".to_string()) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Configuration { - su_command: Vec, + pub su_command: Command, } + + diff --git a/rust/src/main.rs b/rust/src/main.rs index 51c26b7..7787876 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -4,7 +4,7 @@ fn main() -> std::process::ExitCode { log::elog(&format!("tori {}", env!("CARGO_PKG_VERSION"))); let configuration = conf::load(); log::elog(&format!("Configuration: {configuration:#?}")); - let mut order = run::teller::parse(std::env::args()); + let mut order = run::teller::parse(std::env::args(), &configuration); log::elog(&format!("Order: {order:#?}")); order.fill(); log::elog(&format!("Filled Order: {order:#?}")); diff --git a/rust/src/run.rs b/rust/src/run.rs index bab96b1..9b08989 100644 --- a/rust/src/run.rs +++ b/rust/src/run.rs @@ -2,24 +2,20 @@ // B2.2. DONE help | -h | --help -> MUST print '' // B2.3. DONE os -> MUST print the OS name and MUST log contents of /etc/os-release // B2.4. DONE user -> MUST print the output of the 'whoami' command -// B2.5. TODO pkg p -> MUST call the system package manager using the su_command -// to install and then uninstall package p. The user MUST be able to -// freely input to these commands' interactive inputs before control -// is returned. When done, it MUST log 'Done:', a newline, and the -// system commands executed, one per line. If no p is provided, it -// MUST NOT run any system commands and print a message // B2.6. DONE echo x y z -> MUST print x y z // B2.7. DONE echo -> MUST NOT print any output and exit with status code 0 // B2.8. DONE [no input] -> MUST NOT print any output and exit with status code 0 // B2.9. DONE [any other input] -> MUST print 'Unrecognized command: [command]', // a newline, '' and exit with status code 1 +use crate::conf::Configuration; + #[derive(Default, Debug)] -pub struct Order { - tasks: Vec, +pub struct Order<'o> { + tasks: Vec>, } -impl Order { +impl Order<'_> { pub fn fill(&mut self) { for task in self.tasks.iter_mut() { @@ -38,16 +34,17 @@ impl Order { } #[derive(Debug, Clone)] -pub struct Task { +pub struct Task<'t> { kind: TaskKind, done: bool, argument: String, parameters: Vec, + configuration: &'t Configuration, } -impl Task { +impl Task<'_> { pub fn complete(&mut self) { - use crate::{run::exec::{meta, os, shell, pkg}}; + use crate::{run::exec::{meta, os, pkg}}; use TaskKind::*; self.done = match self.kind { @@ -57,16 +54,22 @@ impl Task { User => { os::print_user() }, Echo => { meta::echo(self) }, Unrecognized => { meta::unrecognized(self) }, - _ => false, // TODO + Package => { pkg::install_uninstall(self) }, } } - fn new(kind: TaskKind, argument: String, parameters: Vec) -> Task { + fn new<'t>( + kind: TaskKind, + argument: &str, + parameters: Vec, + configuration: &'t Configuration, + ) -> Task<'t> { Task { kind, done: false, - argument, + argument: String::from(argument), parameters, + configuration, } } } @@ -82,11 +85,17 @@ pub enum TaskKind { Unrecognized, } +#[derive(Debug, Clone)] +pub struct Command { + pub base: String, + pub args: Vec, +} + pub mod teller { - use crate::{log::elog, run::{Order, Task, TaskKind}}; + use crate::{conf::Configuration, log::elog, run::{Order, Task, TaskKind}}; use std::{env, path::PathBuf}; - pub fn parse(mut raw_args: env::Args) -> Order { + pub fn parse(mut raw_args: env::Args, configuration: &Configuration) -> Order<'_> { let (argument, parameters): (String, Vec) = if let Some(first) = raw_args.next() { if is_executable_path(&first) { elog("First argument is the executable path"); @@ -111,28 +120,30 @@ pub mod teller { return Order::default(); }; - use TaskKind::*; + let make_order = |kind: TaskKind| -> Order { + Order { tasks: vec![Task::new(kind, &argument, parameters, configuration)] } + }; if argument == "version" || argument == "-v" || argument == "--version" { elog("Command is 'version'"); - Order { tasks: vec![Task::new(Version, argument, parameters)] } - } else if argument == "help" { + make_order(TaskKind::Version) + } else if argument == "help" || argument == "-h" || argument == "--help" { elog("Command is 'help'"); - Order { tasks: vec![Task::new(Help, argument, parameters)] } + make_order(TaskKind::Help) } else if argument == "os" { elog("Command is 'os'"); - Order { tasks: vec![Task::new(OsInfo, argument, parameters)] } + make_order(TaskKind::OsInfo) } else if argument == "pkg" { elog("Command is 'pkg'"); - Order { tasks: vec![Task::new(Package, argument, parameters)] } + make_order(TaskKind::Package) } else if argument == "user" { elog("Command is 'user'"); - Order { tasks: vec![Task::new(User, argument, parameters)] } + make_order(TaskKind::User) } else if argument == "echo" { elog("Command is 'echo'"); - Order { tasks: vec![Task::new(Echo, argument, parameters)] } + make_order(TaskKind::Echo) } else { - Order { tasks: vec![Task::new(Unrecognized, argument, parameters)] } + make_order(TaskKind::Unrecognized) } } @@ -209,27 +220,14 @@ pub mod exec { } pub mod os { - use crate::log::elog; + use crate::{ log::elog,run::exec::shell::run}; pub fn print_info() -> bool { - use std::process::Command; - let uname_success = if let Ok(output) = Command::new("uname") - .arg("--operating-system") - .output() { - if let Ok(utf8) = String::from_utf8(output.stdout) { - print!("{utf8}"); - true - } else { - elog("Failed UTF8 coversion of uname output"); - false - } - } else { - elog("Failed executing or reading output of uname"); - false - }; + let uname_result = run("uname", &["--operating-system"]); - let os_release_success = if let Ok(os_release) = std::fs::read_to_string("/etc/os-release") { + let os_release_result = if let Ok(os_release) = + std::fs::read_to_string("/etc/os-release") { elog(&os_release); true } else { @@ -237,27 +235,100 @@ pub mod exec { false }; - uname_success && os_release_success + uname_result.is_ok() && os_release_result } pub fn print_user() -> bool { - use std::process::Command; - - if let Ok(output) = Command::new("whoami").output() { - if let Ok(utf8) = String::from_utf8(output.stdout) { - print!("{utf8}"); - true - } else { - elog("Failed UTF8 coversion of whoami output"); - false - } - } else { - elog("Failed executing or reading output of whoami"); - false - } + run("whoami", &[]).is_ok() } } - pub mod shell {} - pub mod pkg {} + pub mod pkg { + use crate::run::Task; + + // B2.5. DONE pkg p -> MUST call the system package manager using the su_command + // to install and then uninstall package p. The user MUST be able to + // freely input to these commands' interactive inputs before control + // is returned. When done, it MUST log 'Done:', a newline, and the + // system commands executed, one per line. If no p is provided, it + // MUST NOT run any system commands and print a message + pub fn install_uninstall(task: &Task) -> bool { + let su_base: String = task.configuration.su_command.base.clone(); + let su_args: Vec = task.configuration.su_command.args.clone(); + let command_base: Vec = vec!["apt".into(), "install".into()]; + let command_args: Vec = task.parameters.clone(); + + let su_args_str: Vec<&str> = su_args.iter().map(|s| s.as_str()).collect(); + let command_base_str: Vec<&str> = command_base.iter().map(|s| s.as_str()).collect(); + let command_args_str: Vec<&str> = command_args.iter().map(|s| s.as_str()).collect(); + + if command_args.is_empty() { + println!("Parameters are empty: Nothing to install or uninstall"); + return false + } + + let args: Vec<&str> = [ + su_args_str, + command_base_str, + command_args_str, + ].iter().flatten().copied().collect(); + + crate::run::exec::shell::spawn(&su_base, &args); + + crate::run::exec::shell::spawn("sudo", vec!["apt", "remove"] + .into_iter() + .chain(task.parameters.iter().map(|s| s.as_str())) + .collect::>().as_slice()); + + println!( + "Done:\n{su_base} {} {} {}", + su_args.join(" "), + command_base.join(" "), + command_args.join(" "), + ); + true + } + } + + pub mod shell { + use std::process::Command; + + pub fn spawn(command: &str, args: &[&str]) -> bool { + if let Ok(mut child) = Command::new(command) + .args(args) + .spawn() { + let Ok(exit_status) = child.wait() else { return false }; + exit_status.success() + } else { + false + } + } + + pub fn run(command: &str, args: &[&str]) -> Result { + use std::process::Command; + + if let Ok(output) = Command::new(command) + .args(args) + .output() + + { + if let Ok(utf8) = String::from_utf8(output.stdout) { + print!("{utf8}"); + Ok(utf8) + } else { + let message = format!("Failed UTF8 coversion of {command} output"); + eprintln!("{message}"); + Err(message) + } + } else { + let message = format!("Failed executing or reading output of {command}"); + eprintln!("{message}"); + Err(message) + + } + } + + } + + }