Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
23 changes: 10 additions & 13 deletions resources/icons/hicolor/scalable/apps/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions src/app/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,69 @@ impl From<zbus::Error> 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<String>, 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);
}
}
}
67 changes: 62 additions & 5 deletions src/app/fprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub async fn list_enrolled_fingers_dbus(
device: &DeviceProxy<'static>,
username: String,
) -> zbus::Result<Vec<String>> {
validate_username(&username)?;
device.list_enrolled_fingers(&username).await
}

Expand All @@ -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?;
Expand All @@ -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?;
Expand All @@ -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;
Expand Down Expand Up @@ -92,22 +100,23 @@ pub async fn clear_all_fingers_dbus(

pub async fn enroll_fingerprint_process<S>(
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<Message> + 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),
};
Expand All @@ -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);
}
Expand Down Expand Up @@ -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());
}
}
51 changes: 32 additions & 19 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -299,14 +315,11 @@ impl cosmic::Application for AppModel {
std::any::TypeId::of::<EnrollmentSubscription>(),
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
Expand Down