aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-10 23:56:32 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-06-10 23:56:32 -0500
commitc0c7c3b6ab40655dbf354509e5db228ad88b490b (patch)
tree26080ec78a55cbca1ffca3a8422aa5eb1bd3175b /src
parentd453ed91b6dc332fd45987c642e9d230efe870c8 (diff)
Improve overwrite VFS case handling
Diffstat (limited to 'src')
-rw-r--r--src/src/vfs/mo2filesystem.cpp61
-rw-r--r--src/src/vfs/overwritemanager.cpp260
-rw-r--r--src/src/vfs/overwritemanager.h10
-rw-r--r--src/tests/test_vfs_file_ops.cpp83
4 files changed, 383 insertions, 31 deletions
diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp
index 5687ba4..7b15d04 100644
--- a/src/src/vfs/mo2filesystem.cpp
+++ b/src/src/vfs/mo2filesystem.cpp
@@ -12,6 +12,7 @@
#include <cstdint>
#include <cstring>
#include <filesystem>
+#include <system_error>
#include <utility>
#if __has_include(<linux/msdos_fs.h>)
@@ -42,6 +43,29 @@ void invalidateAttrCache(Mo2FsContext* ctx, fuse_ino_t ino);
bool pathTouchesMutation(const std::string& cachedPath,
const std::string& changedPath);
+int fuseErrnoFromError(std::error_code ec, int fallback = EIO)
+{
+ if (!ec) {
+ return fallback;
+ }
+
+ if (ec == std::errc::no_such_file_or_directory) return ENOENT;
+ if (ec == std::errc::not_a_directory) return ENOTDIR;
+ if (ec == std::errc::is_a_directory) return EISDIR;
+ if (ec == std::errc::file_exists) return EEXIST;
+ if (ec == std::errc::permission_denied) return EACCES;
+ if (ec == std::errc::directory_not_empty) return ENOTEMPTY;
+ if (ec == std::errc::invalid_argument) return EINVAL;
+ if (ec == std::errc::too_many_symbolic_link_levels) return ELOOP;
+ if (ec == std::errc::filename_too_long) return ENAMETOOLONG;
+
+ if (ec.category() == std::generic_category() ||
+ ec.category() == std::system_category()) {
+ return ec.value() != 0 ? ec.value() : fallback;
+ }
+ return fallback;
+}
+
// RAII helper that records per-op wall-clock nanoseconds into a counter.
struct OpTimer
{
@@ -1872,7 +1896,7 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi)
fd = open(realPath.c_str(), O_RDWR);
}
if (fd < 0) {
- fuse_reply_err(req, EIO);
+ fuse_reply_err(req, errno != 0 ? errno : EIO);
return;
}
} else {
@@ -2105,6 +2129,12 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
joinPath(parentPath, canonicalChildName(ctx, parentPath, name));
_t.path = relative;
+ const auto preCreateSnap = snapshotForPath(ctx, relative);
+ if (preCreateSnap.found && preCreateSnap.is_directory) {
+ fuse_reply_err(req, EISDIR);
+ return;
+ }
+
std::string realPath;
// If this file is tracked to a mod folder, create it there instead of staging
@@ -2131,9 +2161,10 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
}
if (trackedMod.empty()) {
- fd = ctx->overwrite->createFile(relative, mode, &realPath);
+ std::error_code createError;
+ fd = ctx->overwrite->createFile(relative, mode, &realPath, &createError);
if (fd < 0) {
- fuse_reply_err(req, EIO);
+ fuse_reply_err(req, fuseErrnoFromError(createError));
return;
}
}
@@ -2150,7 +2181,7 @@ void mo2_create(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t mode
updateFileNodeKnown(ctx, relative, realPath, origin, createdSize, createdMtime);
invalidateDirCache(ctx, parentPath);
- {
+ if (!preCreateSnap.found) {
std::unique_lock lock(ctx->tree_mutex);
++ctx->tree->file_count;
}
@@ -2242,12 +2273,17 @@ void mo2_rename(fuse_req_t req, fuse_ino_t parent, const char* name,
std::string newRealPath;
- if (!ctx->overwrite->rename(oldRelative, newRelative)) {
+ std::error_code renameError;
+ if (!ctx->overwrite->rename(oldRelative, newRelative, &renameError)) {
// Source file is not in staging or overwrite — it's a backing (game) file
// or a mod file. Copy it to staging at the destination path instead of
// moving the original. This is the VFS equivalent of a rename: the file
// appears at the new path and disappears from the old path in the virtual
// view, but we never modify the real game/mod directories.
+ if (renameError != std::make_error_code(std::errc::no_such_file_or_directory)) {
+ fuse_reply_err(req, fuseErrnoFromError(renameError));
+ return;
+ }
if (oldSnap.is_directory) {
fuse_reply_err(req, EACCES);
return;
@@ -2605,15 +2641,24 @@ void mo2_mkdir(fuse_req_t req, fuse_ino_t parent, const char* name, mode_t /*mod
const std::string relative =
joinPath(parentPath, canonicalChildName(ctx, parentPath, name));
- if (!ctx->overwrite->createDirectory(relative)) {
- fuse_reply_err(req, EIO);
+ const auto preCreateSnap = snapshotForPath(ctx, relative);
+ if (preCreateSnap.found && !preCreateSnap.is_directory) {
+ fuse_reply_err(req, EEXIST);
+ return;
+ }
+
+ std::error_code createError;
+ if (!ctx->overwrite->createDirectory(relative, &createError)) {
+ fuse_reply_err(req, fuseErrnoFromError(createError));
return;
}
{
std::unique_lock lock(ctx->tree_mutex);
ctx->tree->root.insertDirectory(splitPath(relative));
- ++ctx->tree->dir_count;
+ if (!preCreateSnap.found) {
+ ++ctx->tree->dir_count;
+ }
invalidateNodeCache(ctx, relative);
}
invalidateDirCache(ctx, parentPath);
diff --git a/src/src/vfs/overwritemanager.cpp b/src/src/vfs/overwritemanager.cpp
index e4e3d0e..9a6f4ee 100644
--- a/src/src/vfs/overwritemanager.cpp
+++ b/src/src/vfs/overwritemanager.cpp
@@ -3,8 +3,11 @@
#include <fcntl.h>
#include <unistd.h>
+#include <cerrno>
+#include <cctype>
#include <filesystem>
#include <fstream>
+#include <iterator>
namespace
{
@@ -23,6 +26,102 @@ std::string sanitizeRelative(const std::string& relative)
}
return out;
}
+
+std::string lowercaseAscii(const std::string& value)
+{
+ std::string out;
+ out.reserve(value.size());
+ for (unsigned char c : value) {
+ out.push_back(static_cast<char>(std::tolower(c)));
+ }
+ return out;
+}
+
+fs::path resolveCaseInsensitivePath(const fs::path& root,
+ const std::string& relative_path,
+ bool stop_before_leaf,
+ std::error_code* out_error = nullptr)
+{
+ if (out_error != nullptr) {
+ out_error->clear();
+ }
+
+ fs::path current = root;
+ const fs::path relative(sanitizeRelative(relative_path));
+ const auto end = relative.end();
+
+ for (auto it = relative.begin(); it != end; ++it) {
+ if (stop_before_leaf && std::next(it) == end) {
+ current /= *it;
+ break;
+ }
+
+ std::error_code ec;
+ const bool currentExists = fs::exists(current, ec);
+ if (ec) {
+ if (out_error != nullptr) {
+ *out_error = ec;
+ }
+ return current / *it;
+ }
+
+ if (!currentExists) {
+ current /= *it;
+ continue;
+ }
+
+ if (!fs::is_directory(current, ec)) {
+ if (out_error != nullptr) {
+ *out_error = ec ? ec
+ : std::make_error_code(std::errc::not_a_directory);
+ }
+ return current / *it;
+ }
+
+ const std::string wanted = lowercaseAscii(it->string());
+ bool matched = false;
+ for (const auto& entry : fs::directory_iterator(
+ current, fs::directory_options::skip_permission_denied, ec)) {
+ if (lowercaseAscii(entry.path().filename().string()) == wanted) {
+ current = entry.path();
+ matched = true;
+ break;
+ }
+ }
+ if (ec) {
+ if (out_error != nullptr) {
+ *out_error = ec;
+ }
+ return current / *it;
+ }
+ if (!matched) {
+ current /= *it;
+ }
+ }
+
+ return current;
+}
+
+fs::path resolveExistingCaseInsensitivePath(const fs::path& root,
+ const std::string& relative_path,
+ std::error_code* out_error = nullptr)
+{
+ return resolveCaseInsensitivePath(root, relative_path, false, out_error);
+}
+
+fs::path resolveParentCaseInsensitivePath(const fs::path& root,
+ const std::string& relative_path,
+ std::error_code* out_error = nullptr)
+{
+ return resolveCaseInsensitivePath(root, relative_path, true, out_error);
+}
+
+void setError(std::error_code* out_error, std::error_code ec)
+{
+ if (out_error != nullptr) {
+ *out_error = ec;
+ }
+}
} // namespace
OverwriteManager::OverwriteManager(const std::string& staging_dir,
@@ -47,12 +146,29 @@ std::string OverwriteManager::overwritePath(const std::string& relative_path) co
std::string OverwriteManager::copyOnWrite(const std::string& source_path,
const std::string& relative_path) const
{
- const fs::path dest = stagingPath(relative_path);
-
std::error_code ec;
+ fs::path dest = resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWrite", dest, ec);
+ }
+
+ if (!fs::exists(dest, ec)) {
+ dest = resolveParentCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWrite", dest, ec);
+ }
+ }
fs::create_directories(dest.parent_path(), ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWrite", dest.parent_path(), ec);
+ }
if (fs::exists(dest, ec)) {
+ if (fs::is_directory(dest, ec)) {
+ throw fs::filesystem_error(
+ "copyOnWrite", dest,
+ ec ? ec : std::make_error_code(std::errc::is_a_directory));
+ }
// Ensure write permission even for pre-existing staging files (e.g.
// orphaned from a previous crash).
fs::permissions(dest, fs::perms::owner_write, fs::perm_options::add, ec);
@@ -87,12 +203,29 @@ std::string OverwriteManager::copyOnWriteFromFd(
int dir_fd, const std::string& source_relative_path,
const std::string& dest_relative_path) const
{
- const fs::path dest = stagingPath(dest_relative_path);
-
std::error_code ec;
+ fs::path dest = resolveExistingCaseInsensitivePath(m_stagingDir, dest_relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWriteFromFd", dest, ec);
+ }
+
+ if (!fs::exists(dest, ec)) {
+ dest = resolveParentCaseInsensitivePath(m_stagingDir, dest_relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWriteFromFd", dest, ec);
+ }
+ }
fs::create_directories(dest.parent_path(), ec);
+ if (ec) {
+ throw fs::filesystem_error("copyOnWriteFromFd", dest.parent_path(), ec);
+ }
if (fs::exists(dest, ec)) {
+ if (fs::is_directory(dest, ec)) {
+ throw fs::filesystem_error(
+ "copyOnWriteFromFd", dest,
+ ec ? ec : std::make_error_code(std::errc::is_a_directory));
+ }
return dest.string();
}
@@ -127,9 +260,22 @@ std::string OverwriteManager::copyOnWriteFromFd(
std::string OverwriteManager::writeFile(const std::string& relative_path,
const std::vector<uint8_t>& data) const
{
- const fs::path path = stagingPath(relative_path);
std::error_code ec;
+ fs::path path = resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("writeFile", path, ec);
+ }
+
+ if (!fs::exists(path, ec)) {
+ path = resolveParentCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ throw fs::filesystem_error("writeFile", path, ec);
+ }
+ }
fs::create_directories(path.parent_path(), ec);
+ if (ec) {
+ throw fs::filesystem_error("writeFile", path.parent_path(), ec);
+ }
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) {
@@ -144,18 +290,48 @@ std::string OverwriteManager::writeFile(const std::string& relative_path,
}
int OverwriteManager::createFile(const std::string& relative_path, mode_t mode,
- std::string* real_path) const
+ std::string* real_path,
+ std::error_code* out_error) const
{
- const fs::path path = stagingPath(relative_path);
std::error_code ec;
+ fs::path path = resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return -1;
+ }
+
+ if (fs::exists(path, ec)) {
+ if (ec) {
+ setError(out_error, ec);
+ return -1;
+ }
+ if (fs::is_directory(path, ec)) {
+ setError(out_error, ec ? ec : std::make_error_code(std::errc::is_a_directory));
+ return -1;
+ }
+ } else {
+ path = resolveParentCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return -1;
+ }
+ }
+
fs::create_directories(path.parent_path(), ec);
if (ec) {
+ setError(out_error, ec);
return -1;
}
+ if (out_error != nullptr) {
+ out_error->clear();
+ }
const mode_t fileMode = mode != 0 ? (mode & 07777) : 0644;
const int fd = open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC,
fileMode);
+ if (fd < 0) {
+ setError(out_error, std::error_code(errno, std::generic_category()));
+ }
if (fd >= 0 && real_path != nullptr) {
*real_path = path.string();
}
@@ -163,35 +339,50 @@ int OverwriteManager::createFile(const std::string& relative_path, mode_t mode,
}
bool OverwriteManager::rename(const std::string& old_relative,
- const std::string& new_relative) const
+ const std::string& new_relative,
+ std::error_code* out_error) const
{
std::error_code ec;
- fs::path from = stagingPath(old_relative);
- fs::path to = stagingPath(new_relative);
+ fs::path from = resolveExistingCaseInsensitivePath(m_stagingDir, old_relative, &ec);
+ fs::path to = resolveParentCaseInsensitivePath(m_stagingDir, new_relative, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
if (!fs::exists(from, ec)) {
- from = overwritePath(old_relative);
- to = overwritePath(new_relative);
+ from = resolveExistingCaseInsensitivePath(m_overwriteDir, old_relative, &ec);
+ to = resolveParentCaseInsensitivePath(m_overwriteDir, new_relative, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
if (!fs::exists(from, ec)) {
+ setError(out_error, std::make_error_code(std::errc::no_such_file_or_directory));
return false;
}
}
fs::create_directories(to.parent_path(), ec);
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
fs::rename(from, to, ec);
+ setError(out_error, ec);
return !ec;
}
bool OverwriteManager::removeFile(const std::string& relative_path) const
{
std::error_code ec;
- fs::path staged = stagingPath(relative_path);
+ fs::path staged = resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec);
if (fs::exists(staged, ec)) {
return fs::remove(staged, ec);
}
- fs::path overwrite = overwritePath(relative_path);
+ fs::path overwrite = resolveExistingCaseInsensitivePath(m_overwriteDir, relative_path, &ec);
if (fs::exists(overwrite, ec)) {
return fs::remove(overwrite, ec);
}
@@ -208,8 +399,9 @@ bool OverwriteManager::removeDirectory(const std::string& relative_path,
std::error_code ec;
bool removedAny = false;
- for (const fs::path candidate : {fs::path(stagingPath(relative_path)),
- fs::path(overwritePath(relative_path))}) {
+ for (const fs::path candidate : {
+ resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec),
+ resolveExistingCaseInsensitivePath(m_overwriteDir, relative_path, &ec)}) {
if (!fs::exists(candidate, ec))
continue;
if (!fs::is_directory(candidate, ec))
@@ -228,16 +420,44 @@ bool OverwriteManager::removeDirectory(const std::string& relative_path,
return removedAny;
}
-bool OverwriteManager::createDirectory(const std::string& relative_path) const
+bool OverwriteManager::createDirectory(const std::string& relative_path,
+ std::error_code* out_error) const
{
std::error_code ec;
- fs::create_directories(stagingPath(relative_path), ec);
+ fs::path path = resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
+
+ if (fs::exists(path, ec)) {
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
+ if (!fs::is_directory(path, ec)) {
+ setError(out_error, ec ? ec : std::make_error_code(std::errc::file_exists));
+ return false;
+ }
+ setError(out_error, {});
+ return true;
+ }
+
+ path = resolveParentCaseInsensitivePath(m_stagingDir, relative_path, &ec);
+ if (ec) {
+ setError(out_error, ec);
+ return false;
+ }
+ fs::create_directories(path, ec);
+ setError(out_error, ec);
return !ec;
}
bool OverwriteManager::exists(const std::string& relative_path) const
{
std::error_code ec;
- return fs::exists(stagingPath(relative_path), ec) ||
- fs::exists(overwritePath(relative_path), ec);
+ return fs::exists(resolveExistingCaseInsensitivePath(m_stagingDir, relative_path, &ec),
+ ec) ||
+ fs::exists(resolveExistingCaseInsensitivePath(m_overwriteDir, relative_path, &ec),
+ ec);
}
diff --git a/src/src/vfs/overwritemanager.h b/src/src/vfs/overwritemanager.h
index 5579f0d..6725dcd 100644
--- a/src/src/vfs/overwritemanager.h
+++ b/src/src/vfs/overwritemanager.h
@@ -2,6 +2,7 @@
#define VFS_OVERWRITEMANAGER_H
#include <cstdint>
+#include <system_error>
#include <string>
#include <sys/types.h>
#include <vector>
@@ -21,12 +22,15 @@ public:
std::string writeFile(const std::string& relative_path,
const std::vector<uint8_t>& data) const;
int createFile(const std::string& relative_path, mode_t mode,
- std::string* real_path) const;
+ std::string* real_path,
+ std::error_code* out_error = nullptr) const;
- bool rename(const std::string& old_relative, const std::string& new_relative) const;
+ bool rename(const std::string& old_relative, const std::string& new_relative,
+ std::error_code* out_error = nullptr) const;
bool removeFile(const std::string& relative_path) const;
bool removeDirectory(const std::string& relative_path, bool* out_not_empty = nullptr) const;
- bool createDirectory(const std::string& relative_path) const;
+ bool createDirectory(const std::string& relative_path,
+ std::error_code* out_error = nullptr) const;
bool exists(const std::string& relative_path) const;
std::string overwritePath(const std::string& relative_path) const;
diff --git a/src/tests/test_vfs_file_ops.cpp b/src/tests/test_vfs_file_ops.cpp
index d923c41..9a37f52 100644
--- a/src/tests/test_vfs_file_ops.cpp
+++ b/src/tests/test_vfs_file_ops.cpp
@@ -9,6 +9,7 @@
#include <filesystem>
#include <fstream>
#include <string>
+#include <system_error>
namespace fs = std::filesystem;
@@ -103,6 +104,88 @@ TEST(VfsFileOps, CreateFileReturnsWritableStagingHandle)
EXPECT_EQ(readText(realPath), "shader");
}
+TEST(VfsFileOps, CreateFileReusesExistingCaseVariant)
+{
+ TempRoot tmp;
+ ASSERT_FALSE(tmp.path().empty());
+
+ const fs::path staging = tmp.path() / "staging";
+ const fs::path overwrite = tmp.path() / "overwrite";
+
+ OverwriteManager overwriteManager(staging.string(), overwrite.string());
+ std::error_code ec;
+ ASSERT_TRUE(overwriteManager.createDirectory("TempProbe", &ec));
+ ASSERT_FALSE(ec);
+
+ std::string firstPath;
+ int fd = overwriteManager.createFile("TempProbe/.wb_case_test", 0600,
+ &firstPath, &ec);
+ ASSERT_GE(fd, 0);
+ ASSERT_FALSE(ec);
+ const char first[] = "first";
+ ASSERT_EQ(write(fd, first, sizeof(first) - 1), ssize_t(sizeof(first) - 1));
+ close(fd);
+
+ std::string secondPath;
+ fd = overwriteManager.createFile("tempprobe/.Wb_CaSe_TeSt", 0600,
+ &secondPath, &ec);
+ ASSERT_GE(fd, 0);
+ ASSERT_FALSE(ec);
+ const char second[] = "second";
+ ASSERT_EQ(write(fd, second, sizeof(second) - 1), ssize_t(sizeof(second) - 1));
+ close(fd);
+
+ EXPECT_EQ(fs::path(secondPath), fs::path(firstPath));
+ EXPECT_EQ(fs::path(firstPath), staging / "TempProbe/.wb_case_test");
+ EXPECT_EQ(readText(firstPath), "second");
+
+ size_t childCount = 0;
+ for (const auto& entry : fs::directory_iterator(staging / "TempProbe")) {
+ (void)entry;
+ ++childCount;
+ }
+ EXPECT_EQ(childCount, 1u);
+}
+
+TEST(VfsFileOps, CreateDirectoryReusesExistingCaseVariant)
+{
+ TempRoot tmp;
+ ASSERT_FALSE(tmp.path().empty());
+
+ const fs::path staging = tmp.path() / "staging";
+ const fs::path overwrite = tmp.path() / "overwrite";
+
+ OverwriteManager overwriteManager(staging.string(), overwrite.string());
+ std::error_code ec;
+ ASSERT_TRUE(overwriteManager.createDirectory("TempProbe", &ec));
+ ASSERT_FALSE(ec);
+ ASSERT_TRUE(overwriteManager.createDirectory("tempprobe", &ec));
+ ASSERT_FALSE(ec);
+
+ EXPECT_TRUE(fs::is_directory(staging / "TempProbe"));
+ EXPECT_FALSE(fs::exists(staging / "tempprobe"));
+}
+
+TEST(VfsFileOps, CreateDirectoryReportsParentFileAsNotDirectory)
+{
+ TempRoot tmp;
+ ASSERT_FALSE(tmp.path().empty());
+
+ const fs::path staging = tmp.path() / "staging";
+ const fs::path overwrite = tmp.path() / "overwrite";
+
+ OverwriteManager overwriteManager(staging.string(), overwrite.string());
+ std::error_code ec;
+ std::string realPath;
+ const int fd = overwriteManager.createFile("TempProbe", 0600, &realPath, &ec);
+ ASSERT_GE(fd, 0);
+ close(fd);
+ ASSERT_FALSE(ec);
+
+ EXPECT_FALSE(overwriteManager.createDirectory("tempprobe/child", &ec));
+ EXPECT_EQ(ec, std::make_error_code(std::errc::not_a_directory));
+}
+
TEST(VfsFileOps, InodeRenameMovesDescendants)
{
InodeTable table;