use crate::commands::{now, size};
use crate::output;
use crate::session;
use crate::when;
use anyhow::{Result, bail};
use sefy_core::wipe::{Part, Role};
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq)]
enum Answer {
Declined,
Confirmed,
Mistyped,
}
fn answer(typed: &str, name: &str) -> Answer {
match typed.trim() {
"" => Answer::Declined,
typed if typed == name => Answer::Confirmed,
_ => Answer::Mistyped,
}
}
pub fn run(path: &Path, password_env: Option<&str>) -> Result<()> {
if !std::io::stdin().is_terminal() {
bail!(
"wipe asks for the vault's name on a terminal, and input is not one\n\
there is no --yes: what a wipe destroys does not come back"
);
}
let password = session::password(password_env)?;
let vault = session::open(path, &password)?;
let name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
let parts = sefy_core::wipe::parts(vault.path());
println!(
"This overwrites with random bytes and deletes, in {}:",
directory(path).display()
);
print_parts(&parts);
if let Some(stamp) = vault.last_sync()? {
println!(
"Not touched: the remote copy, last reached through {} {}.",
stamp.transport,
when::moment(stamp.at, now())
);
}
println!(
"An SSD, a snapshot, a backup or a synced folder's history can keep\n\
older blocks of these files out of a wipe's reach: see sefy wipe --help."
);
print!("Nothing brings it back. Type the vault's name to wipe it ({name}): ");
std::io::stdout().flush()?;
let mut typed = String::new();
std::io::stdin().read_line(&mut typed)?;
match answer(&typed, &name) {
Answer::Declined => {
println!("nothing was wiped");
return Ok(());
}
Answer::Mistyped => bail!("that is not {name:?}; nothing was wiped"),
Answer::Confirmed => {}
}
let wiped = vault.wipe();
for part in &wiped.destroyed {
println!("wiped {}", file_name(part));
}
for part in &wiped.vanished {
println!(
"gone {} (deleted before the wipe reached it)",
file_name(part)
);
}
if let Some(stamp) = &wiped.last_sync {
println!(
"the remote copy is not touched: delete it through {}",
stamp.transport
);
}
if wiped.complete() {
return Ok(());
}
for (_, error) in &wiped.failed {
eprintln!("not wiped: {error}");
}
bail!(
"{} still on disk; once a file can be written, wipe it on its own:\n\
sefy wipe --vault FILE",
output::count(wiped.failed.len(), "file")
)
}
fn directory(vault: &Path) -> PathBuf {
let absolute = std::path::absolute(vault).unwrap_or_else(|_| vault.to_path_buf());
match absolute.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
_ => PathBuf::from("."),
}
}
fn file_name(part: &Part) -> String {
part.path.file_name().map_or_else(
|| part.path.display().to_string(),
|name| name.to_string_lossy().into_owned(),
)
}
fn print_parts(parts: &[Part]) {
let rows: Vec<(String, String, String)> = parts
.iter()
.map(|part| {
(
file_name(part),
describe(part.role),
part.size.map_or_else(|| "size unknown".to_owned(), size),
)
})
.collect();
let path_width = rows
.iter()
.map(|row| row.0.chars().count())
.max()
.unwrap_or(0);
let role_width = rows
.iter()
.map(|row| row.1.chars().count())
.max()
.unwrap_or(0);
for (path, role, size) in rows {
println!(" {path:path_width$} {role:role_width$} {size}");
}
}
fn describe(role: Role) -> String {
match role {
Role::Vault => "the vault".to_owned(),
Role::Copy(number) => format!("copy {number}"),
Role::Unfinished => "an unfinished save".to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_name_itself_goes_ahead() {
assert_eq!(answer("notes.bak\n", "notes.bak"), Answer::Confirmed);
assert_eq!(answer(" notes.bak \r\n", "notes.bak"), Answer::Confirmed);
}
#[test]
fn a_yes_is_not_the_name() {
for typed in ["y\n", "yes\n", "Y\n", "wipe\n"] {
assert_eq!(answer(typed, "notes.bak"), Answer::Mistyped, "{typed:?}");
}
}
#[test]
fn a_near_miss_is_not_the_name() {
for typed in ["Notes.bak", "notes.ba", "notes.bak.1", "notes"] {
assert_eq!(answer(typed, "notes.bak"), Answer::Mistyped, "{typed:?}");
}
}
#[test]
fn nothing_typed_is_a_no() {
assert_eq!(answer("\n", "notes.bak"), Answer::Declined);
assert_eq!(answer("", "notes.bak"), Answer::Declined);
}
#[test]
fn a_vault_with_no_name_cannot_be_confirmed_by_typing_nothing() {
assert_eq!(answer("\n", ""), Answer::Declined);
}
}