create custom CError trait, using it for errors

The name chosen here is perhaps not ideal, and if you can think of a
better one that should probably be quickly used instead.

This is one option for C exit-code handling which is a lot cleaner than
the existing implementation in my opinion. Much of the cleanup also
comes from the enum error type in config. This enum's implementation
could be a lot cleaner with something like the 'thiserror' crate: the
message method could simply utilize that crate's derived Display
implementation.

Regarding `unwrap_or_else(|e| e.exit())`, it would have been ideal to
simply handle panics with a handler so it could simply be `unwrap()`,
but since panics tend to only pass the error string to the handler, an
exit method seems like the best solution.

Because this enforces consistency of adding the program invokation
before every error message, it has an unfortunate side-effect of the
usage text getting that information duplicated which may be seen as
undesirable. There are some workarounds to this, but I've deferred
deciding how best to do that (if it even is desirable to do so).
This commit is contained in:
[ ] 2023-03-26 11:29:37 +01:00
parent a0453f1cc3
commit 3eac2d2eab
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG Key ID: B117EC19DAA4D0DE
3 changed files with 112 additions and 91 deletions

View File

@ -20,7 +20,7 @@
use std::{
fs::File,
io::Read,
io::{Read, self},
path::PathBuf,
};
@ -31,6 +31,8 @@ use yacexits::{
EX_UNAVAILABLE,
};
use crate::error::CError;
#[derive(Deserialize)]
pub struct Config {
pub hopfiles: Vec<String>,
@ -42,56 +44,64 @@ pub struct Sources {
pub modrinth: Vec<String>,
}
pub fn get_config(dirs: BaseDirectories) -> Result<PathBuf, (String, u32)> {
match dirs.place_config_file("config.toml") {
Ok(file) => Ok(file),
Err(_) => {
Err((
format!("Unable to create configuration file."),
EX_UNAVAILABLE,
))
},
}
pub enum ConfigError {
CreateFailed(io::Error),
OpenError(io::Error),
ReadError(io::Error),
FormatError(std::string::FromUtf8Error),
ParseError(toml::de::Error),
}
impl Config {
pub fn read_config(config_path: PathBuf) -> Result<Self, (String, u32)> {
let mut buf: Vec<u8> = Vec::new();
let mut config_file = match File::open(&config_path) {
Ok(file) => file,
Err(_) => {
return Err((
format!("{}: Permission denied.", config_path.display()),
EX_UNAVAILABLE,
));
impl CError for ConfigError {
fn message(&self) -> String {
match self {
Self::CreateFailed(err) => {
format!("Unable to create configuration file: {}", err)
},
};
if let Some(err) = config_file.read_to_end(&mut buf).err() {
return Err((format!("{:?}", err), EX_DATAERR));
};
let toml = match String::from_utf8(buf) {
Ok(contents) => contents,
Err(err) => {
return Err((
format!("{:?}", err),
EX_DATAERR,
));
Self::OpenError(err) => {
format!("Unable to open configuration file: {}", err)
},
};
match toml::from_str(&toml) {
Ok(val) => Ok(val),
Err(_) => {
Err((
format!(
"{}: Invalid configuration file.", config_path.display()
),
EX_DATAERR,
))
Self::ReadError(err) => {
format!("Error while reading configuration file: {}", err)
},
Self::FormatError(err) => {
format!("Configuration file is not valid utf-8: {}", err)
},
Self::ParseError(err) => {
format!("Unable to parse configuration file: {}", err)
},
}
}
fn code(&self) -> u32 {
match self {
Self::CreateFailed(_) => EX_UNAVAILABLE,
Self::OpenError(_) => EX_UNAVAILABLE,
Self::ReadError(_) => EX_DATAERR,
Self::FormatError(_) => EX_DATAERR,
Self::ParseError(_) => EX_DATAERR,
}
}
}
pub fn get_config(dirs: BaseDirectories) -> Result<PathBuf, ConfigError> {
dirs.place_config_file("config.toml").map_err(ConfigError::CreateFailed)
}
impl Config {
pub fn read_config(config_path: PathBuf) -> Result<Self, ConfigError> {
let mut buf: Vec<u8> = Vec::new();
let mut config_file = File::open(&config_path)
.map_err(ConfigError::OpenError)?;
config_file.read_to_end(&mut buf)
.map_err(ConfigError::ReadError)?;
let toml = String::from_utf8(buf)
.map_err(ConfigError::FormatError)?;
toml::from_str(&toml).map_err(ConfigError::ParseError)
}
}

47
src/error.rs Normal file
View File

@ -0,0 +1,47 @@
use yacexits::*;
pub trait CError {
fn code(&self) -> u32;
fn message(&self) -> String;
fn exit(&self) -> ! {
eprintln!("{}: {}", program_invokation(), self.message());
exit(self.code());
}
}
fn program_invokation() -> String {
// TODO: ideally this would be argv[0] from main.
// This could be done with a const OnceCell, but I'm not sure I like that solution.
// Using std, we can do this though:
std::env::args().next()
// with a fallback to the program name
.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_owned())
}
impl<'l> CError for arg::ParseKind<'l> {
fn message(&self) -> String {
format!(
"Usage: {}{}",
program_invokation(), // argv[0],
" [-v] add | get | init | list | remove | update\n\n".to_owned() +
"add [-m version] [-f hopfiles...] packages...\n" +
"get [-n] [-d directory] [-m versions...] [-t types...] packages\n" +
"init [-f hopfiles...] version type\n" +
"list [[-f hopfiles...] | [-m versions...] [-t types...]]\n" +
"remove [[-f hopfiles...] | type version]] packages...\n" +
"update [[-f hopfiles... | [-m versions...] [-t types...]]",
)
}
fn code(&self) -> u32 { EX_USAGE }
}
impl CError for xdg::BaseDirectoriesError {
fn message(&self) -> String {
format!("Unable to open configuration file: {}", self)
}
fn code(&self) -> u32 { EX_UNAVAILABLE }
}

View File

@ -26,18 +26,18 @@ mod args;
mod client;
mod config;
mod hopfile;
mod error;
use api::*;
use args::*;
use client::*;
use config::*;
use hopfile::*;
use error::*;
use yacexits::{
exit,
EX_SOFTWARE,
EX_UNAVAILABLE,
EX_USAGE,
};
struct AppContext {
@ -50,55 +50,19 @@ struct AppContext {
async fn rust_main(arguments: c_main::Args) {
let argv: Vec<&str> = arguments.into_iter().collect();
let usage_info = format!(
"Usage: {}{}",
argv[0],
" [-v] add | get | init | list | remove | update\n\n".to_owned() +
"add [-m version] [-f hopfiles...] packages...\n" +
"get [-n] [-d directory] [-m versions...] [-t types...] packages\n" +
"init [-f hopfiles...] version type\n" +
"list [[-f hopfiles...] | [-m versions...] [-t types...]]\n" +
"remove [[-f hopfiles...] | type version]] packages...\n" +
"update [[-f hopfiles... | [-m versions...] [-t types...]]",
);
let args = Arguments::from_args(
argv
.clone()
.into_iter()
).unwrap_or_else(|_| {
eprintln!("{}", usage_info);
exit(EX_USAGE);
});
).unwrap_or_else(|e| e.exit());
let xdg_basedirs = xdg::BaseDirectories::with_prefix("hopper");
// this might be cursed; I havent decided
let config = match get_config(
xdg_basedirs.unwrap_or_else(|_| {
eprintln!(
"{}: Unable to open configuration file: Permission denied.",
argv[0],
);
exit(EX_UNAVAILABLE);
})
) {
Ok(path) => {
match Config::read_config(path) {
Ok(file) => file,
Err((err, code)) => {
eprintln!("{}: {}", argv[0], err);
exit(code);
},
}
},
Err((err, code)) => {
eprintln!("{}: {}", argv[0], err);
exit(code);
},
};
let xdg_basedirs = xdg::BaseDirectories::with_prefix("hopper")
.unwrap_or_else(|e| e.exit());
let config = get_config(xdg_basedirs)
.and_then(Config::read_config)
.unwrap_or_else(|e| e.exit());
let ctx = AppContext { args, config };
match ctx.args.sub {