Skip to main content

cleansys_core/
utils.rs

1use anyhow::{Context, Result};
2use colored::*;
3use std::io::{self, Write};
4use std::process::Command;
5#[cfg(unix)]
6use users::get_effective_uid;
7
8/// Check if the program is running with root privileges (Unix) or an
9/// elevated/Administrator token (Windows).
10#[cfg(unix)]
11pub fn check_root() -> bool {
12    get_effective_uid() == 0
13}
14
15/// Check if the current process token is elevated (running "as Administrator").
16#[cfg(windows)]
17pub fn check_root() -> bool {
18    is_elevated::is_elevated()
19}
20
21#[cfg(not(any(unix, windows)))]
22pub fn check_root() -> bool {
23    false
24}
25
26/// Whether this platform's elevation model matches the interactive
27/// sudo-password-prompt flow (Unix: `sudo -S`). Windows uses UAC/Administrator
28/// tokens instead, which front-ends should present very differently (there is
29/// no password to type — the user must relaunch the process elevated).
30pub const fn supports_sudo_prompt() -> bool {
31    cfg!(unix)
32}
33
34/// Prompt for sudo elevation if not already root
35/// Returns true if elevation succeeded or already root, false otherwise
36#[cfg(unix)]
37pub fn elevate_if_needed() -> Result<bool> {
38    if check_root() {
39        return Ok(true);
40    }
41
42    print_warning("System cleaners require root privileges.");
43    println!("You can either:");
44    println!("  1. Run this command again with sudo");
45    println!("  2. Enter your password to elevate now");
46    print!("\nWould you like to elevate now? [Y/n]: ");
47    io::stdout().flush()?;
48
49    let mut response = String::new();
50    io::stdin().read_line(&mut response)?;
51
52    match response.trim().to_lowercase().as_str() {
53        "n" | "no" => {
54            print_warning("Skipping system cleaners. Only user cleaners will run.");
55            Ok(false)
56        }
57        _ => {
58            // Try to validate sudo access by running a simple command
59            print!("Authenticating... ");
60            io::stdout().flush()?;
61
62            let status = Command::new("sudo")
63                .args(["-v"])
64                .status()
65                .context("Failed to execute sudo")?;
66
67            if status.success() {
68                println!("{}", "✓ Authentication successful".green());
69                Ok(true)
70            } else {
71                print_error("Authentication failed. Skipping system cleaners.");
72                Ok(false)
73            }
74        }
75    }
76}
77
78#[cfg(windows)]
79pub fn elevate_if_needed() -> Result<bool> {
80    if check_root() {
81        return Ok(true);
82    }
83    print_warning(
84        "Some system cleaners require Administrator privileges. Restart CleanSys as Administrator (right-click → 'Run as administrator') to use them.",
85    );
86    Ok(false)
87}
88
89#[cfg(not(any(unix, windows)))]
90pub fn elevate_if_needed() -> Result<bool> {
91    print_warning("System cleaners are only available on Unix-like systems and Windows.");
92    Ok(false)
93}
94
95/// Execute a command with sudo if not already root.
96///
97/// If a password was previously cached via [`crate::auth::cache_sudo_password`]
98/// (set by [`crate::auth::authenticate_sudo`] on success), it is piped
99/// directly to `sudo -S` for this specific command — the same reliable
100/// mechanism used to validate it in the first place. This avoids depending on
101/// `sudo`'s own credential cache, which is normally keyed per-TTY/session and
102/// is not guaranteed to be reusable across separate child processes spawned
103/// by a GUI front-end that has no controlling TTY at all.
104///
105/// Falls back to non-interactive `sudo -n` (relying on `sudo`'s own ticket
106/// cache) when no password has been cached — e.g. the TUI/CLI, which
107/// pre-authenticates via a real interactive `sudo -v` prompt on an actual
108/// terminal and never captures the raw password.
109#[cfg(unix)]
110pub fn execute_with_sudo(command: &str, args: &[&str]) -> Result<std::process::Output> {
111    use std::io::Write;
112    use std::process::Stdio;
113
114    if check_root() {
115        // Already root, execute directly
116        return Command::new(command)
117            .args(args)
118            .output()
119            .context(format!("Failed to execute command: {}", command));
120    }
121
122    if let Some(password) = crate::auth::cached_sudo_password() {
123        let mut child = Command::new("sudo")
124            .arg("-S")
125            .arg(command)
126            .args(args)
127            .stdin(Stdio::piped())
128            .stdout(Stdio::piped())
129            .stderr(Stdio::piped())
130            .spawn()
131            .context(format!("Failed to execute command with sudo: {}", command))?;
132
133        if let Some(mut stdin) = child.stdin.take() {
134            let _ = writeln!(stdin, "{}", password);
135        }
136
137        return child
138            .wait_with_output()
139            .context(format!("Failed to execute command with sudo: {}", command));
140    }
141
142    // No cached password (TUI/CLI path) — rely on sudo's own ticket cache.
143    // The -n flag prevents sudo from prompting for a password.
144    let mut sudo_args = vec!["-n", command];
145    sudo_args.extend_from_slice(args);
146
147    Command::new("sudo")
148        .args(sudo_args)
149        .stdin(Stdio::null())
150        .output()
151        .context(format!("Failed to execute command with sudo: {}", command))
152}
153
154#[cfg(not(unix))]
155pub fn execute_with_sudo(command: &str, args: &[&str]) -> Result<std::process::Output> {
156    Command::new(command)
157        .args(args)
158        .output()
159        .context(format!("Failed to execute command: {}", command))
160}
161
162/// Print a header with a colorful banner
163pub fn print_header(text: &str) {
164    let width = 60;
165    let padding = (width - text.len()) / 2;
166    let line = "=".repeat(width);
167
168    println!("\n{}", line.bright_blue());
169    println!(
170        "{}{}{}",
171        " ".repeat(padding),
172        text.bright_white().bold(),
173        " ".repeat(padding)
174    );
175    println!("{}\n", line.bright_blue());
176}
177
178/// Print a success message
179pub fn print_success(message: &str) {
180    println!("{} {}", "✓".green().bold(), message);
181}
182
183/// Print a warning message
184pub fn print_warning(message: &str) {
185    println!("{} {}", "!".yellow().bold(), message);
186}
187
188/// Print an error message
189pub fn print_error(message: &str) {
190    eprintln!("{} {}", "✗".red().bold(), message);
191}
192
193/// Ask for user confirmation
194pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
195    let yes_no = if default { "[Y/n]" } else { "[y/N]" };
196    loop {
197        print!("{} {} ", prompt, yes_no);
198        io::stdout().flush()?;
199
200        let mut response = String::new();
201        io::stdin().read_line(&mut response)?;
202
203        match response.trim().to_lowercase().as_str() {
204            "y" | "yes" => return Ok(true),
205            "n" | "no" => return Ok(false),
206            "" => return Ok(default),
207            _ => {
208                print_warning("Invalid response. Please enter 'y' or 'n'.");
209                // Loop and re-prompt instead of recursing — an automated
210                // or malicious pipe feeding endless invalid lines must not
211                // be able to grow the call stack unboundedly.
212            }
213        }
214    }
215}
216
217/// Format bytes into human-readable sizes
218pub fn format_size(bytes: u64) -> String {
219    const KB: u64 = 1024;
220    const MB: u64 = KB * 1024;
221    const GB: u64 = MB * 1024;
222
223    if bytes >= GB {
224        format!("{:.2} GB", bytes as f64 / GB as f64)
225    } else if bytes >= MB {
226        format!("{:.2} MB", bytes as f64 / MB as f64)
227    } else if bytes >= KB {
228        format!("{:.2} KB", bytes as f64 / KB as f64)
229    } else {
230        format!("{} bytes", bytes)
231    }
232}
233
234/// Get the size of a directory or file in bytes.
235///
236/// Implemented as a pure-Rust recursive walk (no shell-out to `du`), so it
237/// works identically on Linux, macOS, and Windows — the previous `du -sb`
238/// implementation relied on a GNU-only flag and silently reported `0` on
239/// BSD/macOS `du`. Symlinks are never followed and are always counted as
240/// `0` bytes (matching `du`'s default, non-`-L` behaviour), avoiding both
241/// infinite loops on cyclic symlinks and double-counting a target that a
242/// regular file/directory entry elsewhere in the same tree may already
243/// account for.
244pub fn get_size(path: &str) -> Result<u64> {
245    const MAX_DEPTH: u32 = 512;
246    Ok(dir_size(std::path::Path::new(path), 0, MAX_DEPTH))
247}
248
249fn dir_size(path: &std::path::Path, depth: u32, max_depth: u32) -> u64 {
250    let metadata = match std::fs::symlink_metadata(path) {
251        Ok(m) => m,
252        Err(_) => return 0,
253    };
254
255    if metadata.file_type().is_symlink() {
256        return 0;
257    }
258
259    if metadata.is_file() {
260        return metadata.len();
261    }
262
263    if metadata.is_dir() {
264        // Guard against pathological/cyclic directory structures: give up on
265        // descending further rather than risk a stack overflow. In practice
266        // no real cache/temp/trash directory this tool targets comes close
267        // to this depth.
268        if depth >= max_depth {
269            log::warn!(
270                "get_size: max recursion depth ({max_depth}) reached at {:?}; size may be underestimated",
271                path
272            );
273            return 0;
274        }
275
276        let mut total = 0u64;
277        if let Ok(entries) = std::fs::read_dir(path) {
278            for entry in entries.flatten() {
279                total = total.saturating_add(dir_size(&entry.path(), depth + 1, max_depth));
280            }
281        }
282        return total;
283    }
284
285    0
286}