Rewrite moongreet from Python to Rust (v0.3.0)

Complete rewrite of the greetd greeter from Python/PyGObject to Rust/gtk4-rs
for consistency with moonset, single binary without Python runtime, and
improved security through Rust memory safety.

Modules: main, greeter, ipc, config, i18n, users, sessions, power
86 unit tests covering all modules including login_worker IPC flow.
Security hardening: shell-word splitting for exec_cmd, absolute path
validation for session binaries, session-name sanitization, absolute
loginctl path, atomic IPC writes.
This commit is contained in:
2026-03-27 22:08:33 +01:00
parent de0b1d40ba
commit 226bbb75e4
39 changed files with 4395 additions and 2768 deletions
+112
View File
@@ -0,0 +1,112 @@
// ABOUTME: Power actions — reboot and shutdown via loginctl.
// ABOUTME: Wrappers around system commands for the greeter UI.
use std::fmt;
use std::process::Command;
#[derive(Debug)]
pub enum PowerError {
CommandFailed { action: &'static str, message: String },
Timeout { action: &'static str },
}
impl fmt::Display for PowerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PowerError::CommandFailed { action, message } => {
write!(f, "{action} failed: {message}")
}
PowerError::Timeout { action } => {
write!(f, "{action} timed out")
}
}
}
}
impl std::error::Error for PowerError {}
/// Run a command and return a PowerError on failure.
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
let child = Command::new(program)
.args(args)
.spawn()
.map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
let output = child
.wait_with_output()
.map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", output.status, stderr.trim()),
});
}
Ok(())
}
/// Reboot the system via loginctl.
pub fn reboot() -> Result<(), PowerError> {
run_command("reboot", "/usr/bin/loginctl", &["reboot"])
}
/// Shut down the system via loginctl.
pub fn shutdown() -> Result<(), PowerError> {
run_command("shutdown", "/usr/bin/loginctl", &["poweroff"])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn power_error_command_failed_display() {
let err = PowerError::CommandFailed {
action: "reboot",
message: "No such file or directory".to_string(),
};
assert_eq!(err.to_string(), "reboot failed: No such file or directory");
}
#[test]
fn power_error_timeout_display() {
let err = PowerError::Timeout { action: "shutdown" };
assert_eq!(err.to_string(), "shutdown timed out");
}
#[test]
fn run_command_returns_error_for_missing_binary() {
let result = run_command("test", "nonexistent-binary-xyz", &[]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PowerError::CommandFailed { action: "test", .. }));
}
#[test]
fn run_command_returns_error_on_nonzero_exit() {
let result = run_command("test", "false", &[]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PowerError::CommandFailed { action: "test", .. }));
}
#[test]
fn run_command_succeeds_for_true() {
let result = run_command("test", "true", &[]);
assert!(result.is_ok());
}
#[test]
fn run_command_passes_args() {
let result = run_command("test", "echo", &["hello", "world"]);
assert!(result.is_ok());
}
}