diff --git a/CHANGES b/CHANGES index 58eff22..bdefbcf 100644 --- a/CHANGES +++ b/CHANGES @@ -26,3 +26,6 @@ 0.3.11: - Implement a confirmation dialog for "Clear Device" operation - Fix missing bulk deletion logic for all fingerprints of a user +0.3.12: + - Performance improvements + - Updated icon diff --git a/Cargo.toml b/Cargo.toml index aab6fa8..381d00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-ext-fprint" -version = "0.3.11" +version = "0.3.12" edition = "2024" license = "MPL-2.0" description = "GUI for fprintd fingerprint enrolling" diff --git a/resources/icons/hicolor/scalable/apps/icon.svg b/resources/icons/hicolor/scalable/apps/icon.svg index a3b083a..1f973a0 100644 --- a/resources/icons/hicolor/scalable/apps/icon.svg +++ b/resources/icons/hicolor/scalable/apps/icon.svg @@ -1,13 +1,10 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + \ No newline at end of file diff --git a/src/app/error.rs b/src/app/error.rs index 7537129..6426a32 100644 --- a/src/app/error.rs +++ b/src/app/error.rs @@ -59,3 +59,69 @@ impl From for AppError { } } } + +#[cfg(test)] +mod tests { + use super::*; + use zbus::names::ErrorName; + use zbus::message::Message; + + fn create_method_error(name: &str) -> zbus::Error { + let msg = Message::method_call("/", "Ping") + .unwrap() + .destination("org.freedesktop.DBus") + .unwrap() + .build(&()) + .unwrap(); + + let error_name = ErrorName::try_from(name).unwrap(); + // zbus::Error::MethodError(OwnedErrorName, Option, Message) + zbus::Error::MethodError(error_name.into(), None, msg) + } + + #[test] + fn test_zbus_error_conversion() { + let test_cases = vec![ + ("net.reactivated.Fprint.Error.PermissionDenied", AppError::PermissionDenied), + ("net.reactivated.Fprint.Error.AlreadyInUse", AppError::AlreadyInUse), + ("net.reactivated.Fprint.Error.Internal", AppError::Internal), + ("net.reactivated.Fprint.Error.NoEnrolledPrints", AppError::NoEnrolledPrints), + ("net.reactivated.Fprint.Error.ClaimDevice", AppError::ClaimDevice), + ("net.reactivated.Fprint.Error.PrintsNotDeleted", AppError::PrintsNotDeleted), + ("net.reactivated.Fprint.Error.Timeout", AppError::Timeout), + ("net.reactivated.Fprint.Error.DeviceNotFound", AppError::DeviceNotFound), + ]; + + for (error_str, expected) in test_cases { + let zbus_err = create_method_error(error_str); + let app_err = AppError::from(zbus_err); + assert_eq!(app_err, expected, "Failed for error: {}", error_str); + } + } + + #[test] + fn test_unknown_zbus_error() { + let error_str = "net.reactivated.Fprint.Error.UnknownOne"; + let zbus_err = create_method_error(error_str); + let app_err = AppError::from(zbus_err); + + if let AppError::Unknown(msg) = app_err { + assert!(msg.contains(error_str)); + } else { + panic!("Expected AppError::Unknown, got {:?}", app_err); + } + } + + #[test] + fn test_non_method_error() { + // Test a different zbus::Error variant + let zbus_err = zbus::Error::from(std::io::Error::new(std::io::ErrorKind::Other, "test error")); + let app_err = AppError::from(zbus_err); + + if let AppError::Unknown(msg) = app_err { + assert!(msg.contains("test error")); + } else { + panic!("Expected AppError::Unknown, got {:?}", app_err); + } + } +} diff --git a/src/app/fprint.rs b/src/app/fprint.rs index 21777e1..ec6818a 100644 --- a/src/app/fprint.rs +++ b/src/app/fprint.rs @@ -22,6 +22,7 @@ pub async fn list_enrolled_fingers_dbus( device: &DeviceProxy<'static>, username: String, ) -> zbus::Result> { + validate_username(&username)?; device.list_enrolled_fingers(&username).await } @@ -31,6 +32,7 @@ pub async fn delete_fingerprint_dbus( finger: String, username: String, ) -> zbus::Result<()> { + validate_username(&username)?; let device = DeviceProxy::builder(connection).path(path)?.build().await?; device.claim(&username).await?; @@ -44,6 +46,7 @@ pub async fn delete_fingers( path: zbus::zvariant::OwnedObjectPath, username: String, ) -> zbus::Result<()> { + validate_username(&username)?; let device = DeviceProxy::builder(connection).path(path)?.build().await?; device.claim(&username).await?; @@ -60,6 +63,11 @@ pub async fn clear_all_fingers_dbus( let mut last_error = None; for username in usernames { + if let Err(e) = validate_username(&username) { + last_error = Some(e); + continue; + } + if let Err(e) = device.claim(&username).await { last_error = Some(e); continue; @@ -92,22 +100,23 @@ pub async fn clear_all_fingers_dbus( pub async fn enroll_fingerprint_process( connection: zbus::Connection, - path: zbus::zvariant::OwnedObjectPath, - finger_name: String, - username: String, + path: &zbus::zvariant::OwnedObjectPath, + finger_name: &str, + username: &str, output: &mut S, ) -> zbus::Result<()> where S: Sink + Unpin + Send, S::Error: std::fmt::Debug + Send, { + validate_username(&username)?; let device = DeviceProxy::builder(&connection) .path(path)? .build() .await?; // Claim device - match device.claim(&username).await { + match device.claim(username).await { Ok(_) => {} Err(e) => return Err(e), }; @@ -119,7 +128,7 @@ where let _ = output.send(Message::EnrollStart(total_stages)).await; // Start enrollment - if let Err(e) = device.enroll_start(&finger_name).await { + if let Err(e) = device.enroll_start(finger_name).await { let _ = device.release().await; return Err(e); } @@ -165,3 +174,51 @@ where Ok(()) } + +fn validate_username(username: &str) -> zbus::Result<()> { + if username.is_empty() { + return Err(zbus::Error::Failure("Username cannot be empty".to_string())); + } + if username.len() > 255 { + return Err(zbus::Error::Failure("Username is too long".to_string())); + } + if !username.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.') { + return Err(zbus::Error::Failure(format!( + "Invalid characters in username: {}", + username + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_username() { + // Valid usernames + assert!(validate_username("user").is_ok()); + assert!(validate_username("user1").is_ok()); + assert!(validate_username("user_name").is_ok()); + assert!(validate_username("user-name").is_ok()); + assert!(validate_username("user.name").is_ok()); + assert!(validate_username("u").is_ok()); + assert!(validate_username("123").is_ok()); + assert!(validate_username("User").is_ok()); // Uppercase is allowed by our validation + + // Invalid usernames + assert!(validate_username("").is_err()); + assert!(validate_username("user name").is_err()); // space + assert!(validate_username("user/name").is_err()); // slash + assert!(validate_username("user@name").is_err()); // @ + assert!(validate_username("user!name").is_err()); // ! + assert!(validate_username("user?name").is_err()); // ? + + let long_name = "a".repeat(256); + assert!(validate_username(&long_name).is_err()); + + let max_len_name = "a".repeat(255); + assert!(validate_username(&max_len_name).is_ok()); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 9ef3072..cbc01cb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -124,18 +124,7 @@ impl cosmic::Application for AppModel { nav, key_binds: HashMap::new(), // Optional configuration file for an application. - config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION) - .map(|context| match Config::get_entry(&context) { - Ok(config) => config, - Err((errors, config)) => { - for why in errors { - tracing::error!(%why, "error loading app config"); - } - - config - } - }) - .unwrap_or_default(), + config: Config::default(), status: fl!("status-connecting"), device_path: None, device_proxy: None, @@ -170,7 +159,34 @@ impl cosmic::Application for AppModel { cosmic::Action::App, ); - (app, command.chain(connect_task)) + let config_task = Task::perform( + async move { + let config = tokio::task::spawn_blocking(move || { + cosmic_config::Config::new(Self::APP_ID, Config::VERSION) + .map(|context| match Config::get_entry(&context) { + Ok(config) => config, + Err((errors, config)) => { + for why in errors { + tracing::error!(%why, "error loading app config"); + } + + config + } + }) + .unwrap_or_default() + }) + .await + .unwrap_or_else(|e| { + tracing::error!("Config task join error: {}", e); + Config::default() + }); + + Message::UpdateConfig(config) + }, + cosmic::Action::App, + ); + + (app, Task::batch(vec![command, connect_task, config_task])) } /// Elements to pack at the start of the header bar. @@ -299,14 +315,11 @@ impl cosmic::Application for AppModel { std::any::TypeId::of::(), cosmic::iced::stream::channel(100, move |mut output| async move { // Implement enrollment stream here - let username = (*user.username).clone(); - let device_path = (*device_path).clone(); - let finger_name = (*finger_name).clone(); match enroll_fingerprint_process( connection, - device_path, - finger_name, - username, + &device_path, + &finger_name, + &user.username, &mut output, ) .await