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:
+228
@@ -0,0 +1,228 @@
|
||||
// ABOUTME: Session detection — discovers available Wayland and X11 sessions.
|
||||
// ABOUTME: Parses .desktop files from standard session directories.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DEFAULT_WAYLAND_DIRS: &[&str] = &["/usr/share/wayland-sessions"];
|
||||
const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
|
||||
|
||||
/// Represents an available login session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Session {
|
||||
pub name: String,
|
||||
pub exec_cmd: String,
|
||||
pub session_type: String,
|
||||
}
|
||||
|
||||
/// Parse a .desktop file and return a Session, or None if invalid.
|
||||
fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
|
||||
let mut in_section = false;
|
||||
let mut name: Option<String> = None;
|
||||
let mut exec_cmd: Option<String> = None;
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
|
||||
if line.starts_with('[') {
|
||||
in_section = line == "[Desktop Entry]";
|
||||
continue;
|
||||
}
|
||||
|
||||
if !in_section {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let name = name.filter(|s| !s.is_empty())?;
|
||||
let exec_cmd = exec_cmd.filter(|s| !s.is_empty())?;
|
||||
|
||||
Some(Session {
|
||||
name,
|
||||
exec_cmd,
|
||||
session_type: session_type.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Discover available sessions from .desktop files.
|
||||
pub fn get_sessions(
|
||||
wayland_dirs: Option<&[PathBuf]>,
|
||||
xsession_dirs: Option<&[PathBuf]>,
|
||||
) -> Vec<Session> {
|
||||
let default_wayland: Vec<PathBuf> =
|
||||
DEFAULT_WAYLAND_DIRS.iter().map(PathBuf::from).collect();
|
||||
let default_xsession: Vec<PathBuf> =
|
||||
DEFAULT_XSESSION_DIRS.iter().map(PathBuf::from).collect();
|
||||
|
||||
let wayland = wayland_dirs.unwrap_or(&default_wayland);
|
||||
let xsession = xsession_dirs.unwrap_or(&default_xsession);
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
for (dirs, session_type) in [(wayland, "wayland"), (xsession, "x11")] {
|
||||
for directory in dirs {
|
||||
let entries = match fs::read_dir(directory) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let mut paths: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|ext| ext == "desktop"))
|
||||
.collect();
|
||||
paths.sort();
|
||||
|
||||
for path in paths {
|
||||
if let Some(session) = parse_desktop_file(&path, session_type) {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn write_desktop(dir: &Path, name: &str, content: &str) {
|
||||
fs::write(dir.join(name), content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_valid_desktop_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test.desktop");
|
||||
fs::write(
|
||||
&file,
|
||||
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
||||
)
|
||||
.unwrap();
|
||||
let session = parse_desktop_file(&file, "wayland").unwrap();
|
||||
assert_eq!(session.name, "Niri");
|
||||
assert_eq!(session.exec_cmd, "niri-session");
|
||||
assert_eq!(session.session_type, "wayland");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_desktop_file_missing_name() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test.desktop");
|
||||
fs::write(&file, "[Desktop Entry]\nExec=niri-session\n").unwrap();
|
||||
assert!(parse_desktop_file(&file, "wayland").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_desktop_file_missing_exec() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test.desktop");
|
||||
fs::write(&file, "[Desktop Entry]\nName=Niri\n").unwrap();
|
||||
assert!(parse_desktop_file(&file, "wayland").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_desktop_file_wrong_section() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("test.desktop");
|
||||
fs::write(
|
||||
&file,
|
||||
"[Other Section]\nName=Niri\nExec=niri-session\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(parse_desktop_file(&file, "wayland").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_sessions_finds_wayland_and_x11() {
|
||||
let wayland_dir = tempfile::tempdir().unwrap();
|
||||
let x11_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
write_desktop(
|
||||
wayland_dir.path(),
|
||||
"niri.desktop",
|
||||
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
||||
);
|
||||
write_desktop(
|
||||
x11_dir.path(),
|
||||
"i3.desktop",
|
||||
"[Desktop Entry]\nName=i3\nExec=i3\n",
|
||||
);
|
||||
|
||||
let wayland_paths = vec![wayland_dir.path().to_path_buf()];
|
||||
let x11_paths = vec![x11_dir.path().to_path_buf()];
|
||||
let sessions = get_sessions(Some(&wayland_paths), Some(&x11_paths));
|
||||
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert_eq!(sessions[0].name, "Niri");
|
||||
assert_eq!(sessions[0].session_type, "wayland");
|
||||
assert_eq!(sessions[1].name, "i3");
|
||||
assert_eq!(sessions[1].session_type, "x11");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_sessions_skips_missing_dirs() {
|
||||
let sessions = get_sessions(
|
||||
Some(&[PathBuf::from("/nonexistent")]),
|
||||
Some(&[PathBuf::from("/also-nonexistent")]),
|
||||
);
|
||||
assert!(sessions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_sessions_skips_invalid_files() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_desktop(
|
||||
dir.path(),
|
||||
"valid.desktop",
|
||||
"[Desktop Entry]\nName=Valid\nExec=valid\n",
|
||||
);
|
||||
write_desktop(
|
||||
dir.path(),
|
||||
"invalid.desktop",
|
||||
"[Desktop Entry]\nName=Invalid\n",
|
||||
);
|
||||
// Non-.desktop file
|
||||
fs::write(dir.path().join("readme.txt"), "not a session").unwrap();
|
||||
|
||||
let paths = vec![dir.path().to_path_buf()];
|
||||
let sessions = get_sessions(Some(&paths), Some(&[]));
|
||||
assert_eq!(sessions.len(), 1);
|
||||
assert_eq!(sessions[0].name, "Valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sessions_sorted_alphabetically() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_desktop(
|
||||
dir.path(),
|
||||
"z-sway.desktop",
|
||||
"[Desktop Entry]\nName=Sway\nExec=sway\n",
|
||||
);
|
||||
write_desktop(
|
||||
dir.path(),
|
||||
"a-niri.desktop",
|
||||
"[Desktop Entry]\nName=Niri\nExec=niri-session\n",
|
||||
);
|
||||
|
||||
let paths = vec![dir.path().to_path_buf()];
|
||||
let sessions = get_sessions(Some(&paths), Some(&[]));
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert_eq!(sessions[0].name, "Niri");
|
||||
assert_eq!(sessions[1].name, "Sway");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user