#[cfg(unix)]
use crate::sessions::{apply_repair_plan, discover_repair_plan};
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::io::{self, BufRead, IsTerminal, Read, Write};
use std::path::Path;
#[cfg(unix)]
#[derive(Clone, Copy)]
struct TerminalState {
stdin_is_tty: bool,
stderr_is_tty: bool,
}
pub(crate) fn run(root: &Path, yes: bool, dry_run: bool) -> anyhow::Result<()> {
#[cfg(not(unix))]
{
let _ = (root, yes, dry_run);
anyhow::bail!(
"session permission repair is unsupported on this platform; no permissions changed"
)
}
#[cfg(unix)]
{
let mut stdout = io::stdout().lock();
let mut stderr = io::stderr().lock();
run_with_io(
root,
yes,
dry_run,
&mut io::stdin().lock(),
&mut stdout,
&mut stderr,
TerminalState {
stdin_is_tty: io::stdin().is_terminal(),
stderr_is_tty: io::stderr().is_terminal(),
},
)
}
}
#[cfg(unix)]
fn run_with_io<R: BufRead, W: Write, E: Write>(
root: &Path,
yes: bool,
dry_run: bool,
input: &mut R,
stdout: &mut W,
stderr: &mut E,
terminal: TerminalState,
) -> anyhow::Result<()> {
use std::os::unix::fs::MetadataExt;
let metadata = match fs::symlink_metadata(root) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
writeln!(stdout, "No session root exists; nothing to repair.")?;
return Ok(());
}
Err(error) => return Err(error.into()),
};
if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
anyhow::bail!(
"session root is not a non-symlink directory: {}",
root.display()
);
}
if metadata.uid() != unsafe { libc::geteuid() } {
anyhow::bail!("session root owner is not current user: {}", root.display());
}
let plan = discover_repair_plan(root).map_err(|_| {
anyhow::anyhow!(
"session layout is unsafe, unreadable, or unrecognized; inspect {} manually",
root.display()
)
})?;
if plan.permissions_are_secure()? {
writeln!(
stdout,
"Session permissions already secure; nothing to repair."
)?;
return Ok(());
}
let count = plan.target_count();
if dry_run {
writeln!(
stdout,
"Would repair permissions for {count} recognized session objects under {}.",
root.display()
)?;
return Ok(());
}
if !yes && (!terminal.stdin_is_tty || !terminal.stderr_is_tty) {
anyhow::bail!("interactive confirmation requires TTYs; use --yes or --dry-run")
}
if !yes {
writeln!(
stdout,
"Found {count} recognized session objects under {}.",
root.display()
)?;
write!(stderr, "Repair these permissions? [y/N] ")?;
stderr.flush()?;
let mut answer = String::new();
input.take(4096).read_line(&mut answer)?;
if !matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
writeln!(stdout, "Cancelled; no permissions changed.")?;
return Ok(());
}
}
match apply_repair_plan(&plan) {
Ok(changed) => {
writeln!(
stdout,
"Repaired permissions for {changed} recognized session objects."
)?;
Ok(())
}
Err(error) => Err(error),
}
}