Complete desktop parity and security audit (#42)

This commit is contained in:
2026-08-10 20:38:28 +02:00
parent 4a54bf7b6a
commit 40b15a9713
5 changed files with 340 additions and 36 deletions

View File

@@ -2,6 +2,32 @@
use iced::keyboard::{self, key::Named};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DesktopPlatform {
MacOs,
Linux,
Windows,
}
impl DesktopPlatform {
pub const fn current() -> Self {
if cfg!(target_os = "macos") {
Self::MacOs
} else if cfg!(target_os = "windows") {
Self::Windows
} else {
Self::Linux
}
}
const fn primary_modifier(self) -> keyboard::Modifiers {
match self {
Self::MacOs => keyboard::Modifiers::LOGO,
Self::Linux | Self::Windows => keyboard::Modifiers::CTRL,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum UiAction {
About,
@@ -314,8 +340,12 @@ pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec>
}
pub fn shortcut_label(action: UiAction) -> Option<String> {
shortcut_label_for(action, DesktopPlatform::current())
}
pub fn shortcut_label_for(action: UiAction, platform: DesktopPlatform) -> Option<String> {
let shortcut = spec_for(action).shortcut?;
if cfg!(target_os = "macos") {
if platform == DesktopPlatform::MacOs {
Some(shortcut.to_owned())
} else {
Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
@@ -685,13 +715,21 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
}
pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<UiAction> {
shortcut_action_for(DesktopPlatform::current(), key, modifiers)
}
pub fn shortcut_action_for(
platform: DesktopPlatform,
key: &keyboard::Key,
modifiers: keyboard::Modifiers,
) -> Option<UiAction> {
if key.as_ref() == keyboard::Key::Named(Named::Tab) && modifiers.is_empty() {
return Some(UiAction::TogglePaneFocus);
}
if key.as_ref() == keyboard::Key::Named(Named::F1) {
return Some(UiAction::Help);
}
if !modifiers.command() {
if !modifiers.contains(platform.primary_modifier()) {
return None;
}
match key.as_ref() {
@@ -940,36 +978,62 @@ mod tests {
}
#[test]
fn conventional_shortcuts_resolve_to_the_registered_actions() {
let primary = keyboard::Modifiers::COMMAND;
for (key, expected) in [
("o", UiAction::OpenFolder),
("s", UiAction::Save),
("f", UiAction::Find),
("k", UiAction::CommandPalette),
("n", UiAction::NewEntry),
("w", UiAction::CloseWindow),
("q", UiAction::Quit),
("x", UiAction::Cut),
("c", UiAction::CopyField),
("v", UiAction::Paste),
(",", UiAction::Settings),
fn conventional_shortcuts_resolve_on_every_supported_platform() {
for (platform, primary, label) in [
(DesktopPlatform::MacOs, keyboard::Modifiers::LOGO, "⌘S"),
(DesktopPlatform::Linux, keyboard::Modifiers::CTRL, "Ctrl+S"),
(
DesktopPlatform::Windows,
keyboard::Modifiers::CTRL,
"Ctrl+S",
),
] {
assert_eq!(
shortcut_action(&keyboard::Key::Character(key.into()), primary),
Some(expected)
shortcut_label_for(UiAction::Save, platform).as_deref(),
Some(label)
);
for (key, expected) in [
("o", UiAction::OpenFolder),
("s", UiAction::Save),
("f", UiAction::Find),
("k", UiAction::CommandPalette),
("n", UiAction::NewEntry),
("w", UiAction::CloseWindow),
("q", UiAction::Quit),
("x", UiAction::Cut),
("c", UiAction::CopyField),
("v", UiAction::Paste),
(",", UiAction::Settings),
] {
assert_eq!(
shortcut_action_for(platform, &keyboard::Key::Character(key.into()), primary,),
Some(expected)
);
}
assert_eq!(
shortcut_action_for(
platform,
&keyboard::Key::Character("f".into()),
primary | keyboard::Modifiers::SHIFT,
),
Some(UiAction::SearchContents)
);
assert_eq!(
shortcut_action_for(
platform,
&keyboard::Key::Named(Named::F1),
keyboard::Modifiers::NONE,
),
Some(UiAction::Help)
);
}
assert_eq!(
shortcut_action(
&keyboard::Key::Character("f".into()),
primary | keyboard::Modifiers::SHIFT,
shortcut_action_for(
DesktopPlatform::MacOs,
&keyboard::Key::Character("s".into()),
keyboard::Modifiers::CTRL,
),
Some(UiAction::SearchContents)
);
assert_eq!(
shortcut_action(&keyboard::Key::Named(Named::F1), keyboard::Modifiers::NONE),
Some(UiAction::Help)
None
);
assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
}

View File

@@ -852,14 +852,18 @@ fn main() -> iced::Result {
.title(ironstorage::PRODUCT_NAME)
.subscription(App::subscription)
.exit_on_close_request(false)
.window(window::Settings {
size: Size::new(1_080.0, 720.0),
min_size: Some(Size::new(720.0, 480.0)),
..window::Settings::default()
})
.window(window_settings())
.run()
}
fn window_settings() -> window::Settings {
window::Settings {
size: Size::new(1_080.0, 720.0),
min_size: Some(Size::new(720.0, 480.0)),
..window::Settings::default()
}
}
impl App {
fn new() -> (Self, Task<Message>) {
let mut app = Self {
@@ -3345,11 +3349,8 @@ impl App {
.min_size(220)
.on_resize(8, Message::PaneResized);
let shortcut = if cfg!(target_os = "macos") {
"⌘K"
} else {
"Ctrl+K"
};
let shortcut = action::shortcut_label(UiAction::CommandPalette)
.expect("the command palette has a registered shortcut");
let command_input = text_input(
&format!("Search commands ({shortcut})"),
self.palette.query(),
@@ -5034,6 +5035,89 @@ mod tests {
use super::*;
#[test]
fn desktop_coverage_matrix_contains_every_registered_action() {
let matrix = include_str!("../../../docs/desktop-audit.md");
for spec in action::ACTIONS {
assert!(
matrix.contains(&format!("`{}`", spec.action.id())),
"desktop parity matrix is missing {}",
spec.action.id()
);
}
for required_surface in [
"Base pass",
"Pass OTP",
"Embedded Git",
"Configuration and lock",
"macOS",
"Linux",
"Windows",
] {
assert!(
matrix.contains(required_surface),
"missing {required_surface}"
);
}
}
#[test]
fn production_desktop_sources_preserve_security_and_architecture_boundaries() {
let sources = [
("action.rs", include_str!("action.rs")),
("editor.rs", include_str!("editor.rs")),
("folder_picker.rs", include_str!("folder_picker.rs")),
("main.rs", include_str!("main.rs")),
("native_menu.rs", include_str!("native_menu.rs")),
("navigation.rs", include_str!("navigation.rs")),
("palette.rs", include_str!("palette.rs")),
];
for (name, source) in sources {
let production = source
.split("#[cfg(test)]\nmod tests")
.next()
.unwrap_or(source);
let forbidden_tokens = [
["std::", "process"].concat(),
["process", "::Command"].concat(),
["Command", "::new("].concat(),
["Repository", "::open"].concat(),
["GitRepository", "::"].concat(),
["OtpUri", "::parse"].concat(),
["qrcode", "::QrCode"].concat(),
["fs::", "write"].concat(),
["File", "::create"].concat(),
["OpenOptions", "::new"].concat(),
["println", "!("].concat(),
["eprintln", "!("].concat(),
["dbg", "!("].concat(),
["log", "::info"].concat(),
["log", "::debug"].concat(),
["log", "::error"].concat(),
["tracing", "::info"].concat(),
["tracing", "::debug"].concat(),
["tracing", "::error"].concat(),
["http", "://"].concat(),
["unsafe", " {"].concat(),
];
for forbidden in &forbidden_tokens {
assert!(
!production.contains(forbidden),
"{name} crosses the desktop architecture boundary with {forbidden}"
);
}
}
}
#[test]
fn window_contract_keeps_both_scrollable_panes_usable_at_narrow_size() {
let settings = window_settings();
assert_eq!(settings.size, Size::new(1_080.0, 720.0));
assert_eq!(settings.min_size, Some(Size::new(720.0, 480.0)));
let app = test_app(None);
let _view = app.view();
}
#[derive(Clone, Default)]
struct ManualClock(Arc<Mutex<Duration>>);

View File

@@ -160,3 +160,35 @@ fn accelerator(action: UiAction) -> Option<Accelerator> {
};
Some(Accelerator::new(Some(modifiers), code))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_menu_uses_standard_macos_accelerators() {
for (action, modifiers, code) in [
(UiAction::Settings, Modifiers::SUPER, Code::Comma),
(UiAction::NewEntry, Modifiers::SUPER, Code::KeyN),
(UiAction::OpenFolder, Modifiers::SUPER, Code::KeyO),
(UiAction::Save, Modifiers::SUPER, Code::KeyS),
(UiAction::CloseWindow, Modifiers::SUPER, Code::KeyW),
(UiAction::Quit, Modifiers::SUPER, Code::KeyQ),
(UiAction::Find, Modifiers::SUPER, Code::KeyF),
(
UiAction::SearchContents,
Modifiers::SUPER | Modifiers::SHIFT,
Code::KeyF,
),
(UiAction::CommandPalette, Modifiers::SUPER, Code::KeyK),
(UiAction::Refresh, Modifiers::SUPER, Code::KeyR),
(UiAction::Lock, Modifiers::SUPER, Code::KeyL),
(UiAction::Help, Modifiers::empty(), Code::F1),
] {
let accelerator = accelerator(action).expect("registered native accelerator");
assert_eq!(accelerator.modifiers(), modifiers, "{action:?}");
assert_eq!(accelerator.key(), code, "{action:?}");
}
assert!(accelerator(UiAction::GeneratePassword).is_none());
}
}