Skip to content

Commit 8448d28

Browse files
committed
fix: improve error visibility across TUI
1 parent 97279e2 commit 8448d28

8 files changed

Lines changed: 162 additions & 68 deletions

File tree

Cargo.toml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# cargo-features = ["codegen-backend", "trim-paths"]
2+
13
[package]
24
name = "gpg-tui"
35
version = "0.11.2"
@@ -37,7 +39,9 @@ ratatui = "0.30.1"
3739
anyhow = "1.0.102"
3840
chrono = "0.4.45"
3941
unicode-width = "0.2.2"
40-
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
42+
arboard = { version = "3.6.1", default-features = false, features = [
43+
"wayland-data-control",
44+
] }
4145
colorsys = "0.7.3"
4246
shellexpand = "3.1.2"
4347
toml = "1.1.2"
@@ -71,8 +75,15 @@ pretty_assertions = "1.4.1"
7175

7276
[profile.dev]
7377
opt-level = 0
74-
debug = true
75-
panic = "abort"
78+
debug = "full"
79+
panic = "abort" # "unwind" | "abort"
80+
debug-assertions = true
81+
codegen-units = 256
82+
incremental = true
83+
overflow-checks = true
84+
strip = "none" # true | false | "none" | "symbols" | "debuginfo" ## Leave off @ w. THIS IS NOT OBFUSCATION.
85+
lto = "off" # "false" ### # true | false | "off" | "thin" | "fat" ## Can't use with cranelift yet
86+
# trim-paths = "none"
7687

7788
[profile.test]
7889
opt-level = 0

src/app/command.rs

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,41 @@ impl Display for Command {
192192
}
193193

