diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-11 12:21:39 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-03-11 12:21:39 -0500 |
| commit | ad268d496f01512f3b67afeb8d6c358c17559c82 (patch) | |
| tree | 7f4d6e22968053b22ca49935353c986df147377b /src | |
| parent | 7235a616426d55502aac33de68fca68e715ce18d (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.cpp | 22 | ||||
| -rw-r--r-- | src/src/vfs/trackedwrites.cpp | 54 | ||||
| -rw-r--r-- | src/src/vfs/trackedwrites.h | 11 |
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; |
