diff options
| author | SulfurNitride <lukew19@proton.me> | 2026-06-19 18:01:56 -0500 |
|---|---|---|
| committer | SulfurNitride <lukew19@proton.me> | 2026-06-19 18:01:56 -0500 |
| commit | ea794b1eba77e9b5dd8bea0fe4d3280b67d77a5a (patch) | |
| tree | c24a032e914bcf9e8572a47e096571e8770cf8f3 | |
| parent | 6c511ba040b07279bf5495088bfe98e2b6d6d318 (diff) | |
Fix VFS writes and stylesheet URLs
| -rwxr-xr-x | build.sh | 10 | ||||
| -rw-r--r-- | libs/installer_fomod/src/fomodinstallerdialog.cpp | 48 | ||||
| -rw-r--r-- | libs/installer_fomod/src/installerfomod.cpp | 2 | ||||
| -rw-r--r-- | src/src/moapplication.cpp | 3 | ||||
| -rw-r--r-- | src/src/vfs/mo2filesystem.cpp | 98 |
5 files changed, 120 insertions, 41 deletions
@@ -17,8 +17,10 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Auto-detect container runtime if command -v podman >/dev/null 2>&1; then DOCKER=podman + VOLUME_SUFFIX=",Z" elif command -v docker >/dev/null 2>&1; then DOCKER=docker + VOLUME_SUFFIX="" else echo "ERROR: Neither podman nor docker found in PATH" exit 1 @@ -53,8 +55,8 @@ mkdir -p "${CCACHE_DIR}" if [ "${BUILD_MODE}" = "shell" ]; then echo "=== Dropping into build container shell ===" exec ${DOCKER} run --rm -it \ - -v "${SCRIPT_DIR}:/src:rw" \ - -v "${CCACHE_DIR}:/ccache:rw" \ + -v "${SCRIPT_DIR}:/src:rw${VOLUME_SUFFIX}" \ + -v "${CCACHE_DIR}:/ccache:rw${VOLUME_SUFFIX}" \ -e CCACHE_DIR=/ccache \ -w /src \ --device /dev/fuse \ @@ -68,8 +70,8 @@ echo "=== Starting build (mode: ${BUILD_MODE}) ===" # Defaults to all available cores. BUILD_JOBS="${BUILD_JOBS:-$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)}" ${DOCKER} run --rm \ - -v "${SCRIPT_DIR}:/src:rw" \ - -v "${CCACHE_DIR}:/ccache:rw" \ + -v "${SCRIPT_DIR}:/src:rw${VOLUME_SUFFIX}" \ + -v "${CCACHE_DIR}:/ccache:rw${VOLUME_SUFFIX}" \ -e CCACHE_DIR=/ccache \ -e BUILD_MODE="${BUILD_MODE}" \ -e BUILD_JOBS="${BUILD_JOBS}" \ diff --git a/libs/installer_fomod/src/fomodinstallerdialog.cpp b/libs/installer_fomod/src/fomodinstallerdialog.cpp index f675e3d..7c27726 100644 --- a/libs/installer_fomod/src/fomodinstallerdialog.cpp +++ b/libs/installer_fomod/src/fomodinstallerdialog.cpp @@ -146,6 +146,26 @@ struct XmlParseError : std::runtime_error XmlParseError(const QString& message) : std::runtime_error(qUtf8Printable(message)) {} }; +static QString normalizeArchivePath(QString path) +{ + path.replace("\\", "/"); + path = QDir::cleanPath(path); + return path == "." ? QString() : path; +} + +static QString joinArchivePath(const QString& base, const QString& child) +{ + if (base.isEmpty()) { + return normalizeArchivePath(child); + } + return normalizeArchivePath(QDir(base).filePath(child)); +} + +static QString tempArchivePath(const QString& archivePath) +{ + return QDir(QDir::tempPath()).filePath(normalizeArchivePath(archivePath)); +} + QByteArray skipXmlHeader(QIODevice& file) { static const unsigned char UTF16LE_BOM[] = {0xFF, 0xFE}; @@ -245,7 +265,8 @@ void FomodInstallerDialog::readXml(QFile& file, void FomodInstallerDialog::readInfoXml() { - QFile file(QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/info.xml"); + QFile file(tempArchivePath(joinArchivePath( + joinArchivePath(m_FomodPath, m_FomodDirName), "info.xml"))); // We don't need a info.xml file, so we just return if we cannot open it: if (!file.open(QIODevice::ReadOnly)) { @@ -256,7 +277,8 @@ void FomodInstallerDialog::readInfoXml() void FomodInstallerDialog::readModuleConfigXml() { - QFile file(QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/ModuleConfig.xml"); + QFile file(tempArchivePath(joinArchivePath( + joinArchivePath(m_FomodPath, m_FomodDirName), "ModuleConfig.xml"))); if (!file.open(QIODevice::ReadOnly)) { throw Exception(tr("%1 missing.").arg(file.fileName())); } @@ -270,8 +292,8 @@ void FomodInstallerDialog::initData(IOrganizer* moInfo) // parse provided package information readInfoXml(); - QString screenshotPath = - QDir::tempPath() + "/" + m_FomodPath + "/" + m_FomodDirName + "/screenshot.png"; + QString screenshotPath = tempArchivePath(joinArchivePath( + joinArchivePath(m_FomodPath, m_FomodDirName), "screenshot.png")); if (!QImage(screenshotPath).isNull()) { ui->screenshotLabel->setScalableResource(screenshotPath); ui->screenshotExpand->setVisible(false); @@ -645,8 +667,8 @@ void FomodInstallerDialog::highlightControl(QAbstractButton* button) if (screenshotName.isValid()) { QString screenshotFileName = screenshotName.toString(); if (!screenshotFileName.isEmpty()) { - QString temp = QDir::tempPath() + "/" + m_FomodPath + "/" + - QDir::fromNativeSeparators(screenshotFileName); + QString temp = + tempArchivePath(joinArchivePath(m_FomodPath, screenshotFileName)); ui->screenshotLabel->setScalableResource(temp); ui->screenshotExpand->setVisible(true); } else { @@ -775,10 +797,12 @@ void FomodInstallerDialog::readFileList(XmlReader& reader, FileDescriptorList& f log::debug("Ignoring {} entry with empty source.", reader.name().toString()); } else { FileDescriptor* file = new FileDescriptor(this); - file->m_Source = attributes.value("source").toString(); - file->m_Destination = attributes.hasAttribute("destination") - ? attributes.value("destination").toString() - : file->m_Source; + file->m_Source = + normalizeArchivePath(attributes.value("source").toString()); + file->m_Destination = + attributes.hasAttribute("destination") + ? normalizeArchivePath(attributes.value("destination").toString()) + : file->m_Source; file->m_Priority = attributes.hasAttribute("priority") ? attributes.value("priority").toString().toInt() : 0; @@ -1691,8 +1715,8 @@ void FomodInstallerDialog::on_screenshotExpand_clicked() continue; } - QString temp = QDir::tempPath() + "/" + m_FomodPath + "/" + - QDir::fromNativeSeparators(screenshotFileName); + QString temp = + tempArchivePath(joinArchivePath(m_FomodPath, screenshotFileName)); carouselImages.push_back(std::pair<QString, QString>(choice->text(), temp)); // Focus the screenshot carousel on the user's selected choice (or the first if diff --git a/libs/installer_fomod/src/installerfomod.cpp b/libs/installer_fomod/src/installerfomod.cpp index 239ec9a..e526610 100644 --- a/libs/installer_fomod/src/installerfomod.cpp +++ b/libs/installer_fomod/src/installerfomod.cpp @@ -216,7 +216,7 @@ InstallerFomod::install(GuessedValue<QString>& modName, try { std::shared_ptr<const IFileTree> fomodTree = findFomodDirectory(tree); - QString fomodPath = fomodTree->parent()->path(); + QString fomodPath = fomodTree->parent()->path("/"); QString fomodDirName = fomodTree->name(); FomodInstallerDialog dialog( this, modName, fomodPath, fomodDirName, diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index 4b1ee86..eb6e46d 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -852,8 +852,9 @@ QString styleSheetFontSizeOverride(int fontSize) QString resolveStyleSheetUrl(const QString& url, const QString& baseDir) { const QString trimmed = url.trimmed(); + const QUrl parsed(trimmed); if (trimmed.isEmpty() || trimmed.startsWith(':') || trimmed.startsWith('/') || - trimmed.contains("://")) { + !parsed.scheme().isEmpty()) { return trimmed; } diff --git a/src/src/vfs/mo2filesystem.cpp b/src/src/vfs/mo2filesystem.cpp index 9a65017..5176614 100644 --- a/src/src/vfs/mo2filesystem.cpp +++ b/src/src/vfs/mo2filesystem.cpp @@ -1796,17 +1796,21 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) std::string realPath = snap.real_path; const bool writable = isWritableOpen(fi->flags); + const bool truncateOnOpen = writable && ((fi->flags & O_TRUNC) != 0); bool isBacking = snap.is_backing; bool cowPending = false; bool isTracked = false; + uint64_t openSize = snap.size; + auto openMtime = snap.mtime; // Write strategy: // 1. Files already in staging/overwrite → open R/W directly. // 2. Tracked files (user moved from Overwrite to a mod) → open R/W // in-place so writes go back to the user's dedicated mod folder. - // 3. Existing mod/data-dir files → open R/W in place. - // 4. Base-game backing files → open R/O and COW to staging only on the - // first actual write(). + // 3. Existing mod/data-dir/base-game files → open R/O and COW to staging + // on the first actual write(). + // 4. O_TRUNC on a COW source must materialize + truncate immediately so + // the caller sees normal POSIX open(..., O_TRUNC) semantics. // // This matches upstream USVFS behavior more closely than the previous // conservative "copy everything writable into overwrite" approach. @@ -1832,10 +1836,20 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) if (!trackedMod.empty()) { // Tracked file — open R/W in-place in the mod folder. const std::string modFilePath = trackedMod + "/" + path; - fd = open(modFilePath.c_str(), O_RDWR); + int openFlags = O_RDWR | O_CLOEXEC; + if (truncateOnOpen) { + openFlags |= O_TRUNC; + } + fd = open(modFilePath.c_str(), openFlags); if (fd >= 0) { realPath = modFilePath; isTracked = true; + if (truncateOnOpen) { + openSize = 0; + openMtime = std::chrono::system_clock::now(); + updateFileNodeKnown(ctx, path, realPath, originForPath(ctx, realPath), + openSize, openMtime); + } } else { // Mod file disappeared — fall through to normal handling trackedMod.clear(); @@ -1844,18 +1858,49 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) if (fd < 0 && alreadyStaged) { // Already in staging/overwrite — open R/W directly. - fd = open(realPath.c_str(), O_RDWR); - } else if (fd < 0 && isBacking) { - // Backing file — open R/O, defer COW to first write(). + int openFlags = O_RDWR | O_CLOEXEC; + if (truncateOnOpen) { + openFlags |= O_TRUNC; + } + fd = open(realPath.c_str(), openFlags); + if (fd >= 0 && truncateOnOpen) { + openSize = 0; + openMtime = std::chrono::system_clock::now(); + updateFileNodeKnown(ctx, path, realPath, originForPath(ctx, realPath), + openSize, openMtime); + } + } else if (fd < 0 && truncateOnOpen) { + try { + std::string newPath; + if (isBacking && ctx->backing_dir_fd >= 0) { + newPath = ctx->overwrite->copyOnWriteFromFd(ctx->backing_dir_fd, path); + } else { + newPath = ctx->overwrite->copyOnWrite(realPath, path); + } + + fd = open(newPath.c_str(), O_RDWR | O_CLOEXEC | O_TRUNC); + if (fd >= 0) { + realPath = newPath; + isBacking = false; + cowPending = false; + openSize = 0; + openMtime = std::chrono::system_clock::now(); + updateFileNodeKnown(ctx, path, newPath, originForPath(ctx, newPath), + openSize, openMtime); + } + } catch (...) { + fuse_reply_err(req, EIO); + return; + } + } else if (fd < 0) { + // Existing mod/base-game file — open R/O, defer COW to first write(). if (ctx->backing_dir_fd >= 0) { - fd = openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY); + fd = isBacking ? openat(ctx->backing_dir_fd, realPath.c_str(), O_RDONLY | O_CLOEXEC) + : open(realPath.c_str(), O_RDONLY | O_CLOEXEC); } else { - fd = open(realPath.c_str(), O_RDONLY); + fd = open(realPath.c_str(), O_RDONLY | O_CLOEXEC); } cowPending = true; - } else if (fd < 0) { - // Existing mod/data-dir file — write in place. - fd = open(realPath.c_str(), O_RDWR); } if (fd < 0) { fuse_reply_err(req, errno != 0 ? errno : EIO); @@ -1879,8 +1924,8 @@ void mo2_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) of.cow_pending = cowPending; of.is_tracked = isTracked; of.relative_path = path; - of.virtual_size = snap.size; - of.virtual_mtime = snap.mtime; + of.virtual_size = openSize; + of.virtual_mtime = openMtime; ctx->open_files[fh] = std::move(of); } @@ -2010,9 +2055,9 @@ void mo2_write(fuse_req_t req, fuse_ino_t /*ino*/, const char* buf, size_t size, return; } - // Lazy COW applies only to backing-game files. Existing mod files should - // stay in place so config writes update the source mod rather than creating - // a shadow copy in overwrite. + // Lazy COW applies to any existing VFS source that is not already staging, + // overwrite, or an explicitly tracked custom-output file. This preserves + // mod files and lets generated output land in staging/overwrite. if (cowPending) { ctx->cow_write_count.fetch_add(1, std::memory_order_relaxed); try { @@ -2375,13 +2420,20 @@ void mo2_setattr(fuse_req_t req, fuse_ino_t ino, struct stat* attr, int to_set, } } - // COW only for backing files: copy to staging before truncating. - // Tracked files, staging files, and existing mod files are truncated - // in place. + // COW any non-staging source before truncating. Tracked files and already + // staged/overwrite files are truncated in place. const std::string stagedPath = ctx->overwrite->stagingPath(path); - if (targetIsBacking && - fs::path(target).lexically_normal().string() != - fs::path(stagedPath).lexically_normal().string()) { + const std::string overwritePath = ctx->overwrite->overwritePath(path); + const bool alreadyWritableTarget = + fs::path(target).lexically_normal().string() == + fs::path(stagedPath).lexically_normal().string() || + fs::path(target).lexically_normal().string() == + fs::path(overwritePath).lexically_normal().string(); + const bool trackedTarget = + ctx->tracked_writes != nullptr && + !ctx->tracked_writes->modFolderFor(path).empty() && + !targetIsBacking; + if (!alreadyWritableTarget && !trackedTarget) { try { if (targetIsBacking && ctx->backing_dir_fd >= 0) { target = ctx->overwrite->copyOnWriteFromFd(ctx->backing_dir_fd, path); |
