Skip to main content

notalterra/
guard.rs

1//! Game-running guard and transaction logging.
2//!
3//! Before launch and before each destructive operation, check whether
4//! Subnautica 2 is running — the game holds file locks on `.sav` files
5//! while active.
6
7use anyhow::Result;
8use chrono::Local;
9use std::fs::{self, OpenOptions};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13// ── process detection ──────────────────────────────────────────────────────
14
15/// Return `true` if Subnautica 2 appears to be running.
16///
17/// Always returns `false` — process detection (`tasklist` / `pgrep`) is
18/// intentionally disabled to avoid Windows Defender false positives
19/// (Trojan:Win32/Wacatac.C!ml).  Users are reminded to close the game
20/// manually before using the tool.
21#[cfg(target_os = "windows")]
22pub fn game_running() -> bool {
23    // Process detection disabled — avoid AV false-positives.
24    // Close Subnautica 2 manually before using NotAlterra.
25    false
26}
27/// Check if Subnautica 2 is currently running (Linux).
28///
29/// Always returns `false` — process detection via `pgrep` is disabled to
30/// avoid false positives. Users are reminded to close the game manually.
31#[cfg(not(target_os = "windows"))]
32pub fn game_running() -> bool {
33    false
34}
35#[allow(dead_code)]
36/// Check if Subnautica 2 is running via pgrep (Linux). Dormant.
37fn _game_running_linux() -> bool {
38    let patterns = &["Subnautica2", "Subnautica2-Win64-Shipping"];
39    for pat in patterns {
40        if let Ok(out) = std::process::Command::new("pgrep")
41            .args(["-ci", pat])
42            .output()
43        {
44            let s = String::from_utf8_lossy(&out.stdout);
45            if let Ok(n) = s.trim().parse::<u32>() {
46                if n > 0 {
47                    return true;
48                }
49            }
50        }
51    }
52    false
53}
54
55// ── transaction logging ────────────────────────────────────────────────────
56
57/// Migrate the old `transaction.log` (next to the binary) into the new
58/// `logs/` directory.  Appends old entries to the new file, then renames
59/// the old file to `.migrated` so it won't be migrated again.
60/// Returns `true` if an old log was found and handled.
61pub fn migrate_old_log() -> bool {
62    let old = exe_dir().join("transaction.log");
63    if !old.exists() {
64        return false;
65    }
66    let new = log_path();
67    // Read old content
68    if let Ok(content) = std::fs::read_to_string(&old) {
69        if !content.trim().is_empty() {
70            // Append to new log with a migration header
71            let header = format!(
72                "───── migrated from old location [{ts}] ─────\n",
73                ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
74            );
75            if let Ok(mut f) = std::fs::OpenOptions::new()
76                .create(true)
77                .append(true)
78                .open(&new)
79            {
80                use std::io::Write;
81                let _ = write!(f, "{header}{content}");
82            }
83        }
84    }
85    // Rename old file so it won't be processed again
86    let migrated = old.with_extension("log.migrated");
87    let _ = std::fs::rename(&old, &migrated);
88    true
89}
90
91/// Path to `transaction.log` inside the `logs/` directory under the
92/// platform config root.  All timestamped actions are appended here for
93/// audit trail purposes.
94pub fn log_path() -> PathBuf {
95    let p = dirs::data_local_dir()
96        .map(|d| d.join("NotAlterra").join("logs").join("transaction.log"))
97        .unwrap_or_else(|| exe_dir().join("logs").join("transaction.log"));
98    if let Some(parent) = p.parent() {
99        std::fs::create_dir_all(parent).ok();
100    }
101    p
102}
103
104/// Return the directory containing the running executable.
105fn exe_dir() -> PathBuf {
106    std::env::current_exe()
107        .ok()
108        .and_then(|p| p.parent().map(Path::to_path_buf))
109        .unwrap_or_else(|| PathBuf::from("."))
110}
111
112/// Maximum lines before rotation.
113const MAX_LOG_LINES: usize = 10_000;
114
115/// Append a timestamped log entry to `transaction.log`.
116///
117/// Format: `YYYY-MM-DD HH:MM:SS | ACTION   | detail | result`
118/// Auto-rotates if the log exceeds 10,000 lines — the oldest lines are
119/// discarded, keeping only the most recent 10,000.
120pub fn log_action(action: &str, detail: &str, result: &str, log_path: &Path) -> Result<()> {
121    let stamp = Local::now().format("%Y-%m-%d %H:%M:%S");
122    let line = format!("{stamp} | {action:<8} | {detail} | {result}\n");
123
124    // Rotate if needed
125    if log_path.exists() {
126        if let Ok(content) = fs::read_to_string(log_path) {
127            let lines: Vec<&str> = content.lines().collect();
128            if lines.len() > MAX_LOG_LINES {
129                let keep: String = lines[lines.len() - MAX_LOG_LINES..].join("\n");
130                fs::write(log_path, keep + "\n").ok();
131            }
132        }
133    }
134
135    let mut f = OpenOptions::new()
136        .create(true)
137        .append(true)
138        .open(log_path)?;
139    f.write_all(line.as_bytes())?;
140    Ok(())
141}
142/// Truncate a filesystem path to start at `Subnautica2/` or `Subnautica2\`,
143/// stripping the user-specific prefix for privacy-safe logging.
144///
145/// Returns the original path unchanged if `Subnautica2` is not found in the
146/// input (e.g. custom paths entered via `Set save folder`).
147pub fn sanitize_path(p: &str) -> String {
148    let needle = "Subnautica2";
149    let sep = if p.contains('\\') { "\\" } else { "/" };
150    if let Some(pos) = p.find(needle) {
151        format!("...{sep}{}", &p[pos..])
152    } else {
153        p.to_string()
154    }
155}
156
157/// Check whether a path looks like a network/UNC path — for warning purposes.
158/// Matches paths starting with `\\` (Windows) or `//` (Linux).
159pub fn is_network_path(p: &str) -> bool {
160    p.starts_with("\\\\") || p.starts_with("//")
161}
162
163/// Estimate free space on the volume containing `path` in bytes.
164/// Returns `None` on platforms or filesystems where we can't determine this.
165pub fn available_space(_path: &Path) -> Option<u64> {
166    // Advisory only — the PowerShell original had a try/catch fallback.
167    // For a cross-platform build without platform-specific FFI, we
168    // return None and skip the disk-space warning.
169    None
170}