Rewrite moonlock from Python to Rust (v0.4.0)
Complete rewrite of the Wayland lockscreen from Python/PyGObject to Rust/gtk4-rs for memory safety in security-critical PAM code and consistency with the moonset/moongreet Rust ecosystem. Modules: main, lockscreen, auth (PAM FFI), fingerprint (fprintd D-Bus), config, i18n, users, power. 37 unit tests. Security: PAM conversation callback with Zeroizing password, panic hook that never unlocks, root check, ext-session-lock-v1 compositor policy, absolute loginctl path, avatar symlink rejection.
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
// ABOUTME: Locale detection and string lookup for the lockscreen UI.
|
||||
// ABOUTME: Reads system locale (LANG or /etc/locale.conf) and provides DE or EN strings.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Strings {
|
||||
pub password_placeholder: &'static str,
|
||||
pub unlock_button: &'static str,
|
||||
pub reboot_tooltip: &'static str,
|
||||
pub shutdown_tooltip: &'static str,
|
||||
pub fingerprint_prompt: &'static str,
|
||||
pub fingerprint_success: &'static str,
|
||||
pub fingerprint_failed: &'static str,
|
||||
pub auth_failed: &'static str,
|
||||
pub wrong_password: &'static str,
|
||||
pub reboot_failed: &'static str,
|
||||
pub shutdown_failed: &'static str,
|
||||
pub reboot_confirm: &'static str,
|
||||
pub shutdown_confirm: &'static str,
|
||||
pub confirm_yes: &'static str,
|
||||
pub confirm_no: &'static str,
|
||||
pub faillock_attempts_remaining: &'static str,
|
||||
pub faillock_locked: &'static str,
|
||||
}
|
||||
|
||||
const STRINGS_DE: Strings = Strings {
|
||||
password_placeholder: "Passwort",
|
||||
unlock_button: "Entsperren",
|
||||
reboot_tooltip: "Neustart",
|
||||
shutdown_tooltip: "Herunterfahren",
|
||||
fingerprint_prompt: "Fingerabdruck auflegen zum Entsperren",
|
||||
fingerprint_success: "Fingerabdruck erkannt",
|
||||
fingerprint_failed: "Fingerabdruck nicht erkannt",
|
||||
auth_failed: "Authentifizierung fehlgeschlagen",
|
||||
wrong_password: "Falsches Passwort",
|
||||
reboot_failed: "Neustart fehlgeschlagen",
|
||||
shutdown_failed: "Herunterfahren fehlgeschlagen",
|
||||
reboot_confirm: "Wirklich neu starten?",
|
||||
shutdown_confirm: "Wirklich herunterfahren?",
|
||||
confirm_yes: "Ja",
|
||||
confirm_no: "Abbrechen",
|
||||
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
|
||||
faillock_locked: "Konto ist möglicherweise gesperrt",
|
||||
};
|
||||
|
||||
const STRINGS_EN: Strings = Strings {
|
||||
password_placeholder: "Password",
|
||||
unlock_button: "Unlock",
|
||||
reboot_tooltip: "Reboot",
|
||||
shutdown_tooltip: "Shut down",
|
||||
fingerprint_prompt: "Place finger on reader to unlock",
|
||||
fingerprint_success: "Fingerprint recognized",
|
||||
fingerprint_failed: "Fingerprint not recognized",
|
||||
auth_failed: "Authentication failed",
|
||||
wrong_password: "Wrong password",
|
||||
reboot_failed: "Reboot failed",
|
||||
shutdown_failed: "Shutdown failed",
|
||||
reboot_confirm: "Really reboot?",
|
||||
shutdown_confirm: "Really shut down?",
|
||||
confirm_yes: "Yes",
|
||||
confirm_no: "Cancel",
|
||||
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
|
||||
faillock_locked: "Account may be locked",
|
||||
};
|
||||
|
||||
fn parse_lang_prefix(lang: &str) -> String {
|
||||
if lang.is_empty() || lang == "C" || lang == "POSIX" { return "en".to_string(); }
|
||||
let prefix = lang.split('_').next().unwrap_or(lang).split('.').next().unwrap_or(lang).to_lowercase();
|
||||
if prefix.chars().all(|c| c.is_ascii_alphabetic()) && !prefix.is_empty() { prefix } else { "en".to_string() }
|
||||
}
|
||||
|
||||
fn read_lang_from_conf(path: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
for line in content.lines() {
|
||||
if let Some(value) = line.strip_prefix("LANG=") {
|
||||
let value = value.trim();
|
||||
if !value.is_empty() { return Some(value.to_string()); }
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn detect_locale() -> String {
|
||||
let lang = env::var("LANG").ok().filter(|s| !s.is_empty())
|
||||
.or_else(|| read_lang_from_conf(Path::new(DEFAULT_LOCALE_CONF)));
|
||||
match lang { Some(l) => parse_lang_prefix(&l), None => "en".to_string() }
|
||||
}
|
||||
|
||||
pub fn load_strings(locale: Option<&str>) -> &'static Strings {
|
||||
let locale = match locale { Some(l) => l.to_string(), None => detect_locale() };
|
||||
match locale.as_str() { "de" => &STRINGS_DE, _ => &STRINGS_EN }
|
||||
}
|
||||
|
||||
pub fn faillock_warning(attempt_count: u32, strings: &Strings) -> Option<String> {
|
||||
const MAX: u32 = 3;
|
||||
if attempt_count >= MAX { return Some(strings.faillock_locked.to_string()); }
|
||||
let remaining = MAX - attempt_count;
|
||||
if remaining == 1 { return Some(strings.faillock_attempts_remaining.replace("{n}", &remaining.to_string())); }
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test] fn parse_german() { assert_eq!(parse_lang_prefix("de_DE.UTF-8"), "de"); }
|
||||
#[test] fn parse_english() { assert_eq!(parse_lang_prefix("en_US.UTF-8"), "en"); }
|
||||
#[test] fn parse_c() { assert_eq!(parse_lang_prefix("C"), "en"); }
|
||||
#[test] fn parse_empty() { assert_eq!(parse_lang_prefix(""), "en"); }
|
||||
|
||||
#[test] fn read_conf() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let conf = dir.path().join("locale.conf");
|
||||
let mut f = fs::File::create(&conf).unwrap();
|
||||
writeln!(f, "LANG=de_DE.UTF-8").unwrap();
|
||||
assert_eq!(read_lang_from_conf(&conf), Some("de_DE.UTF-8".to_string()));
|
||||
}
|
||||
|
||||
#[test] fn strings_de() { let s = load_strings(Some("de")); assert_eq!(s.password_placeholder, "Passwort"); }
|
||||
#[test] fn strings_en() { let s = load_strings(Some("en")); assert_eq!(s.password_placeholder, "Password"); }
|
||||
#[test] fn strings_fallback() { let s = load_strings(Some("fr")); assert_eq!(s.password_placeholder, "Password"); }
|
||||
|
||||
#[test] fn fingerprint_strings() {
|
||||
let s = load_strings(Some("de"));
|
||||
assert!(!s.fingerprint_prompt.is_empty());
|
||||
assert!(!s.fingerprint_success.is_empty());
|
||||
assert!(!s.fingerprint_failed.is_empty());
|
||||
}
|
||||
|
||||
#[test] fn faillock_zero() { assert!(faillock_warning(0, load_strings(Some("en"))).is_none()); }
|
||||
#[test] fn faillock_one() { assert!(faillock_warning(1, load_strings(Some("en"))).is_none()); }
|
||||
#[test] fn faillock_two() { assert!(faillock_warning(2, load_strings(Some("en"))).is_some()); }
|
||||
#[test] fn faillock_three() { assert_eq!(faillock_warning(3, load_strings(Some("en"))).unwrap(), "Account may be locked"); }
|
||||
}
|
||||
Reference in New Issue
Block a user