diff --git a/src/conf.rs b/src/conf.rs index 71db0d5..24ebb23 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,7 +1,5 @@ use std::{ - collections::HashMap, - fs::{self, DirEntry}, - path::PathBuf, + collections::HashMap, fs::{self, DirEntry}, path::PathBuf }; use crate::{ @@ -9,66 +7,71 @@ use crate::{ run::Command, }; -pub fn load() -> Configuration { +pub fn load() -> Result { log::elog("Loading configuration"); - let mut conf = Configuration { - su_command: SuCommand { - command: Command::new("su", &["-c", "{% command %}"]), - wraps: true, - }, - su_command_wraps: None, - }; + let mut candidate = Configuration::default(); let root = get_root(); - let Ok(contents) = fs::read_to_string(root.join("tori.conf")) else { - eprintln!("Failed reading configuration file at {root:?}"); - return conf; - }; + let contents = fs::read_to_string(root.join("tori.conf"))?; - let lines: Vec<(&str, &str)> = contents + let map: HashMap = contents .lines() .filter_map(|line| line.split_once('=')) + .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect(); - let mut map: HashMap = HashMap::new(); - - for line in &lines { - let (key, value) = line; - map.insert(key.to_string(), value.to_string()); + if let Some(su_command) = map.get("su_command") { + let wraps = map.get("su_command_wraps").is_some_and(|v| v == "true"); + candidate.su_command = parse_su_command(su_command, wraps)?; } - elog(&format!("{lines:#?}")); + if let Some(merge_strategy) = map.get("merge_strategy") { + candidate.merge_strategy = match merge_strategy.as_str() { + "prefer configuration" => MergeStrategy::PreferConfig, + "prefer system" => MergeStrategy::PreferSystem, + _ => MergeStrategy::default(), - if let Some(su_command) = map.get("su_command") { - let split: Vec = su_command - .split(' ') - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(); - - 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 = SuCommand { - command: Command::new_from_strings(base, args), - wraps: map.get("su_command_wraps").is_some_and(|v| v == "true"), - } - } else { - eprintln!("Failed validation of 'su_command' configuration value"); } } - if let Some(su_command_wraps) = map.get("su_command_wraps") { - conf.su_command_wraps = Some(su_command_wraps == "true"); - } + Ok(candidate) +} - conf +fn parse_su_command(config_value: &str, wraps: bool) -> Result { + let split: Vec<&str> = config_value + .split(' ') + .filter(|s| !s.is_empty()) + .collect(); + + let Some((base, args)) = split.split_first() else { + return Err(Error::new("Configuration line is empty", ErrorKind::MalformedConfigLine)) + }; + + let Ok(resolved_base) = resolve_command(base) else { + return Err(Error::new( + "su_command does not resolve to a command in PATH", + ErrorKind::CommandNotInPath, + )) + }; + + let Some(resolved_base_str) = resolved_base.to_str() else { + return Err(Error::new( + "su_command path contains invalid characters (expected UTF-8)", + ErrorKind::UTF8, + )) + }; + + elog(&format!( + "Successfully resolved 'su_command' configuration value \ + {config_value} through PATH from base {base} and args {args:?} \ + to {resolved_base:?}" + )); + + Ok(SuCommand { + command: Command::new(resolved_base_str, args), + wraps, + }) } fn get_root() -> PathBuf { @@ -93,22 +96,17 @@ fn get_root() -> PathBuf { } } -fn resolve_from_path(command: &str) -> Result { +fn resolve_command(command: &str) -> Result { elog(&format!("Solving from PATH for {command}")); - let paths: Vec = if let Ok(path) = std::env::var("PATH") { - path.split(':') - .filter(|p| !p.is_empty() && PathBuf::from(p).is_dir()) - .map(PathBuf::from) - .collect() - } else { - elog("Error: PATH is not set"); - return Err(format!( - "{command} not found: PATH is not set in the environment" - )); - }; + let path_var = std::env::var("PATH")?; + + let paths = path_var.split(':') + .filter(|p| !p.is_empty() && PathBuf::from(p).is_dir()) + .map(PathBuf::from); elog(&format!("Gathered paths {paths:?}")); + for path in paths { elog(&format!("On path {path:?}")); let Ok(mut entries) = fs::read_dir(path) else { @@ -136,29 +134,107 @@ fn resolve_from_path(command: &str) -> Result { continue; }; } - Err(format!( - "{command} not found in any of the directories in PATH" - )) + Err(Error { + message: format!("{command} not found in any of the directories in PATH"), + kind: ErrorKind::CommandNotInPath, + }) } #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Configuration { pub su_command: SuCommand, - pub su_command_wraps: Option, + pub merge_strategy: MergeStrategy, } #[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum MergeStrategy { + PreferSystem, + PreferConfig, + #[default] + Interactive, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SuCommand { command: Command, wraps: bool, } impl SuCommand { - pub fn command(&self) -> Command { - self.command.clone() + pub fn command(&self) -> &Command { + &self.command } pub const fn wraps(&self) -> bool { self.wraps } } + +impl Default for SuCommand { + fn default() -> SuCommand { + SuCommand { + command: Command::new("su", &["-c", "{% command %}"]), + wraps: true, + } + } +} + +pub struct Error { + message: String, + kind: ErrorKind, +} + +impl Error { + pub fn new(message: &str, kind: ErrorKind) -> Error { + Error { + message: message.to_owned(), + kind, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}: {}", self.kind, self.message) + } +} + +impl From for Error { + fn from(var_error: std::env::VarError) -> Error { + Error { + message: format!("Environment variable error: {var_error}"), + kind: ErrorKind::VarError, + } + } +} + +impl From for Error { + fn from(io_error: std::io::Error) -> Error { + Error { + message: format!("{}: {io_error}", io_error.kind()), + kind: ErrorKind::IO, + } + } +} + +pub enum ErrorKind { + CommandNotInPath, + VarError, + MalformedConfigLine, + UTF8, + IO, +} + +impl std::fmt::Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use ErrorKind::*; + let s = match self { + VarError => "Environment variable error", + CommandNotInPath => "Command not in PATH", + MalformedConfigLine => "Malformed configuration line", + UTF8 => "Invalid characters could not be decoded (expected UTF-8)", + IO => "Input/Output error", + }; + write!(f, "{s}") + } +} diff --git a/src/log.rs b/src/log.rs index b7bc14b..a9d5720 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,4 @@ 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() { diff --git a/src/main.rs b/src/main.rs index 48e19e6..3d311e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,17 @@ use tori::{conf, log, run, state}; fn main() -> std::process::ExitCode { log::elog(&format!("tori {}", env!("CARGO_PKG_VERSION"))); - let configuration = conf::load(); + let configuration = match conf::load() { + Ok(c) => c, + Err(error) => { + eprintln!("Configuration error: {error}"); + return 1.into() + }, + }; log::elog(&format!("Configuration: {configuration:#?}")); - let order = run::teller::parse(std::env::args(), &configuration); + let order = run::teller::parse(std::env::args()); log::elog(&format!("Order: {order:#?}")); - let state = state::setup(&configuration, &[order]); + let state = state::setup(configuration, &[order]); log::elog(&format!("State: {state:#?}")); let result = run::expeditor::fulfill(&state); log::elog(&format!("Filled Order: {result:#?}")); diff --git a/src/os.rs b/src/os.rs index e0f9ee6..e477cac 100644 --- a/src/os.rs +++ b/src/os.rs @@ -11,12 +11,12 @@ pub struct OperatingSystem { } impl OperatingSystem { - pub fn kind(&self) -> Kind { - self.kind.clone() + pub fn kind(&self) -> &Kind { + &self.kind } - pub fn packager(&self) -> Packager { - self.packager.clone() + pub fn packager(&self) -> &Packager { + &self.packager } pub const fn new(kind: Kind, packager: Packager) -> OperatingSystem { diff --git a/src/os/debian.rs b/src/os/debian.rs index 7e7f417..d6dfd8d 100644 --- a/src/os/debian.rs +++ b/src/os/debian.rs @@ -1,4 +1,4 @@ -use std::fs::read_to_string; +use std::{collections::HashSet, fs::read_to_string, iter}; use crate::{ conf::Configuration, @@ -38,59 +38,52 @@ impl Packages for Apt { "dpkg-query", &["--show", "--showformat", "${Package} ${Status}\\n"], ))?; - let all: Vec = raw_all - .lines() - .filter_map(|line| { - let pair = line.split_once(' '); - match pair { - Some((pkg, "install ok installed")) => Some(pkg.to_string()), - Some(_) => None, - None => { - elog("Warning: Dropped a None pair when cleaning up package list"); - None - } + let all = raw_all.lines().filter_map(|line| { + let pair = line.split_once(' '); + match pair { + Some((pkg, "install ok installed")) => Some(pkg.to_string()), + Some(_) => None, + None => { + elog("Warning: Dropped a None pair when cleaning up package list"); + None } - }) - .collect(); - - let auto_packages: Vec = self.automatic()?; - let mut manual_packages: Vec = vec![]; - - for package in all { - let auto = Package::new_with_manual(&package, false); - if !auto_packages.contains(&auto) { - manual_packages.push(Package::new_with_manual(&package, true)); } - } + }); + + let auto_set: HashSet = self + .automatic()? + .into_iter() + .map(|package| package.name().to_owned()) + .collect(); + let manual_packages: Vec = all + .into_iter() + .filter(|name| !auto_set.contains(name)) + .map(|name| Package::new_with_manual(&name, true)) + .collect(); Ok(manual_packages) } fn automatic(&self) -> Result, pkg::Error> { let path = "/var/lib/apt/extended_states"; - let Ok(extended_states) = read_to_string(path) else { - return pkg::Error::send( - &format!("Failed reading {path}"), - pkg::ErrorKind::MetadataFileRead, - ); - }; - let lines: Vec = extended_states + let extended_states = read_to_string(path)?; + + let lines: Vec<&str> = extended_states .lines() - .map(|s| s.to_string()) .filter(|line| !line.is_empty()) .collect(); - let iterator = lines.chunks_exact(3); - let remainder = iterator.remainder(); - if !remainder.is_empty() { + let chunks = lines.chunks_exact(3); + if !chunks.remainder().is_empty() { elog(&format!( - "Warning: Reading package extended states left a remainder: {remainder:?}" + "Warning: Package extended states read left a remainder: {:?}", + chunks.remainder() )); } let mut packages: Vec = vec![]; - for chunk in iterator { + for chunk in chunks { if let Some(name_line) = chunk.first() && let Some(auto_line) = chunk.get(2) { @@ -143,8 +136,8 @@ impl Packages for Apt { Ok(packages) } - fn variant(&self) -> Result { - Ok(self.variant.clone()) + fn variant(&self) -> &PackagerVariant { + &self.variant } } @@ -159,11 +152,11 @@ impl Apt { return Ok(()); } - let mut args = vec![subcommand]; - args.extend_from_slice(&packages.iter().map(|p| p.into()).collect::>()); + let args: Vec<&str> = iter::once(subcommand) + .chain(packages.iter().map(|p| p.into())) + .collect(); let command = Command::new("apt", &args).escalate(config)?; - Ok(crate::run::executor::spawn(&command)?) } } diff --git a/src/os/pkg.rs b/src/os/pkg.rs index 4047629..c590b10 100644 --- a/src/os/pkg.rs +++ b/src/os/pkg.rs @@ -12,7 +12,7 @@ pub trait Packages: Clone + Default + Debug + PartialEq + Eq { fn uninstall(&self, packages: &[Package], config: &Configuration) -> Result<(), Error>; fn manual(&self) -> Result, Error>; fn automatic(&self) -> Result, Error>; - fn variant(&self) -> Result; + fn variant(&self) -> &PackagerVariant; } #[derive(Clone, Default, Debug, PartialEq, Eq)] @@ -51,12 +51,10 @@ impl Packages for Packager { } } - fn variant(&self) -> Result { + fn variant(&self) -> &PackagerVariant { match self { Packager::Apt(p) => p.variant(), - Packager::Unknown => Error::unknown_packager( - "Can't determine the package manager's variant because it is unknown", - ), + Packager::Unknown => &PackagerVariant::Unknown, } } } @@ -76,8 +74,8 @@ pub struct Package { } impl Package { - pub fn name(&self) -> String { - self.name.clone() + pub fn name(&self) -> &str { + &self.name } pub fn new_with_manual(name: &str, manual: bool) -> Package { @@ -185,7 +183,7 @@ impl Error { } } - pub fn send(message: &str, kind: ErrorKind) -> Result { + pub fn wrapped(message: &str, kind: ErrorKind) -> Result { Err(Error::new(message, kind)) } @@ -215,6 +213,15 @@ impl From for Error { } } +impl From for Error { + fn from(io_error: std::io::Error) -> Error { + Error { + message: format!("{:?}: {}", io_error.kind(), io_error), + kind: ErrorKind::IO, + } + } +} + #[derive(Debug, Clone)] pub enum ErrorKind { NotFound, @@ -222,4 +229,5 @@ pub enum ErrorKind { MetadataFileRead, RunError, ExecutorError, + IO, } diff --git a/src/run.rs b/src/run.rs index 8cfb045..655ea8a 100644 --- a/src/run.rs +++ b/src/run.rs @@ -18,8 +18,8 @@ impl Order { } } - pub fn tasks(&self) -> Vec { - self.tasks.clone() + pub const fn tasks(&self) -> &Vec { + &self.tasks } } @@ -29,43 +29,32 @@ pub struct Task { done: bool, argument: String, parameters: Vec, - configuration: Configuration, } impl Task { - fn new( - kind: TaskKind, - argument: &str, - parameters: Vec, - configuration: &Configuration, - ) -> Task { + fn new(kind: TaskKind, argument: &str, parameters: Vec) -> Task { Task { kind, done: false, argument: String::from(argument), parameters, - configuration: configuration.clone(), } } - pub fn configuration(&self) -> Configuration { - self.configuration.clone() + pub fn argument(&self) -> &str { + &self.argument } - pub fn argument(&self) -> String { - self.argument.clone() - } - - pub fn parameters(&self) -> Vec { - self.parameters.clone() + pub const fn parameters(&self) -> &Vec { + &self.parameters } pub const fn done(&self) -> bool { self.done } - pub fn kind(&self) -> TaskKind { - self.kind.clone() + pub const fn kind(&self) -> &TaskKind { + &self.kind } } @@ -89,7 +78,7 @@ pub struct Command { impl Command { pub fn escalate(&self, config: &Configuration) -> Result { - let mut args = config.su_command.command().args; + let mut args = config.su_command.command().clone().args; if config.su_command.wraps() { let flattened_command = format!("{} {}", self.base, self.args.join(" ")); @@ -114,7 +103,7 @@ impl Command { } Ok(Command { - base: config.su_command.command().base, + base: config.su_command.command().clone().base, args, }) } @@ -125,13 +114,6 @@ impl Command { args: args.iter().map(|e| e.to_string()).collect(), } } - - pub fn new_from_strings(base: &str, args: &[String]) -> Command { - Command { - base: base.to_string(), - args: args.to_vec(), - } - } } #[derive(Debug)] diff --git a/src/run/teller.rs b/src/run/teller.rs index afb4bb8..b2dc51d 100644 --- a/src/run/teller.rs +++ b/src/run/teller.rs @@ -1,11 +1,10 @@ use crate::{ - conf::Configuration, log::elog, run::{Order, Task, TaskKind}, }; use std::{env, path::PathBuf}; -pub fn parse(mut raw_args: env::Args, configuration: &Configuration) -> Order { +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"); @@ -32,25 +31,20 @@ pub fn parse(mut raw_args: env::Args, configuration: &Configuration) -> Order { let make_order = |kind: TaskKind| -> Order { Order { - tasks: vec![Task::new(kind, &argument, parameters, configuration)], + tasks: vec![Task::new(kind, &argument, parameters)], } }; elog(&format!("Command is {argument}")); - if argument == "version" || argument == "-v" || argument == "--version" { - make_order(TaskKind::Version) - } else if argument == "help" || argument == "-h" || argument == "--help" { - make_order(TaskKind::Help) - } else if argument == "install" { - make_order(TaskKind::PackageInstall) - } else if argument == "uninstall" { - make_order(TaskKind::PackageUninstall) - } else if argument == "auto" { - make_order(TaskKind::PackageListAuto) - } else if argument == "manual" { - make_order(TaskKind::PackageListManual) - } else { - make_order(TaskKind::Unrecognized) + + match argument.as_str() { + "version" | "-v" | "--version" => make_order(TaskKind::Version), + "help" | "-h" | "--help" => make_order(TaskKind::Help), + "install" => make_order(TaskKind::PackageInstall), + "uninstall" => make_order(TaskKind::PackageUninstall), + "auto" => make_order(TaskKind::PackageListAuto), + "manual" => make_order(TaskKind::PackageListManual), + _ => make_order(TaskKind::Unrecognized), } } diff --git a/src/state.rs b/src/state.rs index 83bc73e..391c5ff 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,29 +15,29 @@ pub struct State { } impl State { - fn new(config: &Configuration, os: &OperatingSystem, orders: &[Order]) -> State { + fn new(configuration: Configuration, os: OperatingSystem, orders: &[Order]) -> State { State { - configuration: config.clone(), - os: os.clone(), orders: orders.to_vec(), + configuration, + os, } } - pub fn configuration(&self) -> Configuration { - self.configuration.clone() + pub const fn configuration(&self) -> &Configuration { + &self.configuration } - pub fn os(&self) -> OperatingSystem { - self.os.clone() + pub const fn os(&self) -> &OperatingSystem { + &self.os } - pub fn orders(&self) -> Vec { - self.orders.clone() + pub const fn orders(&self) -> &Vec { + &self.orders } } -pub fn setup(config: &Configuration, orders: &[Order]) -> State { - State::new(config, &detect_os(), orders) +pub fn setup(config: Configuration, orders: &[Order]) -> State { + State::new(config, detect_os(), orders) } fn detect_os() -> OperatingSystem { @@ -46,11 +46,11 @@ fn detect_os() -> OperatingSystem { if let Ok(os_release) = std::fs::read_to_string("/etc/os-release") { elog(&os_release); let mut map: HashMap = HashMap::new(); - let lines: Vec> = os_release + let lines = os_release .lines() - .map(|line| line.split_once('=')) - .collect(); - for line in lines.into_iter().flatten() { + .map(|line| line.split_once('=')); + + for line in lines.flatten() { let (key, value) = line; map.insert(key.to_string(), strip_quotes(value)); }