Complete desktop parity and security audit (#42)
This commit is contained in:
@@ -43,6 +43,9 @@ differences, and executable security audit are documented in
|
|||||||
[`docs/cli-parity.md`](docs/cli-parity.md).
|
[`docs/cli-parity.md`](docs/cli-parity.md).
|
||||||
The Mutt-inspired terminal interaction model, default keys, and complete TUI
|
The Mutt-inspired terminal interaction model, default keys, and complete TUI
|
||||||
operation matrix are documented in [`apps/tui/COMMANDS.md`](apps/tui/COMMANDS.md).
|
operation matrix are documented in [`apps/tui/COMMANDS.md`](apps/tui/COMMANDS.md).
|
||||||
|
Desktop parity, native platform smoke checks, accessibility limitations, and
|
||||||
|
the executable security audit are documented in
|
||||||
|
[`docs/desktop-audit.md`](docs/desktop-audit.md).
|
||||||
Lossless structured entry fields, semantic/sensitivity metadata, conflict
|
Lossless structured entry fields, semantic/sensitivity metadata, conflict
|
||||||
tokens, and atomic frontend saves are documented in
|
tokens, and atomic frontend saves are documented in
|
||||||
[`docs/entry-documents.md`](docs/entry-documents.md).
|
[`docs/entry-documents.md`](docs/entry-documents.md).
|
||||||
|
|||||||
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
use iced::keyboard::{self, key::Named};
|
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)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
pub enum UiAction {
|
pub enum UiAction {
|
||||||
About,
|
About,
|
||||||
@@ -314,8 +340,12 @@ pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec>
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn shortcut_label(action: UiAction) -> Option<String> {
|
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?;
|
let shortcut = spec_for(action).shortcut?;
|
||||||
if cfg!(target_os = "macos") {
|
if platform == DesktopPlatform::MacOs {
|
||||||
Some(shortcut.to_owned())
|
Some(shortcut.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
|
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> {
|
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() {
|
if key.as_ref() == keyboard::Key::Named(Named::Tab) && modifiers.is_empty() {
|
||||||
return Some(UiAction::TogglePaneFocus);
|
return Some(UiAction::TogglePaneFocus);
|
||||||
}
|
}
|
||||||
if key.as_ref() == keyboard::Key::Named(Named::F1) {
|
if key.as_ref() == keyboard::Key::Named(Named::F1) {
|
||||||
return Some(UiAction::Help);
|
return Some(UiAction::Help);
|
||||||
}
|
}
|
||||||
if !modifiers.command() {
|
if !modifiers.contains(platform.primary_modifier()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match key.as_ref() {
|
match key.as_ref() {
|
||||||
@@ -940,8 +978,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn conventional_shortcuts_resolve_to_the_registered_actions() {
|
fn conventional_shortcuts_resolve_on_every_supported_platform() {
|
||||||
let primary = keyboard::Modifiers::COMMAND;
|
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_label_for(UiAction::Save, platform).as_deref(),
|
||||||
|
Some(label)
|
||||||
|
);
|
||||||
for (key, expected) in [
|
for (key, expected) in [
|
||||||
("o", UiAction::OpenFolder),
|
("o", UiAction::OpenFolder),
|
||||||
("s", UiAction::Save),
|
("s", UiAction::Save),
|
||||||
@@ -956,21 +1006,35 @@ mod tests {
|
|||||||
(",", UiAction::Settings),
|
(",", UiAction::Settings),
|
||||||
] {
|
] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shortcut_action(&keyboard::Key::Character(key.into()), primary),
|
shortcut_action_for(platform, &keyboard::Key::Character(key.into()), primary,),
|
||||||
Some(expected)
|
Some(expected)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shortcut_action(
|
shortcut_action_for(
|
||||||
|
platform,
|
||||||
&keyboard::Key::Character("f".into()),
|
&keyboard::Key::Character("f".into()),
|
||||||
primary | keyboard::Modifiers::SHIFT,
|
primary | keyboard::Modifiers::SHIFT,
|
||||||
),
|
),
|
||||||
Some(UiAction::SearchContents)
|
Some(UiAction::SearchContents)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shortcut_action(&keyboard::Key::Named(Named::F1), keyboard::Modifiers::NONE),
|
shortcut_action_for(
|
||||||
|
platform,
|
||||||
|
&keyboard::Key::Named(Named::F1),
|
||||||
|
keyboard::Modifiers::NONE,
|
||||||
|
),
|
||||||
Some(UiAction::Help)
|
Some(UiAction::Help)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
shortcut_action_for(
|
||||||
|
DesktopPlatform::MacOs,
|
||||||
|
&keyboard::Key::Character("s".into()),
|
||||||
|
keyboard::Modifiers::CTRL,
|
||||||
|
),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
|
assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -852,12 +852,16 @@ fn main() -> iced::Result {
|
|||||||
.title(ironstorage::PRODUCT_NAME)
|
.title(ironstorage::PRODUCT_NAME)
|
||||||
.subscription(App::subscription)
|
.subscription(App::subscription)
|
||||||
.exit_on_close_request(false)
|
.exit_on_close_request(false)
|
||||||
.window(window::Settings {
|
.window(window_settings())
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_settings() -> window::Settings {
|
||||||
|
window::Settings {
|
||||||
size: Size::new(1_080.0, 720.0),
|
size: Size::new(1_080.0, 720.0),
|
||||||
min_size: Some(Size::new(720.0, 480.0)),
|
min_size: Some(Size::new(720.0, 480.0)),
|
||||||
..window::Settings::default()
|
..window::Settings::default()
|
||||||
})
|
}
|
||||||
.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -3345,11 +3349,8 @@ impl App {
|
|||||||
.min_size(220)
|
.min_size(220)
|
||||||
.on_resize(8, Message::PaneResized);
|
.on_resize(8, Message::PaneResized);
|
||||||
|
|
||||||
let shortcut = if cfg!(target_os = "macos") {
|
let shortcut = action::shortcut_label(UiAction::CommandPalette)
|
||||||
"⌘K"
|
.expect("the command palette has a registered shortcut");
|
||||||
} else {
|
|
||||||
"Ctrl+K"
|
|
||||||
};
|
|
||||||
let command_input = text_input(
|
let command_input = text_input(
|
||||||
&format!("Search commands ({shortcut})"),
|
&format!("Search commands ({shortcut})"),
|
||||||
self.palette.query(),
|
self.palette.query(),
|
||||||
@@ -5034,6 +5035,89 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
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)]
|
#[derive(Clone, Default)]
|
||||||
struct ManualClock(Arc<Mutex<Duration>>);
|
struct ManualClock(Arc<Mutex<Duration>>);
|
||||||
|
|
||||||
|
|||||||
@@ -160,3 +160,35 @@ fn accelerator(action: UiAction) -> Option<Accelerator> {
|
|||||||
};
|
};
|
||||||
Some(Accelerator::new(Some(modifiers), code))
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
121
docs/desktop-audit.md
Normal file
121
docs/desktop-audit.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Desktop parity, platform, and security audit
|
||||||
|
|
||||||
|
The Iced application is a presentation adapter over `crates/storage`. The
|
||||||
|
tables below are the milestone 01/02 coverage record. `action.rs` is the single
|
||||||
|
desktop action and enablement registry used by menus, shortcuts, direct
|
||||||
|
controls, context controls, Help, and the command palette. A desktop unit test
|
||||||
|
requires every registered action ID to remain present in this document.
|
||||||
|
|
||||||
|
## Coverage matrix
|
||||||
|
|
||||||
|
| Area and compatible operation | Registered desktop action and menu | Direct control, dialog, or view | Command palette |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Configuration and lock: open configured store | `open-folder` (File) | Open Folder button and native folder picker; storage validates and persists configuration | Yes |
|
||||||
|
| Configuration and lock: edit shared settings | `settings` (IronStorage) | Labelled Settings form for vault, default key, and inactivity timeout | Yes |
|
||||||
|
| Configuration and lock: refresh typed tree | `refresh` (View) | Sidebar Refresh button | Yes |
|
||||||
|
| Configuration and lock: lock/unlock | `lock` (Tools) | Lock button; opening protected content starts storage authentication | Lock only; unlock is the protected action being resumed |
|
||||||
|
| Base pass: `init` root or nested recipient policy | `initialize-store`, `new-folder` (File) | Recipient/default-key form and explicit replacement confirmation | Yes |
|
||||||
|
| Base pass: default/list/`ls`/`list` | `refresh` (View) | Expandable, scrollable storage-provided sidebar tree | Yes for refresh; browsing is direct navigation |
|
||||||
|
| Base pass: `show` and reload | `open-entry`, `reload-entry` (Entry) | Entry path control, tree activation, structured viewer, Reload button | Yes |
|
||||||
|
| Base pass: `insert`/`add` | `new-entry` (File) | New Entry form creates a lossless draft, then the structured editor saves it | Yes |
|
||||||
|
| Base pass: `edit` and save | `edit-entry`, `save` (Entry/File) | Edit and Save buttons; shared Save/Discard/Cancel guard | Yes |
|
||||||
|
| Base pass: `generate` and replace | `generate-password` (Entry) | Field Generate control and explicit replacement confirmation | Yes |
|
||||||
|
| Base pass: explicit secret display and clipboard | `toggle-reveal`, `copy-field`, `copy-edited-field` (Entry) | Per-field labelled Reveal/Hide and Copy controls | Yes |
|
||||||
|
| Base pass: `find` | `find` (Edit) | Name-search form and typed result activation | Yes |
|
||||||
|
| Base pass: `grep` | `search-contents` (Edit) | Authenticated decrypted-search form and typed result activation | Yes |
|
||||||
|
| Base pass: `mv`/`rename`, `cp`/`copy`, `rm`/`remove` | `move-entry`, `copy-entry`, `delete-entry` (Entry) | Sidebar context controls and validated mutation forms; delete is confirmed | Yes |
|
||||||
|
| Pass OTP: code/show and timed copy | `generate-otp`, `copy-otp` (Entry) | OTP panel shows typed metadata, code, validity, and HOTP confirmation | Yes |
|
||||||
|
| Pass OTP: insert/add/append and validate | `import-otp` (Entry) | URI/QR import form; storage validates, replaces, and commits | Yes |
|
||||||
|
| Pass OTP: URI terminal/clipboard/QR | `show-otp-uri`, `copy-otp-uri`, `show-otp-qr` (Entry) | Explicit secret view, timed copy, and storage-provided QR matrix | Yes |
|
||||||
|
| Pass OTP: remove | `remove-otp` (Entry) | Explicit permanent-removal confirmation | Yes |
|
||||||
|
| Embedded Git: status, history, remote divergence | `git-status` (Tools) | Git dashboard renders the storage snapshot and recent history | Yes |
|
||||||
|
| Embedded Git: fetch/pull | `git-pull` (Tools) | Pull progress/cancel control; fetch is the receive phase of the typed pull | Yes |
|
||||||
|
| Embedded Git: push | `git-push` (Tools) | Push progress/cancel control | Yes |
|
||||||
|
| Embedded Git: sync and conflicts | `git-sync` (Tools) | Sync progress plus per-path local/remote conflict choices | Yes |
|
||||||
|
| Desktop command discovery and pane navigation | `command-palette`, `toggle-pane-focus` (View) | Toolbar search, Tab focus transfer, arrows/Home/End/Enter navigation | The palette opens itself by shortcut/control and is intentionally not a result |
|
||||||
|
| Desktop text editing | `undo`, `redo`, `cut`, `paste` (Edit) | Platform text controls own these standard operations; the shared registry explains why app-level dispatch is disabled | Listed with its current availability |
|
||||||
|
| Window/application lifecycle | `about`, `close-window`, `quit`, `minimize` (IronStorage/File/Window) | Standard native roles on macOS and equivalent in-window actions elsewhere | Yes except native-only minimize behavior where the OS owns the role |
|
||||||
|
| Help and current shortcuts | `help` (Help) | Labelled, scrollable Help view generated from the action registry | Yes |
|
||||||
|
|
||||||
|
The CLI/TUI expose a few terminal-shaped selectors rather than separate domain
|
||||||
|
capabilities. Generic `show --qrcode`, OTP version output, arbitrary embedded
|
||||||
|
Git plumbing, and shell completion are therefore not duplicate desktop
|
||||||
|
actions. The desktop viewer already renders an ordinary field, OTP version
|
||||||
|
compatibility belongs to storage/CLI metadata, Git initialization and commits
|
||||||
|
occur within typed storage mutations, remote configuration is shared Settings,
|
||||||
|
and desktop command discovery is the palette. Git history and conflict
|
||||||
|
resolution are controls inside `git-status` rather than separate commands.
|
||||||
|
These are deliberate presentation differences, not storage-feature gaps.
|
||||||
|
|
||||||
|
## Interaction and accessibility evidence
|
||||||
|
|
||||||
|
- Keyboard-only operation uses Tab between panes, arrows/Home/End/Enter within
|
||||||
|
trees, fields, menus, and palette results, Escape to dismiss, and registered
|
||||||
|
accelerators. The same controls are mouse/touch activatable; sidebar context
|
||||||
|
actions also accept a secondary click.
|
||||||
|
- Both panes are independently scrollable, the divider is resizable, action
|
||||||
|
rows wrap, long names and multiline values are retained, and the supported
|
||||||
|
narrow window floor is 720 by 480 logical pixels. Iced/winit applies native
|
||||||
|
display scaling before layout.
|
||||||
|
- Interactive controls use visible, operation-specific text instead of icon-
|
||||||
|
only labels. Focused/selected controls use the theme's primary contrast pair;
|
||||||
|
light, dark, and operating-system high-contrast palettes retain a visible
|
||||||
|
text label as a non-colour focus cue.
|
||||||
|
- The app implements no animation or motion-driven state transition. The one-
|
||||||
|
second subscription updates lease, OTP, Git, and clipboard presentation state
|
||||||
|
without moving focus or renewing authentication, so reduced-motion mode has
|
||||||
|
no additional transition to disable.
|
||||||
|
- Iced 0.14 does not expose a stable application API for supplying a native
|
||||||
|
screen-reader accessibility tree. Controls therefore have complete visible
|
||||||
|
labels and deterministic keyboard focus, but native screen-reader role/name
|
||||||
|
integration is an explicit framework limitation rather than a silent stub.
|
||||||
|
|
||||||
|
## Platform menu and workflow smoke contract
|
||||||
|
|
||||||
|
`conventional_shortcuts_resolve_on_every_supported_platform` executes the
|
||||||
|
Command-versus-Control mapping for macOS, Linux, and Windows on every test host.
|
||||||
|
The macOS-only native adapter test verifies its native accelerators. Linux and
|
||||||
|
Windows use the same action registry through the in-window menu, so its labels,
|
||||||
|
enablement, palette dispatch, and workflow state tests are platform-neutral.
|
||||||
|
|
||||||
|
Native equivalent runners use stable Rust and run from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test --package ironstorage-desktop --all-targets
|
||||||
|
cargo check --package ironstorage-desktop --all-targets
|
||||||
|
cargo run --package ironstorage-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
For the final smoke step on each native host:
|
||||||
|
|
||||||
|
| Platform | Menu/accelerator smoke | Primary workflow smoke |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| macOS | System App/File/Edit/View/Entry/Tools/Window/Help menus; Command-O/S/F/K/L, Tab, F1 | Open a test vault, unlock/view/edit/save, search, Git refresh, OTP code/copy, lock, close |
|
||||||
|
| Linux | In-window menus; Ctrl-O/S/F/K/L, Tab, F1 | Same sequence under X11 or Wayland with the desktop portal available |
|
||||||
|
| Windows | In-window menus; Ctrl-O/S/F/K/L, Tab, F1 | Same sequence with the native folder picker and Windows secure store available |
|
||||||
|
|
||||||
|
The automated state tests use temporary real encrypted stores and cover the
|
||||||
|
same primary workflows without retaining a GUI password or starting a helper.
|
||||||
|
Native picker, clipboard, secure-store, and actual screen-reader integration
|
||||||
|
remain native-host smoke checks because CI cannot emulate those OS services.
|
||||||
|
|
||||||
|
## Security and architecture audit
|
||||||
|
|
||||||
|
| Risk | Enforced behavior and executable evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| Plaintext lifetime and persistence | Entry/OTP values use storage `SecretBytes` or zeroizing edit buffers. Lock, expiry, vault switch, and stale completion paths drop the editor and sensitive presentation state. The source audit rejects desktop filesystem writes. |
|
||||||
|
| Masking, errors, and diagnostics | Storage supplies sensitivity and redacted typed errors. Viewer/editor tests require masking until explicit reveal; malformed and non-UTF-8 fields remain lossless. Desktop messages are not `Debug`, and the source audit rejects print/debug/log-style output. |
|
||||||
|
| Clipboard | `NativeClipboardManager` owns timeout and replacement-safe cleanup. Desktop state shows a live remaining-seconds value, cancels cleanup on lock, and ignores stale completions. |
|
||||||
|
| Authentication expiry | Storage authentication leases own the clock and policy. Passive ticks, rendering, pointer movement, and window events do not renew activity; deterministic tests cover expiry during protected state. |
|
||||||
|
| Dirty documents and conflicts | Every entry/vault/window/Git worktree replacement routes through one Save/Discard/Cancel decision. Failed saves and conflicts keep the complete draft. |
|
||||||
|
| Background and window lifecycle | Generation counters reject stale asynchronous results. Lock cancels Git/clipboard work and clears OTP, QR, URI, entry, and editor state. Close and quit use the same dirty guard. |
|
||||||
|
| Repository and domain ownership | The executable source audit rejects repository/Git construction, process launch, OTP/QR parsing, filesystem writes, unsafe blocks, and non-HTTPS literals in production desktop modules. The folder picker may read only a user-selected QR image; all password-store I/O remains in `crates/storage`. |
|
||||||
|
|
||||||
|
Run the complete repository gate after the desktop-specific checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
RUSTFLAGS="-D warnings" cargo check --workspace --all-targets
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user