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