Complete TUI coverage and security audit
This commit is contained in:
@@ -4,6 +4,8 @@ use std::{error::Error, fmt};
|
||||
|
||||
use ironstorage::command::{CliAction, CommandRequest, GitRequest, OtpRequest, parse_from};
|
||||
|
||||
use crate::action::{ACTIONS, Action};
|
||||
|
||||
const HISTORY_LIMIT: usize = 100;
|
||||
|
||||
const ROOT_COMMANDS: &[&str] = &[
|
||||
@@ -81,6 +83,256 @@ pub const COMMAND_COVERAGE: &[CommandCoverage] = &[
|
||||
coverage("unlock the TUI", ":unlock"),
|
||||
];
|
||||
|
||||
/// Executable feature matrix for the complete upstream pass and pass-otp
|
||||
/// surface. `None` means the command prompt is the intentional keyboard UI;
|
||||
/// otherwise the action registry supplies the default hotkey and help entry.
|
||||
pub const TUI_COVERAGE: &[TuiCoverage] = &[
|
||||
tui(
|
||||
"initialize recipients",
|
||||
"recipient form",
|
||||
Some(Action::Initialize),
|
||||
":init GPG-ID…",
|
||||
),
|
||||
tui(
|
||||
"list a directory",
|
||||
"password tree",
|
||||
Some(Action::Activate),
|
||||
":list [PATH]",
|
||||
),
|
||||
tui(
|
||||
"show an entry",
|
||||
"structured viewer",
|
||||
Some(Action::Activate),
|
||||
":show [ENTRY]",
|
||||
),
|
||||
tui(
|
||||
"copy a selected entry line",
|
||||
"clipboard feedback",
|
||||
Some(Action::Copy),
|
||||
":show --clip [LINE] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"show a selected entry line as QR",
|
||||
"resizable QR popup",
|
||||
None,
|
||||
":show --qrcode [LINE] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"find names",
|
||||
"incremental tree filter",
|
||||
Some(Action::Filter),
|
||||
":find TERM…",
|
||||
),
|
||||
tui(
|
||||
"grep decrypted entries",
|
||||
"decrypted result pane",
|
||||
Some(Action::Grep),
|
||||
":grep [OPTIONS] PATTERN",
|
||||
),
|
||||
tui(
|
||||
"insert an entry",
|
||||
"masked insert form",
|
||||
Some(Action::InsertEntry),
|
||||
":insert [OPTIONS] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"edit an entry",
|
||||
"structured editor",
|
||||
Some(Action::EditEntry),
|
||||
":edit ENTRY",
|
||||
),
|
||||
tui(
|
||||
"generate a password",
|
||||
"generation form",
|
||||
Some(Action::GenerateEntry),
|
||||
":generate [OPTIONS] ENTRY [LENGTH]",
|
||||
),
|
||||
tui(
|
||||
"present a generated password",
|
||||
"generation presentation selector",
|
||||
Some(Action::GenerateEntry),
|
||||
":generate (--clip | --qrcode) ENTRY [LENGTH]",
|
||||
),
|
||||
tui(
|
||||
"remove entries or directories",
|
||||
"confirmed removal form",
|
||||
Some(Action::RemoveEntry),
|
||||
":remove [OPTIONS] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"move entries or directories",
|
||||
"move form",
|
||||
Some(Action::MoveEntry),
|
||||
":move [OPTIONS] SOURCE DESTINATION",
|
||||
),
|
||||
tui(
|
||||
"copy entries or directories",
|
||||
"copy form",
|
||||
Some(Action::CopyEntry),
|
||||
":copy [OPTIONS] SOURCE DESTINATION",
|
||||
),
|
||||
tui("initialize Git", "Git dashboard", None, ":git init"),
|
||||
tui("show Git status", "Git dashboard", None, ":git status"),
|
||||
tui("show Git log", "Git dashboard", None, ":git log [OPTIONS]"),
|
||||
tui(
|
||||
"show Git diff",
|
||||
"zeroizing Git detail pane",
|
||||
None,
|
||||
":git diff [PATH]…",
|
||||
),
|
||||
tui("stage Git paths", "Git dashboard", None, ":git add PATH…"),
|
||||
tui(
|
||||
"create a Git commit",
|
||||
"Git dashboard",
|
||||
None,
|
||||
":git commit -m MESSAGE",
|
||||
),
|
||||
tui(
|
||||
"manage Git remotes",
|
||||
"Git dashboard",
|
||||
None,
|
||||
":git remote [COMMAND]",
|
||||
),
|
||||
tui(
|
||||
"manage Git configuration",
|
||||
"Git dashboard",
|
||||
None,
|
||||
":git config (--get KEY | KEY VALUE)",
|
||||
),
|
||||
tui(
|
||||
"fetch Git remote",
|
||||
"Git progress view",
|
||||
None,
|
||||
":git fetch [REMOTE]",
|
||||
),
|
||||
tui(
|
||||
"pull Git remote",
|
||||
"Git progress view",
|
||||
Some(Action::GitPull),
|
||||
":git pull [REMOTE] [BRANCH]",
|
||||
),
|
||||
tui(
|
||||
"push Git remote",
|
||||
"Git progress view",
|
||||
Some(Action::GitPush),
|
||||
":git push [REMOTE] [BRANCH]",
|
||||
),
|
||||
tui(
|
||||
"synchronize Git remote",
|
||||
"Git progress view",
|
||||
None,
|
||||
":git sync [REMOTE]",
|
||||
),
|
||||
tui(
|
||||
"resolve Git conflicts locally",
|
||||
"Git conflict view",
|
||||
None,
|
||||
":git resolve-local",
|
||||
),
|
||||
tui(
|
||||
"resolve Git conflicts remotely",
|
||||
"Git conflict view",
|
||||
None,
|
||||
":git resolve-remote",
|
||||
),
|
||||
tui(
|
||||
"generate an OTP code",
|
||||
"focused OTP field",
|
||||
Some(Action::OtpCode),
|
||||
":otp code [OPTIONS] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"copy an OTP code",
|
||||
"clipboard feedback",
|
||||
Some(Action::OtpCopyCode),
|
||||
":otp code --clip ENTRY",
|
||||
),
|
||||
tui(
|
||||
"insert an OTP entry",
|
||||
"masked OTP insert form",
|
||||
Some(Action::OtpInsert),
|
||||
":otp insert [OPTIONS] [ENTRY]",
|
||||
),
|
||||
tui(
|
||||
"append OTP data",
|
||||
"masked OTP append form",
|
||||
Some(Action::OtpAppend),
|
||||
":otp append [OPTIONS] ENTRY",
|
||||
),
|
||||
tui(
|
||||
"present an OTP URI",
|
||||
"secret URI popup",
|
||||
Some(Action::OtpUri),
|
||||
":otp uri ENTRY",
|
||||
),
|
||||
tui(
|
||||
"copy an OTP URI",
|
||||
"clipboard feedback",
|
||||
Some(Action::OtpCopyUri),
|
||||
":otp uri --clip ENTRY",
|
||||
),
|
||||
tui(
|
||||
"present an OTP URI as QR",
|
||||
"resizable QR popup",
|
||||
Some(Action::OtpQr),
|
||||
":otp uri --qrcode ENTRY",
|
||||
),
|
||||
tui(
|
||||
"validate an OTP URI",
|
||||
"masked validation form",
|
||||
Some(Action::OtpValidate),
|
||||
":otp validate URI",
|
||||
),
|
||||
tui("show pass-otp version", "status line", None, ":otp version"),
|
||||
tui(
|
||||
"show help",
|
||||
"contextual help overlay",
|
||||
Some(Action::Help),
|
||||
":help [TOPIC]",
|
||||
),
|
||||
tui("show version", "status line", None, ":version"),
|
||||
tui("lock the TUI", "locked screen", Some(Action::Lock), ":lock"),
|
||||
tui(
|
||||
"unlock the TUI",
|
||||
"locked screen",
|
||||
Some(Action::Unlock),
|
||||
":unlock",
|
||||
),
|
||||
];
|
||||
|
||||
const fn tui(
|
||||
operation: &'static str,
|
||||
element: &'static str,
|
||||
action: Option<Action>,
|
||||
command: &'static str,
|
||||
) -> TuiCoverage {
|
||||
TuiCoverage {
|
||||
operation,
|
||||
element,
|
||||
action,
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TuiCoverage {
|
||||
pub operation: &'static str,
|
||||
pub element: &'static str,
|
||||
pub action: Option<Action>,
|
||||
pub command: &'static str,
|
||||
}
|
||||
|
||||
impl TuiCoverage {
|
||||
pub fn default_hotkey(self) -> Option<&'static str> {
|
||||
let action = self.action?;
|
||||
ACTIONS
|
||||
.iter()
|
||||
.find(|spec| spec.action == action)
|
||||
.and_then(|spec| spec.bindings.first())
|
||||
.map(|binding| binding.display)
|
||||
}
|
||||
}
|
||||
|
||||
const fn coverage(operation: &'static str, command: &'static str) -> CommandCoverage {
|
||||
CommandCoverage { operation, command }
|
||||
}
|
||||
@@ -852,6 +1104,35 @@ mod tests {
|
||||
.any(|row| row.command.starts_with(command))
|
||||
);
|
||||
}
|
||||
|
||||
for command_row in COMMAND_COVERAGE {
|
||||
assert!(
|
||||
TUI_COVERAGE
|
||||
.iter()
|
||||
.any(|row| row.operation == command_row.operation),
|
||||
"missing TUI coverage for {}",
|
||||
command_row.operation
|
||||
);
|
||||
}
|
||||
for (index, row) in TUI_COVERAGE.iter().enumerate() {
|
||||
assert!(!row.operation.is_empty());
|
||||
assert!(!row.element.is_empty());
|
||||
assert!(row.command.starts_with(':'));
|
||||
assert!(
|
||||
!TUI_COVERAGE[index + 1..]
|
||||
.iter()
|
||||
.any(|other| other.operation == row.operation),
|
||||
"duplicate TUI operation {}",
|
||||
row.operation
|
||||
);
|
||||
if row.action.is_some() {
|
||||
assert!(
|
||||
row.default_hotkey().is_some(),
|
||||
"{} has no registered default hotkey",
|
||||
row.operation
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user