Overall cleanup, refactor and performance tweaks

This commit is contained in:
Juno Takano 2026-04-04 15:01:15 -03:00
commit 10d47dc51c
11 changed files with 280 additions and 222 deletions

View file

@ -82,6 +82,7 @@ match_wild_err_arm = "warn"
match_wildcard_for_single_variants = "warn"
maybe_infinite_iter = "warn"
mismatching_type_param_order = "warn"
misnamed_getters = "warn"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_fields_in_debug = "warn"

View file

@ -9,66 +9,70 @@ use crate::{
run::Command,
};
pub fn load() -> Configuration {
pub fn load() -> Result<Configuration, Error> {
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<String, String> = contents
.lines()
.filter_map(|line| line.split_once('='))
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect();
let mut map: HashMap<String, String> = 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(su_command) = map.get("su_command") {
let split: Vec<String> = 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(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_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<SuCommand, Error> {
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,26 +97,24 @@ fn get_root() -> PathBuf {
}
}
fn resolve_from_path(command: &str) -> Result<PathBuf, String> {
fn resolve_command(command: &str) -> Result<PathBuf, Error> {
elog(&format!("Solving from PATH for {command}"));
let paths: Vec<PathBuf> = 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 {
elog("Skipping: Could not read directory contents");
let Ok(mut entries) = fs::read_dir(&path) else {
elog(&format!(
"Skipping: Could not read directory contents for path {:?}",
&path
));
continue;
};
@ -136,29 +138,107 @@ fn resolve_from_path(command: &str) -> Result<PathBuf, String> {
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<bool>,
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 const 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<std::env::VarError> for Error {
fn from(var_error: std::env::VarError) -> Error {
Error {
message: format!("Environment variable error: {var_error}"),
kind: ErrorKind::VarError,
}
}
}
impl From<std::io::Error> 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}")
}
}

View file

@ -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()
{

View file

@ -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:#?}"));

View file

@ -11,12 +11,12 @@ pub struct OperatingSystem {
}
impl OperatingSystem {
pub fn kind(&self) -> Kind {
self.kind.clone()
pub const fn kind(&self) -> &Kind {
&self.kind
}
pub fn packager(&self) -> Packager {
self.packager.clone()
pub const fn packager(&self) -> &Packager {
&self.packager
}
pub const fn new(kind: Kind, packager: Packager) -> OperatingSystem {

View file

@ -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,53 @@ impl Packages for Apt {
"dpkg-query",
&["--show", "--showformat", "${Package} ${Status}\\n"],
))?;
let all: Vec<String> = 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
}
})
}
});
let auto_set: HashSet<String> = self
.automatic()?
.into_iter()
.map(|package| package.name().to_owned())
.collect();
let mut manual_packages: Vec<Package> = all
.into_iter()
.filter(|name| !auto_set.contains(name))
.map(|name| Package::new_with_manual(&name, true))
.collect();
let auto_packages: Vec<Package> = self.automatic()?;
let mut manual_packages: Vec<Package> = 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));
}
}
manual_packages.sort();
Ok(manual_packages)
}
fn automatic(&self) -> Result<Vec<Package>, 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<String> = 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<Package> = vec![];
for chunk in iterator {
for chunk in chunks {
if let Some(name_line) = chunk.first()
&& let Some(auto_line) = chunk.get(2)
{
@ -140,11 +134,12 @@ impl Packages for Apt {
}
}
packages.sort();
Ok(packages)
}
fn variant(&self) -> Result<PackagerVariant, pkg::Error> {
Ok(self.variant.clone())
fn variant(&self) -> &PackagerVariant {
&self.variant
}
}
@ -159,11 +154,11 @@ impl Apt {
return Ok(());
}
let mut args = vec![subcommand];
args.extend_from_slice(&packages.iter().map(|p| p.into()).collect::<Vec<&str>>());
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)?)
}
}

View file

