aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-11 12:21:39 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-11 12:21:39 -0500
commitad268d496f01512f3b67afeb8d6c358c17559c82 (patch)
tree7f4d6e22968053b22ca49935353c986df147377b /src
parent7235a616426d55502aac33de68fca68e715ce18d (diff)
Fix tracked writes false positives, stale entries, portability, and launcher self-destruct
- Remove initialScan() from mount and unmount — its heuristic ("file exists in both overwrite and a mod") caused false-positive tracking of game-generated config files. Tracking now only happens through explicit UI actions or snapshot-based detectManualMoves(). - Prune stale tracked entries on load when mod folder no longer exists on disk. - Store mod paths as relative (to JSON file parent) for instance portability; legacy absolute paths auto-migrate on next save. - Guard launcher sync against HERE==BIN_DST to prevent rm -rf self-destruct when running directly from ~/.local/share/fluorine/bin/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/src/fuseconnector.cpp22
-rw-r--r--src/src/vfs/trackedwrites.cpp54
-rw-r--r--src/src/vfs/trackedwrites.h11
3 files changed, 78 insertions, 9 deletions
diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp
index 1581423..c1aa38a 100644
--- a/src/src/fuseconnector.cpp
+++ b/src/src/fuseconnector.cpp
@@ -283,10 +283,13 @@ bool FuseConnector::mount(
m_trackedWrites->load(m_trackingFilePath);
if (existed) {
m_trackedWrites->detectManualMoves(m_overwriteDir, mods);
- } else {
- // First run: scan overwrite for files that also exist in a mod
- m_trackedWrites->initialScan(m_overwriteDir, mods);
}
+ // NOTE: initialScan() is no longer called on first run. Its heuristic
+ // ("file exists in both overwrite and a mod → track it") produces false
+ // positives: game-generated overwrite files that happen to share a name
+ // with a mod file get incorrectly tracked. Tracking now only happens
+ // through explicit user actions (UI move/sync/drag-drop) or the
+ // snapshot-based detectManualMoves().
m_trackedWrites->save(m_trackingFilePath);
} else {
std::fprintf(stderr, "[VFS] WARNING: tracking file path is empty!\n");
@@ -394,11 +397,16 @@ void FuseConnector::unmount()
static_cast<long long>(ms));
}
- // After flush: scan overwrite for files that also exist in mods (bootstraps
- // tracking on first run), snapshot overwrite contents for next session's
- // manual-move detection, then save tracking data.
+ // Snapshot overwrite contents for next session's manual-move detection,
+ // then save tracking data.
+ //
+ // NOTE: initialScan() is intentionally NOT called here. It was previously
+ // run at every unmount, which caused false-positive tracking: any file that
+ // exists in both overwrite and a mod got auto-tracked, even if the overwrite
+ // copy was a game-generated modification (not a user move). Tracking should
+ // only happen through explicit user actions (UI move/sync) or the
+ // snapshot-based detectManualMoves() at mount time.
if (m_trackedWrites && !m_trackingFilePath.empty()) {
- m_trackedWrites->initialScan(m_overwriteDir, m_lastMods);
m_trackedWrites->snapshotOverwrite(m_overwriteDir);
m_trackedWrites->save(m_trackingFilePath);
}
diff --git a/src/src/vfs/trackedwrites.cpp b/src/src/vfs/trackedwrites.cpp
index 4392db3..62bb88b 100644
--- a/src/src/vfs/trackedwrites.cpp
+++ b/src/src/vfs/trackedwrites.cpp
@@ -84,6 +84,9 @@ void TrackedWrites::load(const std::string& path)
const std::string data = ss.str();
f.close();
+ // Base directory for resolving relative mod paths.
+ const std::string baseDir = fs::path(path).parent_path().string();
+
std::lock_guard lock(m_mutex);
m_tracked.clear();
m_overwriteSnapshot.clear();
@@ -136,7 +139,17 @@ void TrackedWrites::load(const std::string& path)
++pos;
std::string modPath = parseJsonString(data, pos);
- m_tracked[toLower(relPath)] = modPath;
+ // Resolve mod path: if it's relative, resolve against base dir.
+ // If it's absolute (legacy format), use as-is for backwards compat.
+ std::error_code ec;
+ std::string resolved;
+ if (!modPath.empty() && modPath[0] != '/') {
+ resolved = fs::weakly_canonical(fs::path(baseDir) / modPath, ec).string();
+ } else {
+ resolved = modPath;
+ }
+
+ m_tracked[toLower(relPath)] = resolved;
skipWhitespace(data, pos);
if (pos < data.size() && data[pos] == ',')
@@ -171,6 +184,9 @@ void TrackedWrites::load(const std::string& path)
}
std::fprintf(stderr, "[VFS] loaded %zu tracked write mappings\n", m_tracked.size());
+
+ // Prune entries whose mod folder no longer exists (e.g. user deleted the mod).
+ pruneStaleUnlocked();
}
void TrackedWrites::save(const std::string& path) const
@@ -187,12 +203,22 @@ void TrackedWrites::save(const std::string& path) const
return;
}
+ // Base directory for making mod paths relative (portable).
+ const fs::path baseDir = fs::path(path).parent_path();
+
f << "{\n \"tracked\": {";
bool first = true;
for (const auto& [relPath, modPath] : m_tracked) {
if (!first)
f << ",";
- f << "\n \"" << jsonEscape(relPath) << "\": \"" << jsonEscape(modPath) << "\"";
+ // Convert absolute mod path to relative for portability.
+ std::string storedPath = modPath;
+ if (!modPath.empty() && modPath[0] == '/') {
+ auto rel = fs::relative(fs::path(modPath), baseDir, ec);
+ if (!ec && !rel.empty())
+ storedPath = rel.string();
+ }
+ f << "\n \"" << jsonEscape(relPath) << "\": \"" << jsonEscape(storedPath) << "\"";
first = false;
}
f << "\n },\n \"overwrite_snapshot\": [";
@@ -209,6 +235,30 @@ void TrackedWrites::save(const std::string& path) const
std::fprintf(stderr, "[VFS] saved %zu tracked write mappings\n", m_tracked.size());
}
+void TrackedWrites::pruneStale()
+{
+ std::lock_guard lock(m_mutex);
+ pruneStaleUnlocked();
+}
+
+void TrackedWrites::pruneStaleUnlocked()
+{
+ std::error_code ec;
+ int pruned = 0;
+ for (auto it = m_tracked.begin(); it != m_tracked.end(); ) {
+ if (!fs::is_directory(it->second, ec)) {
+ std::fprintf(stderr, "[VFS] pruning stale tracking: %s -> %s (mod folder gone)\n",
+ it->first.c_str(), it->second.c_str());
+ it = m_tracked.erase(it);
+ ++pruned;
+ } else {
+ ++it;
+ }
+ }
+ if (pruned > 0)
+ std::fprintf(stderr, "[VFS] pruned %d stale tracked write entries\n", pruned);
+}
+
void TrackedWrites::track(const std::string& relative_path,
const std::string& mod_folder_path)
{
diff --git a/src/src/vfs/trackedwrites.h b/src/src/vfs/trackedwrites.h
index c9218ae..a8ebe08 100644
--- a/src/src/vfs/trackedwrites.h
+++ b/src/src/vfs/trackedwrites.h
@@ -24,11 +24,19 @@ public:
TrackedWrites() = default;
// Load tracking data from a JSON file.
+ // Mod paths are stored relative to the JSON file's parent directory for
+ // portability. They are resolved to absolute paths on load.
void load(const std::string& path);
// Save tracking data to a JSON file.
+ // Absolute mod paths are converted to relative (relative to the JSON file's
+ // parent directory) before writing.
void save(const std::string& path) const;
+ // Remove entries whose mod folder no longer exists on disk.
+ // Called automatically after load(), but can also be called explicitly.
+ void pruneStale();
+
// Record that a file at relative_path now lives in mod_folder_path.
// mod_folder_path is the absolute path to the mod's root directory.
void track(const std::string& relative_path, const std::string& mod_folder_path);
@@ -62,6 +70,9 @@ public:
std::unordered_map<std::string, std::string> allMappings() const;
private:
+ // Prune without locking (caller must hold m_mutex).
+ void pruneStaleUnlocked();
+
// relative_path (lowercase) -> absolute mod folder path
mutable std::mutex m_mutex;
std::unordered_map<std::string, std::string> m_tracked;