tirith 0.3.0

Terminal security - catches homograph attacks, pipe-to-shell, ANSI injection
use tirith_core::checkpoint;

pub fn list_checkpoints(json: bool) -> i32 {
    match checkpoint::list() {
        Ok(entries) => {
            if json {
                let json_val = serde_json::json!({
                    "checkpoints": entries,
                });
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|e| {
                        eprintln!("tirith: checkpoint: JSON serialization failed: {e}");
                        "{}".to_string()
                    })
                );
            } else {
                if entries.is_empty() {
                    println!("No checkpoints found.");
                } else {
                    println!(
                        "{id:<38} {created:<26} {files:<8} {size:<12} Trigger",
                        id = "ID",
                        created = "Created",
                        files = "Files",
                        size = "Size"
                    );
                    println!("{}", "-".repeat(100));
                    for e in &entries {
                        let size = format_bytes(e.total_bytes);
                        let trigger = e
                            .trigger_command
                            .as_deref()
                            .unwrap_or("-")
                            .chars()
                            .take(30)
                            .collect::<String>();
                        println!(
                            "{:<38} {:<26} {:<8} {:<12} {}",
                            e.id, e.created_at, e.file_count, size, trigger
                        );
                    }
                    println!("\n{} checkpoint(s)", entries.len());
                }
            }
            0
        }
        Err(e) => {
            eprintln!("tirith checkpoint list: {e}");
            2
        }
    }
}

pub fn restore_checkpoint(id: &str, json: bool) -> i32 {
    match checkpoint::restore(id) {
        Ok(restored) => {
            if json {
                let json_val = serde_json::json!({
                    "restored": restored,
                    "count": restored.len(),
                });
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|e| {
                        eprintln!("tirith: checkpoint: JSON serialization failed: {e}");
                        "{}".to_string()
                    })
                );
            } else {
                println!("Restored {} file(s):", restored.len());
                for path in &restored {
                    println!("  {path}");
                }
            }
            0
        }
        Err(e) => {
            eprintln!("tirith checkpoint restore: {e}");
            2
        }
    }
}

pub fn diff_checkpoint(id: &str, json: bool) -> i32 {
    match checkpoint::diff(id) {
        Ok(diffs) => {
            if json {
                let json_val = serde_json::json!({
                    "diffs": diffs,
                    "count": diffs.len(),
                });
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|e| {
                        eprintln!("tirith: checkpoint: JSON serialization failed: {e}");
                        "{}".to_string()
                    })
                );
            } else if diffs.is_empty() {
                println!("No differences found — all files match checkpoint.");
            } else {
                for d in &diffs {
                    let s = tirith_core::style::Stream::Stdout;
                    let status = match d.status {
                        checkpoint::DiffStatus::Deleted => tirith_core::style::red("deleted", s),
                        checkpoint::DiffStatus::Modified => {
                            tirith_core::style::yellow("modified", s)
                        }
                        checkpoint::DiffStatus::BackupCorrupt => {
                            tirith_core::style::red("corrupt", s)
                        }
                    };
                    println!("  {status:>18}  {}", d.path);
                }
                println!("\n{} difference(s)", diffs.len());
            }
            0
        }
        Err(e) => {
            eprintln!("tirith checkpoint diff: {e}");
            2
        }
    }
}

pub fn purge_checkpoints(json: bool) -> i32 {
    let config = checkpoint::CheckpointConfig::default();
    match checkpoint::purge(&config) {
        Ok(result) => {
            if json {
                let json_val = serde_json::json!(result);
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|e| {
                        eprintln!("tirith: checkpoint: JSON serialization failed: {e}");
                        "{}".to_string()
                    })
                );
            } else if result.removed_count == 0 {
                println!("No checkpoints needed purging.");
            } else {
                println!(
                    "Purged {} checkpoint(s), freed {}",
                    result.removed_count,
                    format_bytes(result.freed_bytes)
                );
            }
            0
        }
        Err(e) => {
            eprintln!("tirith checkpoint purge: {e}");
            2
        }
    }
}

pub fn create_checkpoint(paths: &[String], trigger: Option<&str>, json: bool) -> i32 {
    let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
    match checkpoint::create(&path_refs, trigger) {
        Ok(meta) => {
            if json {
                let json_val = serde_json::json!(meta);
                println!(
                    "{}",
                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|e| {
                        eprintln!("tirith: checkpoint: JSON serialization failed: {e}");
                        "{}".to_string()
                    })
                );
            } else {
                println!("Checkpoint created: {}", meta.id);
                println!(
                    "  {} file(s), {}",
                    meta.file_count,
                    format_bytes(meta.total_bytes)
                );
            }
            0
        }
        Err(e) => {
            eprintln!("tirith checkpoint create: {e}");
            2
        }
    }
}

fn format_bytes(bytes: u64) -> String {
    if bytes < 1024 {
        format!("{bytes} B")
    } else if bytes < 1024 * 1024 {
        format!("{:.1} KiB", bytes as f64 / 1024.0)
    } else if bytes < 1024 * 1024 * 1024 {
        format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
    } else {
        format!("{:.1} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
    }
}