//! El tiquet de sesión: lo que el greeter le entrega al compositor tras //! una autenticación exitosa. //! //! El greeter de carmen corre como proceso hijo del compositor. Cuando //! el login tiene éxito, imprime **una línea** de tiquet a su stdout; el //! compositor escanea las líneas del hijo buscando el prefijo //! [`TICKET_TAG`] y, al encontrarlo, hace el traspaso a modo sesión. use std::path::PathBuf; use crate::UserInfo; /// Etiqueta + versión de la línea de tiquet. El compositor sólo trata /// como tiquet las líneas que empiezan con esto — el resto del stdout /// del greeter (logs, ruido) se ignora. pub const TICKET_TAG: &str = "MIRADA-SESSION-TICKET-v1"; /// Resultado de un login: la identidad autenticada más, opcionalmente, /// el comando de sesión elegido. El greeter lo produce; el compositor lo /// consume para arrancar la sesión (setuid al usuario + spawn). #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionTicket { /// Identidad del usuario autenticado. pub user: UserInfo, /// Comando de sesión a ejecutar como el usuario. Vacío = que el /// compositor decida (su autostart por defecto). pub session: String, } impl SessionTicket { /// Crea un tiquet sin comando de sesión explícito. pub fn new(user: UserInfo) -> Self { Self { user, session: String::new(), } } /// Fija el comando de sesión. Encadenable. pub fn with_session(mut self, session: impl Into) -> Self { self.session = session.into(); self } /// Serializa el tiquet a una línea única, apta para stdout. Campos /// separados por tabulador: ni los nombres de usuario, ni los paths, /// ni los comandos de sesión suelen contener tabuladores. pub fn to_line(&self) -> String { format!( "{TICKET_TAG}\t{}\t{}\t{}\t{}\t{}\t{}", self.user.name, self.user.uid, self.user.gid, self.user.home.display(), self.user.shell.display(), self.session, ) } /// Parsea una línea producida por [`to_line`]. `None` si la línea no /// es un tiquet (otra salida del greeter) o está malformada. pub fn from_line(line: &str) -> Option { let mut f = line.trim_end_matches(['\r', '\n']).split('\t'); if f.next()? != TICKET_TAG { return None; } let name = f.next()?.to_string(); let uid = f.next()?.parse().ok()?; let gid = f.next()?.parse().ok()?; let home = PathBuf::from(f.next()?); let shell = PathBuf::from(f.next()?); // El comando de sesión puede venir vacío. let session = f.next().unwrap_or("").to_string(); Some(SessionTicket { user: UserInfo { name, uid, gid, home, shell, }, session, }) } } #[cfg(test)] mod tests { use super::*; fn sample() -> UserInfo { UserInfo { name: "sergio".into(), uid: 1000, gid: 1000, home: PathBuf::from("/home/sergio"), shell: PathBuf::from("/usr/bin/bash"), } } #[test] fn round_trip_without_session() { let t = SessionTicket::new(sample()); let back = SessionTicket::from_line(&t.to_line()).expect("parsea"); assert_eq!(back, t); assert!(back.session.is_empty()); } #[test] fn round_trip_with_session() { let t = SessionTicket::new(sample()).with_session("shuma-shell --launcher"); let back = SessionTicket::from_line(&t.to_line()).expect("parsea"); assert_eq!(back, t); assert_eq!(back.session, "shuma-shell --launcher"); } #[test] fn from_line_ignores_non_ticket() { assert!(SessionTicket::from_line("[INFO] arrancando greeter").is_none()); assert!(SessionTicket::from_line("").is_none()); } #[test] fn from_line_rejects_malformed() { // Prefijo correcto pero faltan campos. assert!(SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio")).is_none()); // uid no numérico. assert!( SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio\tXX\t1000\t/h\t/sh\t")) .is_none() ); } #[test] fn tolerates_trailing_newline() { let line = format!("{}\n", SessionTicket::new(sample()).to_line()); assert!(SessionTicket::from_line(&line).is_some()); } }