@ -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<Vec<Package>, Error>;
fn automatic(&self) -> Result<Vec<Package>, Error>;
fn variant(&self) -> Result<PackagerVariant, Error>;
fn variant(&self) -> &PackagerVariant;
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
@ -51,12 +51,10 @@ impl Packages for Packager {
}
}
fn variant(&self) -> Result<PackagerVariant, Error> {
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,
}
}
}
@ -68,27 +66,53 @@ pub enum PackagerVariant {
Unknown,
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
#[derive(Default, Debug, Clone, Eq)]
pub struct Package {
name: String,
version: Option<Version>,
manual: Option<bool>,
}
impl Package {
pub fn name(&self) -> String {
self.name.clone()
pub fn name(&self) -> &str {
&self.name
}
pub const fn manual(&self) -> &Option<bool> {
&self.manual
}
pub fn new_with_manual(name: &str, manual: bool) -> Package {
Package {
name: name.to_string(),
version: None,
manual: Some(manual),
}
}
}
impl PartialEq for Package {
fn eq(&self, other: &Package) -> bool {
self.name() == other.name()
}
}
impl std::hash::Hash for Package {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name().hash(state);
}
}
impl PartialOrd for Package {
fn partial_cmp(&self, other: &Package) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Package {
fn cmp(&self, other: &Package) -> std::cmp::Ordering {
self.name().cmp(other.name())
}
}
impl From<&String> for Package {
fn from(s: &String) -> Package {
Package {
@ -136,38 +160,7 @@ impl<'s> From<&'s Package> for &'s str {
impl std::fmt::Display for Package {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(version) = &self.version {
write!(f, "{} {}", &self.name, version)
} else {
write!(f, "{}", &self.name)
}
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
struct Version {
major: u32,
minor: Option<u32>,
patch: Option<u32>,
qualifier: Option<String>,
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(minor) = &self.minor
&& let Some(patch) = &self.patch
&& let Some(qualifier) = &self.qualifier
{
write!(f, "{}.{minor}.{patch}-{qualifier}", &self.major)
} else if let Some(minor) = &self.minor
&& let Some(patch) = &self.patch
{
write!(f, "{}.{minor}.{patch}", &self.major)
} else if let Some(minor) = &self.minor {
write!(f, "{}.{minor}", &self.major)
} else {
write!(f, "{}", &self.major)
}
write!(f, "{}", &self.name)
}
}
@ -185,7 +178,7 @@ impl Error {
}
}
pub fn send<T>(message: &str, kind: ErrorKind) -> Result<T, Error> {
pub fn wrapped<T>(message: &str, kind: ErrorKind) -> Result<T, Error> {
Err(Error::new(message, kind))
}
@ -215,6 +208,15 @@ impl From<run::Error> for Error {
}
}
impl From<std::io::Error> 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 +224,5 @@ pub enum ErrorKind {
MetadataFileRead,
RunError,
ExecutorError,
IO,
}

View file

@ -18,8 +18,8 @@ impl Order {
}
}
pub fn tasks(&self) -> Vec<Task> {
self.tasks.clone()
pub const fn tasks(&self) -> &Vec<Task> {
&self.tasks
}
}
@ -29,43 +29,32 @@ pub struct Task {
done: bool,
argument: String,
parameters: Vec<String>,
configuration: Configuration,
}
impl Task {
fn new(
kind: TaskKind,
argument: &str,
parameters: Vec<String>,
configuration: &Configuration,
) -> Task {
fn new(kind: TaskKind, argument: &str, parameters: Vec<String>) -> 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<String> {
self.parameters.clone()
pub const fn parameters(&self) -> &Vec<String> {
&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<Command, Error> {
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)]

View file

@ -20,14 +20,14 @@ pub fn fulfill(state: &State) -> Result<(), Error> {
state
.os()
.packager()
.install(&packages, &state.configuration())?;
.install(&packages, state.configuration())?;
}
TaskKind::PackageUninstall => {
let packages: Vec<Package> = task.parameters.iter().map(|s| s.into()).collect();
state
.os()
.packager()
.uninstall(&packages, &state.configuration())?;
.uninstall(&packages, state.configuration())?;
}
TaskKind::PackageListAuto => {
match state.os().packager().automatic() {

View file

@ -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<String>) = 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),
}
}

View file

@ -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<Order> {
self.orders.clone()
pub const fn orders(&self) -> &Vec<Order> {
&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,9 @@ fn detect_os() -> OperatingSystem {
if let Ok(os_release) = std::fs::read_to_string("/etc/os-release") {
elog(&os_release);
let mut map: HashMap<String, String> = HashMap::new();
let lines: Vec<Option<(&str, &str)>> = os_release
.lines()
.map(|line| line.split_once('='))
.collect();
for line in lines.into_iter().flatten() {
let lines = os_release.lines().map(|line| line.split_once('='));
for line in lines.flatten() {
let (key, value) = line;
map.insert(key.to_string(), strip_quotes(value));
}