greetd-moongreet/src/sessions.rs
nevaforget 226bbb75e4 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.
2026-03-27 22:08:33 +01:00

229 lines
6.9 KiB
Rust

// 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");
}
}