diff --git a/CLAUDE.md b/CLAUDE.md index d3c928e..ba81f18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,8 +45,8 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - `power.rs` — Reboot/Shutdown via loginctl - `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen - `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback -- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback -- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions) +- `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200) +- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files) - `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger - `resources/style.css` — Catppuccin-inspiriertes Theme @@ -57,13 +57,13 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads - **Socket-Cancellation**: `Arc>>` + `AtomicBool` für saubere Abbrüche - **Avatar-Cache**: `HashMap` in `Rc>` -- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate -- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd. +- **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate. Blurred Texture wird per `Rc>>` über alle Monitore gecacht (1x GPU-Renderpass statt N). +- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd. Device-Proxy in `GreeterState` gecacht, Generation-Counter gegen Race Conditions bei schnellem User-Switch. - **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur) - **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt - **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config - **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var -- **File Permissions**: Cache-Dateien 0o600 +- **File Permissions**: Cache-Verzeichnisse 0o700 via `DirBuilder::mode()`, Cache-Dateien 0o600 - **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests - **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor - **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None` diff --git a/Cargo.toml b/Cargo.toml index ab3f20a..7ac6f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moongreet" -version = "0.6.1" +version = "0.6.2" edition = "2024" description = "A greetd greeter for Wayland with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md index d2578c2..f43e25b 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,12 @@ # Decisions +## 2026-03-30 – Full audit fix: security, quality, performance (v0.6.2) + +- **Who**: Ragnar, Dom +- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing. +- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees. +- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests. + ## 2026-03-29 – Fingerprint authentication via greetd multi-stage PAM - **Who**: Ragnar, Dom diff --git a/src/config.rs b/src/config.rs index 6026081..0a9517c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -72,8 +72,12 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config { Some(parent.join(&bg).to_string_lossy().to_string()); } } - if appearance.background_blur.is_some() { - merged.background_blur = appearance.background_blur; + if let Some(blur) = appearance.background_blur { + if blur.is_finite() { + merged.background_blur = Some(blur.clamp(0.0, 200.0)); + } else { + log::warn!("Ignoring non-finite background-blur value"); + } } if appearance.gtk_theme.is_some() { merged.gtk_theme = appearance.gtk_theme; @@ -283,4 +287,45 @@ mod tests { let config = load_config(Some(&paths)); assert!(!config.fingerprint_enabled); } + + // -- Blur validation tests -- + + #[test] + fn load_config_blur_clamped_to_max() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + fs::write(&conf, "[appearance]\nbackground-blur = 999.0\n").unwrap(); + let config = load_config(Some(&[conf])); + assert_eq!(config.background_blur, Some(200.0)); + } + + #[test] + fn load_config_blur_negative_clamped_to_zero() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + fs::write(&conf, "[appearance]\nbackground-blur = -5.0\n").unwrap(); + let config = load_config(Some(&[conf])); + assert_eq!(config.background_blur, Some(0.0)); + } + + #[test] + fn load_config_blur_nan_rejected() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + // TOML doesn't support NaN literals, but the parser may return NaN for nan + fs::write(&conf, "[appearance]\nbackground-blur = nan\n").unwrap(); + let config = load_config(Some(&[conf])); + // nan is not valid TOML float, so the whole config parse fails → no blur + assert!(config.background_blur.is_none()); + } + + #[test] + fn load_config_blur_inf_rejected() { + let dir = tempfile::tempdir().unwrap(); + let conf = dir.path().join("moongreet.toml"); + fs::write(&conf, "[appearance]\nbackground-blur = inf\n").unwrap(); + let config = load_config(Some(&[conf])); + // inf is valid TOML → parsed as f32::INFINITY → rejected by is_finite() guard + assert!(config.background_blur.is_none()); + } } diff --git a/src/greeter.rs b/src/greeter.rs index 3d10560..648ded0 100644 --- a/src/greeter.rs +++ b/src/greeter.rs @@ -87,7 +87,9 @@ fn is_valid_username(name: &str) -> bool { if name.is_empty() || name.len() > MAX_USERNAME_LENGTH { return false; } - let first = name.chars().next().unwrap(); + let Some(first) = name.chars().next() else { + return false; + }; if !first.is_ascii_alphanumeric() && first != '_' { return false; } @@ -95,6 +97,14 @@ fn is_valid_username(name: &str) -> bool { .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' || c == '@') } +/// Validate a GTK theme name — alphanumeric plus `_-+.` only. +fn is_valid_gtk_theme(name: &str) -> bool { + !name.is_empty() + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.')) +} + /// Load background texture from filesystem. pub fn load_background_texture(bg_path: &Path) -> Option { if let Ok(meta) = std::fs::metadata(bg_path) @@ -140,8 +150,8 @@ fn render_blurred_texture( // Clip output to original texture size snapshot.push_clip(&graphene_rs::Rect::new(pad, pad, w, h)); snapshot.push_blur(sigma as f64); - // Render texture with padding on all sides (edges repeat via oversized bounds) - snapshot.append_texture(texture, &graphene_rs::Rect::new(0.0, 0.0, w + 2.0 * pad, h + 2.0 * pad)); + // Render texture at native size, shifted so edge pixels fill the padding area + snapshot.append_texture(texture, &graphene_rs::Rect::new(-pad, -pad, w + 2.0 * pad, h + 2.0 * pad)); snapshot.pop(); // blur snapshot.pop(); // clip @@ -154,6 +164,7 @@ fn render_blurred_texture( pub fn create_wallpaper_window( texture: &gdk::Texture, blur_radius: Option, + blur_cache: &Rc>>, app: >k::Application, ) -> gtk::ApplicationWindow { let window = gtk::ApplicationWindow::builder() @@ -161,14 +172,19 @@ pub fn create_wallpaper_window( .build(); window.add_css_class("wallpaper"); - let background = create_background_picture(texture, blur_radius); + let background = create_background_picture(texture, blur_radius, blur_cache); window.set_child(Some(&background)); window } /// Create a Picture widget for the wallpaper background, optionally with GPU blur. -fn create_background_picture(texture: &gdk::Texture, blur_radius: Option) -> gtk::Picture { +/// Uses `blur_cache` to compute the blurred texture only once across all monitors. +fn create_background_picture( + texture: &gdk::Texture, + blur_radius: Option, + blur_cache: &Rc>>, +) -> gtk::Picture { let background = gtk::Picture::for_paintable(texture); background.set_content_fit(gtk::ContentFit::Cover); background.set_hexpand(true); @@ -176,9 +192,16 @@ fn create_background_picture(texture: &gdk::Texture, blur_radius: Option) - if let Some(sigma) = blur_radius.filter(|s| *s > 0.0) { let texture = texture.clone(); + let blur_cache = blur_cache.clone(); background.connect_realize(move |picture| { + // Use cached blurred texture if available + if let Some(ref cached) = *blur_cache.borrow() { + picture.set_paintable(Some(cached)); + return; + } if let Some(blurred) = render_blurred_texture(picture, &texture, sigma) { picture.set_paintable(Some(&blurred)); + *blur_cache.borrow_mut() = Some(blurred); } }); } @@ -195,12 +218,17 @@ struct GreeterState { greetd_sock: Arc>>, login_cancelled: Arc, fingerprint_available: bool, + /// Incremented on each user switch to discard stale async results. + user_switch_generation: u64, + /// Cached fprintd device proxy — initialized once on first use. + fingerprint_probe: Option, } /// Create the main greeter window with login UI. pub fn create_greeter_window( texture: Option<&gdk::Texture>, config: &Config, + blur_cache: &Rc>>, app: >k::Application, ) -> gtk::ApplicationWindow { let window = gtk::ApplicationWindow::builder() @@ -211,11 +239,7 @@ pub fn create_greeter_window( // Apply GTK theme from config if let Some(ref theme_name) = config.gtk_theme { - if !theme_name.is_empty() - && theme_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.')) - { + if is_valid_gtk_theme(theme_name) { if let Some(settings) = gtk::Settings::default() { settings.set_gtk_theme_name(Some(theme_name)); } @@ -241,6 +265,8 @@ pub fn create_greeter_window( greetd_sock: Arc::new(Mutex::new(None)), login_cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)), fingerprint_available: false, + user_switch_generation: 0, + fingerprint_probe: None, })); // Root overlay for layering @@ -249,7 +275,7 @@ pub fn create_greeter_window( // Background wallpaper if let Some(texture) = texture { - overlay.set_child(Some(&create_background_picture(texture, config.background_blur))); + overlay.set_child(Some(&create_background_picture(texture, config.background_blur, blur_cache))); } // Main layout: 3 rows (top spacer, center login, bottom bar) @@ -559,6 +585,7 @@ pub fn create_greeter_window( } /// Select the last user or the first available user. +#[allow(clippy::too_many_arguments)] fn select_initial_user( users: &[User], state: &Rc>, @@ -601,6 +628,7 @@ fn select_initial_user( } /// Update the UI to show the selected user. +#[allow(clippy::too_many_arguments)] fn switch_to_user( user: &User, state: &Rc>, @@ -616,11 +644,13 @@ fn switch_to_user( strings: &'static Strings, ) { log::debug!("Switching to user: {}", user.username); - { + let generation = { let mut s = state.borrow_mut(); s.selected_user = Some(user.clone()); s.fingerprint_available = false; - } + s.user_switch_generation += 1; + s.user_switch_generation + }; username_label.set_text(user.display_name()); password_entry.set_text(""); @@ -650,7 +680,7 @@ fn switch_to_user( // Pre-select last used session for this user select_last_session(&user.username, session_dropdown, sessions); - // Probe fprintd for fingerprint availability + // Probe fprintd for fingerprint availability (cached device proxy, generation-guarded) if fingerprint_enabled { let username = user.username.clone(); glib::spawn_future_local(clone!( @@ -659,9 +689,29 @@ fn switch_to_user( #[strong] state, async move { - let mut probe = crate::fingerprint::FingerprintProbe::new(); - probe.init_async().await; - let available = probe.is_available_async(&username).await; + // Initialize probe on first use, then reuse cached device proxy + let needs_init = state.borrow().fingerprint_probe.is_none(); + if needs_init { + let mut probe = crate::fingerprint::FingerprintProbe::new(); + probe.init_async().await; + state.borrow_mut().fingerprint_probe = Some(probe); + } + + // Take probe out of state to avoid holding borrow across await + let probe = state.borrow_mut().fingerprint_probe.take(); + let available = match &probe { + Some(p) => p.is_available_async(&username).await, + None => false, + }; + state.borrow_mut().fingerprint_probe = probe; + + // Discard result if user switched while we were probing + let s = state.borrow(); + if s.user_switch_generation != generation { + return; + } + drop(s); + state.borrow_mut().fingerprint_available = available; fp_label.set_visible(available); if available { @@ -851,10 +901,10 @@ fn cancel_pending_session(state: &Rc>) { let s = state.borrow(); s.login_cancelled .store(true, std::sync::atomic::Ordering::SeqCst); - if let Ok(mut sock_guard) = s.greetd_sock.lock() { - if let Some(sock) = sock_guard.take() { - let _ = sock.shutdown(std::net::Shutdown::Both); - } + if let Ok(mut sock_guard) = s.greetd_sock.lock() + && let Some(sock) = sock_guard.take() + { + let _ = sock.shutdown(std::net::Shutdown::Both); } } @@ -902,28 +952,6 @@ fn attempt_login( return; } - match std::fs::metadata(&sock_pathbuf) { - Ok(meta) => { - use std::os::unix::fs::FileTypeExt; - if !meta.file_type().is_socket() { - show_error( - error_label, - password_entry, - strings.greetd_sock_not_socket, - ); - return; - } - } - Err(_) => { - show_error( - error_label, - password_entry, - strings.greetd_sock_unreachable, - ); - return; - } - } - // Reset cancellation flag and disable UI { let s = state.borrow(); @@ -1030,6 +1058,7 @@ enum LoginResult { } /// Run greetd IPC in a background thread. +#[allow(clippy::too_many_arguments)] fn login_worker( username: &str, password: &str, @@ -1190,7 +1219,7 @@ fn execute_power_action( #[weak] button, async move { - let result = gio::spawn_blocking(move || action_fn()).await; + let result = gio::spawn_blocking(action_fn).await; match result { Ok(Ok(())) => {} @@ -1213,6 +1242,15 @@ fn execute_power_action( // -- Last user/session persistence -- +/// Create a cache directory with restricted permissions (0o700). +fn create_cache_dir(path: &Path) -> std::io::Result<()> { + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(path) +} + fn load_last_user() -> Option { load_last_user_from(Path::new(LAST_USER_PATH)) } @@ -1236,7 +1274,7 @@ fn save_last_user(username: &str) { fn save_last_user_to(path: &Path, username: &str) { log::debug!("Saving last user: {username}"); if let Some(parent) = path.parent() - && let Err(e) = std::fs::create_dir_all(parent) + && let Err(e) = create_cache_dir(parent) { log::warn!("Failed to create cache dir {}: {e}", parent.display()); return; @@ -1289,7 +1327,10 @@ fn save_last_session(username: &str, session_name: &str) { return; } let dir = Path::new(LAST_SESSION_DIR); - let _ = std::fs::create_dir_all(dir); + if let Err(e) = create_cache_dir(dir) { + log::warn!("Failed to create session cache dir {}: {e}", dir.display()); + return; + } save_last_session_to(&dir.join(username), session_name); } @@ -1884,4 +1925,55 @@ mod tests { let resp = serde_json::json!({"type": "error"}); assert_eq!(extract_greetd_description(&resp, "fallback"), "fallback"); } + + // -- GTK theme validation -- + + #[test] + fn valid_gtk_themes() { + assert!(is_valid_gtk_theme("Adwaita")); + assert!(is_valid_gtk_theme("Catppuccin-Mocha")); + assert!(is_valid_gtk_theme("Arc_Dark")); + assert!(is_valid_gtk_theme("Theme+Variant")); + assert!(is_valid_gtk_theme("v1.0")); + } + + #[test] + fn invalid_gtk_themes() { + assert!(!is_valid_gtk_theme("")); + assert!(!is_valid_gtk_theme("../evil")); + assert!(!is_valid_gtk_theme("theme/path")); + assert!(!is_valid_gtk_theme("theme name")); + assert!(!is_valid_gtk_theme("thème")); + assert!(!is_valid_gtk_theme("theme\0null")); + } + + // -- Username validation: Unicode edge cases -- + + #[test] + fn invalid_unicode_usernames() { + assert!(!is_valid_username("üser")); + assert!(!is_valid_username("用户")); + assert!(!is_valid_username("user🔑")); + } + + // -- Cache directory permissions -- + + #[test] + fn create_cache_dir_sets_mode_0o700() { + let tmp = tempfile::tempdir().unwrap(); + let cache_dir = tmp.path().join("cache"); + create_cache_dir(&cache_dir).unwrap(); + + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(&cache_dir).unwrap().permissions().mode() & 0o777; + assert_eq!(mode, 0o700, "Cache dir should be 0o700, got {mode:#o}"); + } + + #[test] + fn save_last_session_with_unwritable_dir() { + // Attempt to save in a non-existent dir under /proc (guaranteed unwritable) + let path = Path::new("/proc/nonexistent-moongreet-test/session"); + save_last_session_to(path, "niri"); + // Should not panic — just logs a warning + } } diff --git a/src/i18n.rs b/src/i18n.rs index 8a9c276..b5f7929 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -19,8 +19,6 @@ pub struct Strings { pub no_session_selected: &'static str, pub greetd_sock_not_set: &'static str, pub greetd_sock_not_absolute: &'static str, - pub greetd_sock_not_socket: &'static str, - pub greetd_sock_unreachable: &'static str, pub auth_failed: &'static str, pub wrong_password: &'static str, pub fingerprint_prompt: &'static str, @@ -43,8 +41,6 @@ const STRINGS_DE: Strings = Strings { no_session_selected: "Keine Session ausgewählt", greetd_sock_not_set: "GREETD_SOCK nicht gesetzt", greetd_sock_not_absolute: "GREETD_SOCK ist kein absoluter Pfad", - greetd_sock_not_socket: "GREETD_SOCK zeigt nicht auf einen Socket", - greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar", auth_failed: "Authentifizierung fehlgeschlagen", wrong_password: "Falsches Passwort", fingerprint_prompt: "Fingerabdruck auflegen oder Passwort eingeben", @@ -65,8 +61,6 @@ const STRINGS_EN: Strings = Strings { no_session_selected: "No session selected", greetd_sock_not_set: "GREETD_SOCK not set", greetd_sock_not_absolute: "GREETD_SOCK is not an absolute path", - greetd_sock_not_socket: "GREETD_SOCK does not point to a socket", - greetd_sock_unreachable: "GREETD_SOCK unreachable", auth_failed: "Authentication failed", wrong_password: "Wrong password", fingerprint_prompt: "Place finger on reader or enter password", diff --git a/src/main.rs b/src/main.rs index 04b4ea5..ba1d6cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,11 +58,13 @@ fn activate(app: >k::Application) { greeter::load_background_texture(&path) }); + let blur_cache = std::rc::Rc::new(std::cell::RefCell::new(None)); + let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err(); log::debug!("Layer shell: {use_layer_shell}"); // Main greeter window (login UI) — compositor picks focused monitor - let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, app); + let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app); if use_layer_shell { setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top); } @@ -79,7 +81,7 @@ fn activate(app: >k::Application) { .item(i) .and_then(|obj| obj.downcast::().ok()) { - let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, app); + let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, &blur_cache, app); setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom); wallpaper.set_monitor(Some(&monitor)); wallpaper.present(); diff --git a/src/sessions.rs b/src/sessions.rs index e0f6844..96d3a6e 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -36,14 +36,14 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option { continue; } - if let Some(value) = line.strip_prefix("Name=") { - if name.is_none() { - name = Some(value.to_string()); - } - } else if let Some(value) = line.strip_prefix("Exec=") { - if exec_cmd.is_none() { - exec_cmd = Some(value.to_string()); - } + if let Some(value) = line.strip_prefix("Name=") + && name.is_none() + { + name = Some(value.to_string()); + } else if let Some(value) = line.strip_prefix("Exec=") + && exec_cmd.is_none() + { + exec_cmd = Some(value.to_string()); } } diff --git a/src/users.rs b/src/users.rs index 0cb4f65..7dd5a29 100644 --- a/src/users.rs +++ b/src/users.rs @@ -70,7 +70,7 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec { Err(_) => continue, }; - if uid < MIN_UID || uid > MAX_UID { + if !(MIN_UID..=MAX_UID).contains(&uid) { continue; } if NOLOGIN_SHELLS.contains(&shell) {