pacsea/logic/
doas_persist_setup.rs1use ratatui::style::{Modifier, Style};
4use ratatui::text::{Line, Span};
5
6use crate::state::AppState;
7use crate::state::modal::DoasPersistChoice;
8use crate::theme::theme;
9
10pub const DOAS_PERSIST_INSTRUCTION_VIEWPORT_LINES: usize = 10;
12
13fn current_username_for_snippet() -> String {
25 let from_env = std::env::var("USER")
26 .or_else(|_| std::env::var("LOGNAME"))
27 .ok()
28 .map(|s| s.trim().to_string())
29 .filter(|s| !s.is_empty());
30 if let Some(name) = from_env {
31 return name;
32 }
33 let from_id = std::process::Command::new("id")
34 .args(["-un"])
35 .output()
36 .ok()
37 .filter(|out| out.status.success())
38 .and_then(|out| String::from_utf8(out.stdout).ok())
39 .map(|s| s.trim().to_string())
40 .filter(|s| !s.is_empty());
41 from_id.unwrap_or_else(|| "youruser".to_string())
42}
43
44#[must_use]
56pub fn doas_conf_has_persist_rule(contents: &str) -> bool {
57 contents.lines().any(|line| {
58 let trimmed = line.trim();
59 if trimmed.is_empty() || trimmed.starts_with('#') {
60 return false;
61 }
62 let without_inline_comment = trimmed
63 .split_once('#')
64 .map_or(trimmed, |(rule, _)| rule.trim());
65 let tokens: Vec<&str> = without_inline_comment.split_whitespace().collect();
66 tokens.contains(&"permit") && tokens.contains(&"persist")
67 })
68}
69
70#[must_use]
81pub fn pacsea_doas_persist_configured() -> bool {
82 std::fs::read_to_string("/etc/doas.conf")
83 .ok()
84 .is_some_and(|contents| doas_conf_has_persist_rule(&contents))
85}
86
87#[must_use]
98pub fn doas_persist_snippet(choice: DoasPersistChoice, username: &str) -> String {
99 match choice {
100 DoasPersistChoice::WheelScoped => "permit persist :wheel as root".to_string(),
101 DoasPersistChoice::UserScoped => format!("permit persist {username} as root"),
102 DoasPersistChoice::Skip => "# Skip setup".to_string(),
103 }
104}
105
106#[must_use]
117pub fn validation_commands() -> Vec<&'static str> {
118 vec!["doas -C /etc/doas.conf", "doas -n true"]
119}
120
121#[must_use]
133#[allow(clippy::vec_init_then_push)]
134pub fn doas_persist_instruction_lines(
135 app: &AppState,
136 choice: DoasPersistChoice,
137) -> Vec<Line<'static>> {
138 let th = theme();
139 let user = current_username_for_snippet();
140 let snippet = doas_persist_snippet(choice, &user);
141 let mut lines: Vec<Line<'static>> = Vec::new();
142 lines.push(Line::from(Span::styled(
143 crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_heading"),
144 Style::default().fg(th.mauve).add_modifier(Modifier::BOLD),
145 )));
146 lines.push(Line::from(""));
147 lines.push(Line::from(Span::styled(
148 crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_note"),
149 Style::default().fg(th.text),
150 )));
151 lines.push(Line::from(""));
152 lines.push(Line::from(Span::styled(
153 crate::i18n::t(app, "app.modals.doas_persist_setup.label_snippet"),
154 Style::default()
155 .fg(th.overlay1)
156 .add_modifier(Modifier::BOLD),
157 )));
158 lines.push(Line::from(Span::styled(
159 snippet,
160 Style::default().fg(th.lavender),
161 )));
162 lines.push(Line::from(""));
163 lines.push(Line::from(Span::styled(
164 crate::i18n::t(app, "app.modals.doas_persist_setup.label_checks"),
165 Style::default()
166 .fg(th.overlay1)
167 .add_modifier(Modifier::BOLD),
168 )));
169 for cmd in validation_commands() {
170 lines.push(Line::from(Span::styled(
171 format!(" {cmd}"),
172 Style::default().fg(th.subtext1),
173 )));
174 }
175 lines.push(Line::from(""));
176 lines.push(Line::from(Span::styled(
177 crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_footer"),
178 Style::default().fg(th.overlay1),
179 )));
180 lines
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn snippet_generation_uses_expected_forms() {
189 assert_eq!(
190 doas_persist_snippet(DoasPersistChoice::WheelScoped, "alice"),
191 "permit persist :wheel as root"
192 );
193 assert_eq!(
194 doas_persist_snippet(DoasPersistChoice::UserScoped, "alice"),
195 "permit persist alice as root"
196 );
197 }
198
199 #[test]
200 fn validation_commands_include_required_checks() {
201 let cmds = validation_commands();
202 assert!(cmds.iter().any(|c| c.contains("doas -C /etc/doas.conf")));
203 assert!(cmds.iter().any(|c| c.contains("doas -n true")));
204 }
205
206 #[test]
207 fn doas_conf_parser_detects_active_persist_rule() {
208 let conf = r"
209 # comment
210 permit persist :wheel as root
211 ";
212 assert!(doas_conf_has_persist_rule(conf));
213 }
214
215 #[test]
216 fn doas_conf_parser_ignores_commented_persist_rule() {
217 let conf = r"
218 # permit persist :wheel as root
219 permit :wheel as root
220 ";
221 assert!(!doas_conf_has_persist_rule(conf));
222 }
223}