diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 415bdfc..35e7360 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,5 +10,3 @@ documentation = "https://tori.jutty.dev/docs/" edition = "2024" rust-version = "1.94.0" - -[dependencies] diff --git a/rust/src/conf.rs b/rust/src/conf.rs new file mode 100644 index 0000000..4e3235c --- /dev/null +++ b/rust/src/conf.rs @@ -0,0 +1,22 @@ +use crate::log; + +pub fn load() -> Configuration { + log::elog("Loading configuration"); + + // TODO A3.1. Before parsing the user arguments, a configuration file at + // $XDG_CONFIG_DIR/tori/tori.conf MUST be read for a line such as: + // 'su_command = doas'. + // TODO A3.2. If this line is not found, the su_command MUST default to 'su -c'. + // TODO A3.3. If it is found, the su_command used MUST be whatever was specified. + // TODO A3.4. Whatever su_command MUST be validated once for presence at the path + // provided or obtained from $PATH and filesystem permission to execute + + Configuration { + su_command: String::default(), + } +} + +#[derive(Debug)] +pub struct Configuration { + su_command: String, +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..fc03ecd --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,3 @@ +pub mod conf; +pub mod log; +pub mod run; diff --git a/rust/src/log.rs b/rust/src/log.rs new file mode 100644 index 0000000..7732696 --- /dev/null +++ b/rust/src/log.rs @@ -0,0 +1,6 @@ +pub fn elog(message: &str) { + // DONE MUST be printed only if DEBUG is set in the environment + if let Ok(debug) = std::env::var("DEBUG") && !debug.is_empty() { + eprintln!(" [log] {message}"); + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs index 66449d7..e926ee0 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,3 +1,11 @@ +use tori::{conf, log, run}; + fn main() { - println!("Hello, Rust!"); + 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()); + 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 new file mode 100644 index 0000000..c77b9ed --- /dev/null +++ b/rust/src/run.rs @@ -0,0 +1,177 @@ +// B2.1. DONE version | -v | --version -> MUST print the version as in v0.8.0 +// B2.2. TODO help | -h | --help -> MUST print '' +// B2.3. TODO os -> MUST print the OS name and MUST log contents of /etc/os-release +// B2.4. TODO 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. TODO echo x y z -> MUST print x y z +// B2.7. TODO 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. TODO [any other input] -> MUST print 'Unrecognized command: [command]', +// a newline, '' and exit with status code 1 + +#[derive(Default, Debug)] +pub struct Order { + tasks: Vec, +} + +impl Order { + + pub fn fill(&mut self) { + for task in self.tasks.iter_mut() { + if task.done { continue } + task.complete(); + } + } + + fn is_complete(&self) -> bool { + self.tasks.iter().any(|e| !e.done) + } +} + +#[derive(Debug, Clone)] +pub struct Task { + kind: TaskKind, + done: bool, + parameters: Vec, +} + +impl Task { + pub fn complete(&mut self) { + use crate::{run::exec::{meta, os, shell, pkg}}; + use TaskKind::*; + + self.done = match self.kind { + Version => { meta::print_version() }, + _ => false, // TODO + } + } + + fn new(kind: TaskKind, parameters: Vec) -> Task { + Task { + kind, + done: false, + parameters, + } + } +} + +#[derive(Debug, Clone)] +pub enum TaskKind { + Version, + OperatingSystem, + Package, + User, + Echo, +} + +pub mod teller { + use crate::{log::elog, run::{Order, Task, TaskKind}}; + use std::{env, path::PathBuf}; + + pub fn parse(mut raw_args: env::Args) -> 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"); + if let Some(second) = raw_args.next() { + elog(&format!( + "Assembled command {second}, arguments {raw_args:?}" + )); + (second, raw_args.collect()) + } else { + elog("No arguments provided"); + return Order::default(); + } + } else { + elog("First argument is not the executable path"); + elog(&format!( + "Assembled command {first}, arguments {raw_args:?}" + )); + (first, raw_args.collect()) + } + } else { + elog("No arguments provided"); + return Order::default(); + }; + + use TaskKind::*; + + if argument == "version" || argument == "-v" || argument == "--version" { + elog("Command is 'version'"); + Order { tasks: vec![Task::new(Version, parameters)] } + } else if argument == "os" { + elog("Command is 'os'"); + Order { tasks: vec![Task::new(OperatingSystem, parameters)] } + } else if argument == "pkg" { + elog("Command is 'pkg'"); + Order { tasks: vec![Task::new(Package, parameters)] } + } else if argument == "user" { + elog("Command is 'user'"); + Order { tasks: vec![Task::new(User, parameters)] } + } else if argument == "echo" { + elog("Command is 'echo'"); + Order { tasks: vec![Task::new(Echo, parameters)] } + } else { + Order::default() + } + } + + fn is_executable_path(candidate: &str) -> bool { + + fn assume(message: &str) -> bool { + elog(&format!("Assuming args[0] is the executable {message}")); + true + } + + let Ok(executable_path) = env::current_exe() else { + return assume("Failed to get executable path") + }; + let Some(executable_file) = executable_path.file_name() else { + return assume("Executable path lacks a file component") + }; + + let argument_path = PathBuf::from(candidate); + let Some(argument_file) = argument_path.file_name() else { + return assume("Argument path lacks a file component") + }; + + elog(&format!( + "Executable path: {executable_path:?}, file {executable_file:?} \ + Argument path: {argument_path:?}, file {argument_file:?} " + )); + + if argument_path.exists() { + if let Ok(argument_canonical) = argument_path.canonicalize() + && let Ok(executable_canonical) = executable_path.canonicalize() { + let judgment = argument_canonical == executable_canonical; + elog(&format!("args[0] canonically is executable path: {judgment}")); + judgment + } else { + assume("Could not canonicalize executable and argument paths") + } + } else { + let judgment = argument_file == executable_file; + elog(&format!("args[0] matches executable path by name only: {judgment}")); + judgment + } + } + +} + +pub mod expeditor {} + +pub mod exec { + pub mod meta { + pub fn print_version() -> bool { + println!("v{}", env!("CARGO_PKG_VERSION")); + true + } + } + pub mod os {} + pub mod shell {} + pub mod pkg {} +}