launcher: default to GUI and hard-fail unsupported paste chars
This commit is contained in:
parent
7225ed6a1a
commit
fa47fa4e30
@ -12,7 +12,7 @@ pub use diagnostics::{DiagnosticsLog, PerformanceSample, SnapshotReport, quality
|
|||||||
pub use state::{DeviceSelection, InputRouting, LauncherState, ViewMode};
|
pub use state::{DeviceSelection, InputRouting, LauncherState, ViewMode};
|
||||||
|
|
||||||
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
||||||
if args.iter().any(|arg| arg == "--launcher") {
|
if should_run_launcher(args) {
|
||||||
let server_addr = resolve_server_addr(args);
|
let server_addr = resolve_server_addr(args);
|
||||||
ui::run_gui_launcher(server_addr)?;
|
ui::run_gui_launcher(server_addr)?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@ -20,6 +20,14 @@ pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decides when to present the GUI launcher instead of direct session startup.
|
||||||
|
fn should_run_launcher(args: &[String]) -> bool {
|
||||||
|
if args.iter().any(|arg| arg == "--no-launcher") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
args.iter().any(|arg| arg == "--launcher") || args.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
||||||
let mut envs = BTreeMap::new();
|
let mut envs = BTreeMap::new();
|
||||||
envs.insert(
|
envs.insert(
|
||||||
@ -107,4 +115,35 @@ mod tests {
|
|||||||
let args = vec!["http://server:50051".to_string()];
|
let args = vec!["http://server:50051".to_string()];
|
||||||
assert!(!maybe_run_launcher(&args).expect("launcher check"));
|
assert!(!maybe_run_launcher(&args).expect("launcher check"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn maybe_run_launcher_returns_true_with_launcher_flag() {
|
||||||
|
let args = vec!["--launcher".to_string()];
|
||||||
|
assert!(maybe_run_launcher(&args).expect("launcher should run"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(coverage)]
|
||||||
|
fn maybe_run_launcher_defaults_to_launcher_for_empty_args() {
|
||||||
|
let args: Vec<String> = vec![];
|
||||||
|
assert!(maybe_run_launcher(&args).expect("launcher should run"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_run_launcher_defaults_true_for_empty_args() {
|
||||||
|
assert!(should_run_launcher(&[]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_run_launcher_honors_explicit_opt_out() {
|
||||||
|
let args = vec!["--no-launcher".to_string()];
|
||||||
|
assert!(!should_run_launcher(&args));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_run_launcher_respects_direct_server_args() {
|
||||||
|
let args = vec!["http://server:50051".to_string()];
|
||||||
|
assert!(!should_run_launcher(&args));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
"client/src/launcher/mod.rs": {
|
"client/src/launcher/mod.rs": {
|
||||||
"clippy_warnings": 4,
|
"clippy_warnings": 4,
|
||||||
"doc_debt": 4,
|
"doc_debt": 4,
|
||||||
"loc": 110
|
"loc": 149
|
||||||
},
|
},
|
||||||
"client/src/launcher/state.rs": {
|
"client/src/launcher/state.rs": {
|
||||||
"clippy_warnings": 8,
|
"clippy_warnings": 8,
|
||||||
@ -193,7 +193,7 @@
|
|||||||
"server/src/paste.rs": {
|
"server/src/paste.rs": {
|
||||||
"clippy_warnings": 6,
|
"clippy_warnings": 6,
|
||||||
"doc_debt": 3,
|
"doc_debt": 3,
|
||||||
"loc": 146
|
"loc": 204
|
||||||
},
|
},
|
||||||
"server/src/runtime_support.rs": {
|
"server/src/runtime_support.rs": {
|
||||||
"clippy_warnings": 14,
|
"clippy_warnings": 14,
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"client/src/launcher/mod.rs": {
|
"client/src/launcher/mod.rs": {
|
||||||
"line_percent": 96.15384615384616,
|
"line_percent": 96.15384615384616,
|
||||||
"loc": 110
|
"loc": 149
|
||||||
},
|
},
|
||||||
"client/src/launcher/state.rs": {
|
"client/src/launcher/state.rs": {
|
||||||
"line_percent": 99.32432432432432,
|
"line_percent": 99.32432432432432,
|
||||||
@ -134,7 +134,7 @@
|
|||||||
},
|
},
|
||||||
"server/src/paste.rs": {
|
"server/src/paste.rs": {
|
||||||
"line_percent": 96.73913043478261,
|
"line_percent": 96.73913043478261,
|
||||||
"loc": 146
|
"loc": 204
|
||||||
},
|
},
|
||||||
"server/src/runtime_support.rs": {
|
"server/src/runtime_support.rs": {
|
||||||
"line_percent": 96.42857142857143,
|
"line_percent": 96.42857142857143,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ pub fn decrypt(req: &PasteRequest) -> Result<String> {
|
|||||||
let plaintext = cipher
|
let plaintext = cipher
|
||||||
.decrypt(nonce, req.data.as_ref())
|
.decrypt(nonce, req.data.as_ref())
|
||||||
.map_err(|e| anyhow::anyhow!("paste decrypt failed: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("paste decrypt failed: {e}"))?;
|
||||||
Ok(String::from_utf8(plaintext).context("paste plaintext not UTF-8")?)
|
String::from_utf8(plaintext).context("paste plaintext not UTF-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type clipboard text into the HID keyboard gadget.
|
/// Type clipboard text into the HID keyboard gadget.
|
||||||
@ -51,6 +51,13 @@ pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
|||||||
.unwrap_or(8);
|
.unwrap_or(8);
|
||||||
let delay = std::time::Duration::from_millis(delay_ms);
|
let delay = std::time::Duration::from_millis(delay_ms);
|
||||||
|
|
||||||
|
let (unsupported_count, unsupported_preview) = unsupported_chars(text.chars().take(max));
|
||||||
|
if unsupported_count > 0 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"paste contains {unsupported_count} unsupported character(s): {unsupported_preview}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut kb = kb.lock().await;
|
let mut kb = kb.lock().await;
|
||||||
for c in text.chars().take(max) {
|
for c in text.chars().take(max) {
|
||||||
if let Some((usage, mods)) = char_to_usage(c) {
|
if let Some((usage, mods)) = char_to_usage(c) {
|
||||||
@ -65,6 +72,26 @@ pub async fn type_text(kb: &Mutex<File>, text: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Summarize unsupported clipboard characters for explicit operator feedback.
|
||||||
|
fn unsupported_chars(chars: impl Iterator<Item = char>) -> (usize, String) {
|
||||||
|
let mut count = 0usize;
|
||||||
|
let mut preview = Vec::new();
|
||||||
|
for ch in chars {
|
||||||
|
if char_to_usage(ch).is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
if preview.len() < 6 {
|
||||||
|
preview.push(format!("{:?}", ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
(0, String::new())
|
||||||
|
} else {
|
||||||
|
(count, preview.join(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_key() -> Result<[u8; 32]> {
|
fn load_key() -> Result<[u8; 32]> {
|
||||||
let raw = std::env::var("LESAVKA_PASTE_KEY")
|
let raw = std::env::var("LESAVKA_PASTE_KEY")
|
||||||
.context("LESAVKA_PASTE_KEY not set (required for PasteText RPC)")?;
|
.context("LESAVKA_PASTE_KEY not set (required for PasteText RPC)")?;
|
||||||
@ -73,7 +100,7 @@ fn load_key() -> Result<[u8; 32]> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{decrypt, type_text};
|
use super::{decrypt, type_text, unsupported_chars};
|
||||||
use chacha20poly1305::aead::{Aead, KeyInit};
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
||||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
use lesavka_common::lesavka::PasteRequest;
|
use lesavka_common::lesavka::PasteRequest;
|
||||||
@ -134,12 +161,43 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("open temp file");
|
.expect("open temp file");
|
||||||
let kb = Mutex::new(file);
|
let kb = Mutex::new(file);
|
||||||
type_text(&kb, "A!🙂").await.expect("type text");
|
type_text(&kb, "A!?").await.expect("type text");
|
||||||
|
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
let mut file = File::open(&path).await.expect("reopen temp file");
|
let mut file = File::open(&path).await.expect("reopen temp file");
|
||||||
file.read_to_end(&mut bytes).await.expect("read reports");
|
file.read_to_end(&mut bytes).await.expect("read reports");
|
||||||
assert_eq!(bytes.len(), 32);
|
assert_eq!(bytes.len(), 48);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsupported_chars_reports_count_and_preview() {
|
||||||
|
let (count, preview) = unsupported_chars("ok🙂éx".chars());
|
||||||
|
assert_eq!(count, 2);
|
||||||
|
assert!(preview.contains("'🙂'"));
|
||||||
|
assert!(preview.contains("'é'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
/// Reject unsupported clipboard characters instead of silently dropping them.
|
||||||
|
fn type_text_rejects_unsupported_chars() {
|
||||||
|
let rt = Runtime::new().expect("runtime");
|
||||||
|
with_var("LESAVKA_PASTE_DELAY_MS", Some("0"), || {
|
||||||
|
rt.block_on(async {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let path = dir.path().join("hidg0.bin");
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)
|
||||||
|
.await
|
||||||
|
.expect("open temp file");
|
||||||
|
let kb = Mutex::new(file);
|
||||||
|
let err = type_text(&kb, "pw🙂").await.expect_err("unsupported char should fail");
|
||||||
|
assert!(err.to_string().contains("unsupported character"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user