diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-06-10 23:56:32 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-06-10 23:56:32 -0500 |
| commit | c0c7c3b6ab40655dbf354509e5db228ad88b490b (patch) | |
| tree | 26080ec78a55cbca1ffca3a8422aa5eb1bd3175b | |
| parent | d453ed91b6dc332fd45987c642e9d230efe870c8 (diff) | |
Improve overwrite VFS case handling
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 61 | ||||
| -rw-r--r-- | src/src/vfs/overwritemanager.cpp | 260 | ||||
| -rw-r--r-- | src/src/vfs/overwritemanager.h | 10 | ||||
| -rw-r--r-- | src/tests/test_vfs_file_ops.cpp | 83 |
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; |