194194
impl FromStr for Command {
195-
type Err = ();
195+
type Err = String;
196196
fn from_str(s: &str) -> Result<Self, Self::Err> {
197-
let mut values = s
197+
let values = s
198198
.replacen(':', "", 1)
199199
.to_lowercase()
200200
.split_whitespace()
201201
.map(String::from)
202202
.collect::<Vec<String>>();
203+
if values.is_empty() {
204+
return Err(String::from("command string is empty"));
205+
}
206+
debug_assert!(!values.is_empty(), "command string is empty");
207+
debug_assert_eq!(
208+
values[0],
209+
values[0].to_lowercase(),
210+
"command is not lowercase"
211+
);
203212
let command = values.first().cloned().unwrap_or_default();
204-
let args = values.drain(1..).collect::<Vec<String>>();
213+
debug_assert!(!command.is_empty(), "command is empty");
214+
215+
let args = values.iter().skip(1).cloned().collect::<Vec<String>>();
216+
if command.is_empty() {
217+
return Err(String::from("command is empty"));
218+
}
219+
220+
let arg = args.first().cloned().unwrap_or_default();
221+
222+
#[rustfmt::skip]
223+
let arg_or_fallback = |fallback: &str| -> Result<String, String> {
224+
if arg.is_empty() && !fallback.is_empty() {
225+
Ok(fallback.to_string())
226+
} else if arg.is_empty() && fallback.is_empty() {
227+
Err(String::from("argument is missing"))
228+
} else { Ok(arg.clone()) } };
229+
205230
match command.as_str() {
206231
"confirm" => Ok(Command::Confirm(Box::new(if args.is_empty() {
207232
Command::None
@@ -210,28 +235,27 @@ impl FromStr for Command {
210235
}))),
211236
"help" | "h" => Ok(Command::ShowHelp),
212237
"style" => Ok(Command::ChangeStyle(
213-
Style::from_str(
214-
&args.first().cloned().unwrap_or_default(),
215-
true,
216-
)
217-
.unwrap_or_default(),
238+
Style::from_str(&arg, true).unwrap_or_default(),
218239
)),
219240
"output" | "out" => {
220241
if !args.is_empty() {
221242
Ok(Command::ShowOutput(
222-
OutputType::from(
223-
args.first().cloned().unwrap_or_default(),
224-
),
243+
OutputType::from_str(&arg).unwrap_or_default(),
225244
args[1..].join(" "),
226245
))
227246
} else {
228-
Err(())
247+
Err(String::from("output type is missing"))
229248
}
230249
}
231250
"options" | "opt" => Ok(Command::ShowOptions),
232-
"list" | "ls" => Ok(Command::ListKeys(KeyType::from_str(
233-
&args.first().cloned().unwrap_or_else(|| String::from("pub")),
234-
)?)),
251+
"list" | "ls" => Ok(Command::ListKeys(
252+
KeyType::from_str(
253+
&arg_or_fallback("pub")?, // 'is_empty()' when unwrap_or_default() was used above the match statemnt
254+
)
255+
.map_err(|_| {
256+
String::from("invalid key type :: [list] | [ls]")
257+
})?,
258+
)),
235259
"import" | "receive" => Ok(Command::ImportKeys(
236260
s.replacen(':', "", 1)
237261
.split_whitespace()
@@ -253,11 +277,10 @@ impl FromStr for Command {
253277
patterns.truncate(patterns.len() - 1)
254278
}
255279
Ok(Command::ExportKeys(
256-
KeyType::from_str(
257-
&args
258-
.first()
259-
.cloned()
260-
.unwrap_or_else(|| String::from("pub")),
280+
KeyType::from_str(&arg_or_fallback("pub")?).map_err(
281+
|_| {
282+
String::from("invalid key type :: [export] | [exp]")
283+
},
261284
)?,
262285
patterns,
263286
export_subkeys,
@@ -266,28 +289,27 @@ impl FromStr for Command {
266289
"delete" | "del" => {
267290
let key_id = args.get(1).cloned().unwrap_or_default();
268291
Ok(Command::DeleteKey(
269-
KeyType::from_str(
270-
&args
271-
.first()
272-
.cloned()
273-
.unwrap_or_else(|| String::from("pub")),
274-
)?,
292+
KeyType::from_str(&arg_or_fallback("pub")?)?,
275293
if let Some(key) = key_id.strip_prefix("0x") {
276294
format!("0x{}", key.to_string().to_uppercase())
277295
} else {
278296
key_id
279297
},
280298
))
281299
}
282-
"send" => Ok(Command::SendKey(args.first().cloned().ok_or(())?)),
283-
"edit" => Ok(Command::EditKey(args.first().cloned().ok_or(())?)),
284-
"sign" => Ok(Command::SignKey(args.first().cloned().ok_or(())?)),
300+
#[rustfmt::skip]
301+
"send" => Ok(Command::SendKey(args.first().cloned().expect("Invalid"))),
302+
#[rustfmt::skip]
303+
"edit" => Ok(Command::EditKey(args.first().cloned().expect("Invalid"))),
304+
#[rustfmt::skip]
305+
"sign" => Ok(Command::SignKey(args.first().cloned().expect("Invalid"))),
306+
#[rustfmt::skip]
285307
"generate" | "gen" => Ok(Command::GenerateKey),
286308
"copy" | "c" => {
287309
if let Some(arg) = args.first().cloned() {
288-
Ok(Command::Copy(
289-
Selection::from_str(&arg, true).map_err(|_| ())?,
290-
))
310+
Ok(Command::Copy(Selection::from_str(&arg, true).map_err(
311+
|_| String::from("invalid key type :: [copy] | [c]"),
312+
)?))
291313
} else {
292314
Ok(Command::SwitchMode(Mode::Copy))
293315
}
@@ -320,9 +342,22 @@ impl FromStr for Command {
320342
"get" | "g" => {
321343
Ok(Command::Get(args.first().cloned().unwrap_or_default()))
322344
}
323-
"mode" | "m" => Ok(Command::SwitchMode(Mode::from_str(
324-
&args.first().cloned().ok_or(())?,
325-
)?)),
345+
"mode" | "m" => {
346+
// Ok(Command::SwitchMode(Mode::from_str(&args.first().cloned())?))
347+
let args = arg_or_fallback("").map_err(|_| {
348+
String::from("mode is missing :: [mode] | [m]")
349+
})?;
350+
351+
if let Ok(mode) = match Mode::from_str(&args) {
352+
Ok(mode) => Ok(mode),
353+
Err(_) => Err(String::from("invalid mode :: [mode] | [m]")),
354+
} {
355+
Ok(Command::SwitchMode(mode))
356+
} else {
357+
Err(String::from("invalid mode :: [mode] | [m]"))
358+
}
359+
}
360+
326361
"normal" | "n" => Ok(Command::SwitchMode(Mode::Normal)),
327362
"visual" | "v" => Ok(Command::SwitchMode(Mode::Visual)),
328363
"paste" | "p" => Ok(Command::Paste),
@@ -331,7 +366,7 @@ impl FromStr for Command {
331366
"next" => Ok(Command::NextTab),
332367
"previous" | "prev" => Ok(Command::PreviousTab),
333368
"refresh" | "r" => {
334-
if args.first() == Some(&String::from("keys")) {
369+
if args.first().cloned() == Some(String::from("keys")) {
335370
Ok(Command::RefreshKeys)
336371
} else {
337372
Ok(Command::Refresh)
@@ -340,7 +375,7 @@ impl FromStr for Command {
340375
"quit" | "q" | "q!" => Ok(Command::Quit),
341376
"logs" | "l" => Ok(Command::Logs),
342377
"none" => Ok(Command::None),
343-
_ => Err(()),
378+
_ => Err(format!("invalid command: {command}")),
344379
}
345380
}
346381
}
@@ -350,7 +385,7 @@ mod tests {
350385
use super::*;
351386
use pretty_assertions::assert_eq;
352387
#[test]
353-
fn test_app_command() -> Result<(), ()> {
388+
fn test_app_command() -> Result<(), String> {
354389
assert_eq!(
355390
Command::Confirm(Box::new(Command::None)),
356391
Command::from_str(":confirm none")?

src/app/mode.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ impl Display for Mode {
2121
}
2222

2323
impl FromStr for Mode {
24-
type Err = ();
24+
type Err = String;
2525
fn from_str(s: &str) -> Result<Self, Self::Err> {
2626
match s.to_lowercase().as_str() {
2727
"normal" | "n" => Ok(Self::Normal),
2828
"visual" | "v" => Ok(Self::Visual),
2929
"copy" | "c" => Ok(Self::Copy),
30-
_ => Err(()),
30+
_ => Err(format!("invalid mode: {s}")),
3131
}
3232
}
3333
}
@@ -37,7 +37,7 @@ mod tests {
3737
use super::*;
3838
use pretty_assertions::assert_eq;
3939
#[test]
40-
fn test_app_mode() -> Result<(), ()> {
40+
fn test_app_mode() -> Result<(), String> {
4141
let mode = Mode::from_str("normal")?;
4242
assert_eq!(Mode::Normal, mode);
4343
assert_eq!(String::from("-- normal --"), mode.to_string());

src/app/prompt.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use log::Level;
33
use crate::app::command::Command;
44
use std::cmp::Ordering;
55
use std::fmt::{Display, Formatter, Result as FmtResult};
6+
use std::str::FromStr;
67
use std::time::Instant;
78

89
/// Prefix character for indicating command input.
@@ -41,6 +42,26 @@ impl Display for OutputType {
4142
}
4243
}
4344

45+
impl FromStr for OutputType {
46+
type Err = String;
47+
48+
fn from_str(s: &str) -> Result<Self, Self::Err> {
49+
Ok(match s.to_lowercase().as_str() {
50+
"success" => Self::Success,
51+
"warning" => Self::Warning,
52+
"failure" => Self::Failure,
53+
"action" => Self::Action,
54+
_ => Self::None,
55+
})
56+
}
57+
}
58+
59+
impl From<OutputType> for String {
60+
fn from(output_type: OutputType) -> Self {
61+
output_type.to_string()
62+
}
63+
}
64+
4465
impl From<String> for OutputType {
4566
fn from(s: String) -> Self {
4667
match s.to_lowercase().as_str() {

src/config.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use anyhow::Result;
99
use clap::ValueEnum;
1010
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1111
use serde::{de, Deserialize, Deserializer, Serialize};
12-
use std::fs;
1312
use std::str::FromStr;
1413
use toml::value::Value;
1514

@@ -72,8 +71,23 @@ where
7271
D: Deserializer<'de>,
7372
{
7473
let mut key_bindings = Vec::new();
75-
let keys: Vec<Value> = Deserialize::deserialize(deserializer)?;
74+
let keys: Vec<Value> = Deserialize::deserialize(deserializer)
75+
.map_err(|_| de::Error::custom("expected an array of strings"))?;
76+
77+
if keys.is_empty() {
78+
return Err(de::Error::custom(
79+
"The 'keys' component of the keys array cannot be empty",
80+
));
81+
}
82+
7683
for key in keys {
84+
if !key.is_str() || !key.is_human_readable() {
85+
// NOTE: Worth noting it's _could_ be viable here to `continue;` ?
86+
return Err(de::Error::custom(
87+
"each key binding must be a human-readable string",
88+
));
89+
}
90+
7791
if let Some(key_str) = key.as_str() {
7892
let mut modifiers = KeyModifiers::NONE;
7993
// parse a single character
@@ -123,7 +137,12 @@ fn deserialize_command<'de, D>(deserializer: D) -> Result<Command, D::Error>
123137
where
124138
D: Deserializer<'de>,
125139
{
126-
let s = String::deserialize(deserializer)?;
140+
let s = String::deserialize(deserializer).unwrap_or_else(|_| String::new());
141+
if s.is_empty() {
142+
return Err(de::Error::custom(
143+
"The 'command' component of the keys array cannot be empty",
144+
));
145+
}
127146
Command::from_str(&s)
128147
.map_err(|_| de::Error::custom(format!("invalid command ({s})")))
129148
}
@@ -169,8 +188,12 @@ impl Config {
169188

170189
/// Parses the configuration file.
171190
pub fn parse_config(file: &str) -> Result<Config> {
172-
let contents = fs::read_to_string(file)?;
173-
let config: Config = toml::from_str(&contents)?;
191+
let contents =
192+
std::fs::read_to_string(file).expect("failed to read config file");
193+
let config: Config = toml::from_str(&contents).map_err(|err| {
194+
anyhow::anyhow!("failed to parse config file: {err}")
195+
})?;
196+
174197
Ok(config)
175198
}
176199

src/gpg/key.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ impl Display for KeyType {
2828
}
2929

3030
impl FromStr for KeyType {
31-
type Err = ();
31+
type Err = String;
3232
fn from_str(s: &str) -> Result<Self, Self::Err> {
3333
for key_type in &[Self::Public, Self::Secret] {
3434
if key_type.to_string().matches(&s).count() >= 1 {
3535
return Ok(*key_type);
3636
}
3737
}
38-
Err(())
38+
Err(format!("invalid key type: {s}"))
3939
}
4040
}
4141

0 commit comments

Comments
 (0)