aboutsummaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-23 16:09:59 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-23 16:09:59 -0500
commit96a6ba196c72e955c3db822d93da38fce8cd7045 (patch)
tree3fcc40e939bd609c4cb7b84f05e85875dde64fc6 /libs
parent6b9f0876cfbf6f8c723c86ec941ee70933aa883f (diff)
Fix VFS rmdir, prefix/launch issues, add version + beta channel
- VFS: add mo2_rmdir + OverwriteManager::removeDirectory so Wrye Bash shutil.rmtree stops failing with WinError 267 (issue #47). - FileRenamer: case-insensitive fallback via new resolvePathCaseInsensitive walks each path component; bulk hide in Conflict tab no longer dies with a misleading "Input/output error" when DirectoryEntry normalised parent dir case (issue #54). formatSystemMessage stops aliasing Windows error code 5 to errno 5 (EIO). - Versioning: FLUORINE_VERSION_* + fluorine_build_info.h generated from CMake; Linux createVersionInfo returns Fluorine's version. About dialog shows Fluorine version + MO2 engine + commit. Beta builds stamp "<semver>B<yyyymmddhhmm>" (issue #51). - CI: ci.yml determines channel from ref (v* tags = stable, main = beta), publishes a rolling `beta` GitHub release whose body carries a machine-parseable fluorine-meta block (timestamp, commit) for the in-app updater. - Updater: new FluorineUpdater polls GitHub API on startup, compares against embedded metadata, notifies on new release without auto-install. Channel toggle piggybacks on the existing "beta versions" checkbox. - Bethesda plugins: determineMyGamesPath no longer creates Documents/My Games/<GameName> for every possible title at plugin load; only games with a detected install path get their dirs created (issue #55). - Starfield: dataDirectory() on Linux now points at <game>/Data instead of My Games/Starfield/Data so the single FUSE mount lands where SFSE loads plugins; My Games Data becomes a secondary symlinked mapping (issue #56). - Prefix resolution: prefer Fluorine config, then explicit fluorine/prefix_path, then legacy Settings/* keys (with a warning) so auto-detected Heroic/Bottles prefixes can't silently override a user- configured Fluorine prefix (issue #52). compatDataPathFromPrefix no longer hands Proton the wrong parent dir for plain wine prefixes. - xrandr: ensureXrandrInstalled back-fills the helper for existing SLR installs, prefix init runs it before wineboot, SLR wrap exposes xrandr-bin via --filesystem= and prepends PATH inside the container (issue #49). - Process tracking: drop the wineserver-as-last-resort fallback that made MO2 hang on Proton's session manager after game exit. Rescan the prefix for matching game executables instead (covers reparented launcher children like f4se_loader → Fallout4.exe). Unlock button now kills wineserver for the prefix (SIGTERM then SIGKILL on timeout). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs')
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.cpp34
-rw-r--r--libs/game_bethesda/src/gamebryo/gamegamebryo.h8
-rw-r--r--libs/game_bethesda/src/games/enderalse/gameenderalse.cpp4
-rw-r--r--libs/game_bethesda/src/games/fallout3/gamefallout3.cpp2
-rw-r--r--libs/game_bethesda/src/games/fallout4/gamefallout4.cpp2
-rw-r--r--libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp2
-rw-r--r--libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp2
-rw-r--r--libs/game_bethesda/src/games/fallout76/gamefallout76.cpp2
-rw-r--r--libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp4
-rw-r--r--libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp4
-rw-r--r--libs/game_bethesda/src/games/starfield/gamestarfield.cpp22
-rw-r--r--libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp4
-rw-r--r--libs/uibase/include/uibase/filesystemutilities.h14
-rw-r--r--libs/uibase/src/filesystemutilities.cpp57
-rw-r--r--libs/uibase/src/utility.cpp56
15 files changed, 188 insertions, 29 deletions
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
index 9d29dcf..fc3ba8a 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp
@@ -56,7 +56,7 @@ GameGamebryo::GameGamebryo() {}
void GameGamebryo::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath(gameName());
+ m_MyGamesPath = determineMyGamesPath(gameName(), !m_GamePath.isEmpty());
}
bool GameGamebryo::init(MOBase::IOrganizer* moInfo)
@@ -714,7 +714,8 @@ QString GameGamebryo::getSpecialPath(const QString& name)
}
#endif // _WIN32
-QString GameGamebryo::determineMyGamesPath(const QString& gameName)
+QString GameGamebryo::determineMyGamesPath(const QString& gameName,
+ bool createIfMissing)
{
const QString pattern = "%1/My Games/" + gameName;
@@ -781,21 +782,36 @@ QString GameGamebryo::determineMyGamesPath(const QString& gameName)
}
}
- // If no existing directory was found, try to create it in the configured prefix
- // so that the game launcher can populate it on first run.
+ // No existing directory found. By default we return the expected path
+ // (under the configured prefix) WITHOUT creating it — every Bethesda
+ // plugin constructs itself at startup, and pre-creating `My Games/<Game>`
+ // for every possible title (Fallout4, Oblivion, Morrowind, …) clutters
+ // the user's prefix with empty folders for games they don't have.
+ // See issue #55.
+ //
+ // Callers that actually need the directory (profile initialization,
+ // save writes, ini deployment) should mkpath on demand or pass
+ // createIfMissing=true explicitly.
if (!configuredPrefix.isEmpty()) {
const QString configuredDocs =
QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents");
const QString newPath = pattern.arg(configuredDocs);
- if (QDir().mkpath(newPath)) {
- MOBase::log::info("determineMyGamesPath: created '{}' for game '{}'", newPath,
- gameName);
+ if (createIfMissing) {
+ if (QDir().mkpath(newPath)) {
+ MOBase::log::info("determineMyGamesPath: created '{}' for game '{}'",
+ newPath, gameName);
+ return newPath;
+ }
+ } else {
+ // Return the expected path for reference; callers may check for
+ // existence before writing.
return newPath;
}
}
- MOBase::log::warn("determineMyGamesPath: could not find My Games path for '{}'",
- gameName);
+ MOBase::log::debug(
+ "determineMyGamesPath: no existing My Games path for '{}' (create=false)",
+ gameName);
#endif
return {};
diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.h b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
index 78ac1d4..c926c4e 100644
--- a/libs/game_bethesda/src/gamebryo/gamegamebryo.h
+++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.h
@@ -148,7 +148,13 @@ protected:
WORD getArch(QString const& program) const;
- static QString determineMyGamesPath(const QString& gameName);
+ // createIfMissing=false is the safe default: the per-game plugin ctor
+ // calls this for every installed Bethesda game and we don't want to
+ // spam `Documents/My Games/<GameName>` directories for games the user
+ // isn't actually managing (see issue #55). Pass true only when we
+ // know the game is installed and likely to be used.
+ static QString determineMyGamesPath(const QString& gameName,
+ bool createIfMissing = false);
static QString parseEpicGamesLocation(const QStringList& manifests);
diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp
index 26587b8..e2f8952 100644
--- a/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp
+++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp
@@ -55,7 +55,7 @@ void GameEnderalSE::detectGame()
{
m_GamePath = identifyGamePath();
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QString GameEnderalSE::identifyGamePath() const
@@ -99,7 +99,7 @@ void GameEnderalSE::setGamePath(const QString& path)
{
m_GamePath = path;
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QDir GameEnderalSE::savesDirectory() const
diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
index 75e1ea3..b0d3be7 100644
--- a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
+++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp
@@ -104,7 +104,7 @@ void GameFallout3::detectGame()
{
m_GamePath = identifyGamePath();
setGameVariant(identifyVariant());
- m_MyGamesPath = determineMyGamesPath("Fallout3");
+ m_MyGamesPath = determineMyGamesPath("Fallout3", !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFallout3::executables() const
diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
index 02d30cb..d1ba92f 100644
--- a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
+++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp
@@ -63,7 +63,7 @@ QString GameFallout4::gameName() const
void GameFallout4::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath("Fallout4");
+ m_MyGamesPath = determineMyGamesPath("Fallout4", !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFallout4::executables() const
diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
index 646bf1c..06a39bd 100644
--- a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
+++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp
@@ -65,7 +65,7 @@ QString GameFallout4London::gameName() const
void GameFallout4London::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath("Fallout4");
+ m_MyGamesPath = determineMyGamesPath("Fallout4", !m_GamePath.isEmpty());
}
QString GameFallout4London::identifyGamePath() const
diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
index 852bf7d..e0e5e38 100644
--- a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
+++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp
@@ -55,7 +55,7 @@ QString GameFallout4VR::gameName() const
void GameFallout4VR::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath("Fallout4VR");
+ m_MyGamesPath = determineMyGamesPath("Fallout4VR", !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFallout4VR::executables() const
diff --git a/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp
index bfeb507..c1348f3 100644
--- a/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp
+++ b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp
@@ -50,7 +50,7 @@ QString GameFallout76::gameName() const
void GameFallout76::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath(gameName());
+ m_MyGamesPath = determineMyGamesPath(gameName(), !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFallout76::executables() const
diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
index 7a147e5..f7b9413 100644
--- a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
+++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp
@@ -102,7 +102,7 @@ void GameFalloutNV::setGamePath(const QString& path)
{
m_GamePath = path;
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QDir GameFalloutNV::savesDirectory() const
@@ -137,7 +137,7 @@ void GameFalloutNV::detectGame()
{
m_GamePath = identifyGamePath();
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFalloutNV::executables() const
diff --git a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
index d0492af..377c65c 100644
--- a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
+++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp
@@ -58,7 +58,7 @@ void GameSkyrimSE::detectGame()
{
m_GamePath = identifyGamePath();
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QString GameSkyrimSE::identifyGamePath() const
@@ -110,7 +110,7 @@ void GameSkyrimSE::setGamePath(const QString& path)
{
m_GamePath = path;
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QDir GameSkyrimSE::savesDirectory() const
diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
index c6c211a..ebec948 100644
--- a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
+++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp
@@ -67,7 +67,7 @@ QString GameStarfield::gameName() const
void GameStarfield::detectGame()
{
m_GamePath = identifyGamePath();
- m_MyGamesPath = determineMyGamesPath("Starfield");
+ m_MyGamesPath = determineMyGamesPath("Starfield", !m_GamePath.isEmpty());
}
QString GameStarfield::identifyGamePath() const
@@ -77,16 +77,36 @@ QString GameStarfield::identifyGamePath() const
QDir GameStarfield::dataDirectory() const
{
+#ifdef _WIN32
+ // On Windows, USVFS hooks both the game-side Data folder (where SFSE
+ // loads plugins from) and My Games\Starfield\Data (where loose content
+ // lives) transparently, so MO2 can report the My Games path as primary.
QDir dataDir = documentsDirectory().absoluteFilePath("Data");
if (!dataDir.exists())
dataDir.mkdir(dataDir.path());
return documentsDirectory().absoluteFilePath("Data");
+#else
+ // On Linux we have a single FUSE mount, so the primary dataDirectory
+ // MUST be the game-install Data folder — that's where SFSE looks for
+ // plugins. If the mount lived under My Games/Starfield/Data, the
+ // base game Data folder would only see dangling symlinks and SFSE
+ // plugins would silently fail to load. See issue #56.
+ return gameDirectory().absoluteFilePath("Data");
+#endif
}
QMap<QString, QDir> GameStarfield::secondaryDataDirectories() const
{
QMap<QString, QDir> directories;
+#ifdef _WIN32
directories.insert("game_data", gameDirectory().absoluteFilePath("Data"));
+#else
+ // Primary is now gameDirectory/Data; My Games/Starfield/Data still
+ // needs to exist so the launcher and loose-file content work. Get it
+ // populated via the secondary-mapping symlink path.
+ directories.insert("documents_data",
+ documentsDirectory().absoluteFilePath("Data"));
+#endif
return directories;
}
diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
index e09ddf0..7ba105b 100644
--- a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
+++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp
@@ -126,7 +126,7 @@ void GameFalloutTTW::setGamePath(const QString& path)
{
m_GamePath = path;
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QDir GameFalloutTTW::savesDirectory() const
@@ -166,7 +166,7 @@ void GameFalloutTTW::detectGame()
{
m_GamePath = identifyGamePath();
checkVariants();
- m_MyGamesPath = determineMyGamesPath(gameDirectoryName());
+ m_MyGamesPath = determineMyGamesPath(gameDirectoryName(), !m_GamePath.isEmpty());
}
QList<ExecutableInfo> GameFalloutTTW::executables() const
diff --git a/libs/uibase/include/uibase/filesystemutilities.h b/libs/uibase/include/uibase/filesystemutilities.h
index a98ba7b..b492687 100644
--- a/libs/uibase/include/uibase/filesystemutilities.h
+++ b/libs/uibase/include/uibase/filesystemutilities.h
@@ -43,6 +43,20 @@ QDLLEXPORT bool validFileName(const QString& name);
*/
QDLLEXPORT QString resolveFileCaseInsensitive(const QString& path);
+/**
+ * @brief Case-insensitively resolve every component of an absolute path.
+ *
+ * Unlike resolveFileCaseInsensitive(), walks each directory component and
+ * matches it case-insensitively against the real filesystem, so a path like
+ * `mods/foo/meshes/dlc01/bar.nif` resolves correctly when the on-disk
+ * directory is `meshes/DLC01/`.
+ *
+ * On Windows, returns the cleaned path as-is.
+ * On Linux, returns the first walk that resolves each component; if any
+ * component has no case-insensitive match, returns the cleaned input path.
+ */
+QDLLEXPORT QString resolvePathCaseInsensitive(const QString& path);
+
} // namespace MOBase
#endif // FILESYSTEM_H
diff --git a/libs/uibase/src/filesystemutilities.cpp b/libs/uibase/src/filesystemutilities.cpp
index b027f5f..ba05699 100644
--- a/libs/uibase/src/filesystemutilities.cpp
+++ b/libs/uibase/src/filesystemutilities.cpp
@@ -95,4 +95,61 @@ QString resolveFileCaseInsensitive(const QString& path)
#endif
}
+QString resolvePathCaseInsensitive(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::cleanPath(path);
+#else
+ const QString clean = QDir::cleanPath(path);
+ if (QFileInfo::exists(clean)) {
+ return clean;
+ }
+
+ // Walk each component and match case-insensitively against the real
+ // filesystem. Stops as soon as a component has no case-insensitive match;
+ // returns the cleaned input in that case.
+ const QString prefix = clean.startsWith('/') ? QStringLiteral("/") : QString();
+ const QStringList parts =
+ clean.split('/', Qt::SkipEmptyParts);
+
+ QString current = prefix;
+ for (int i = 0; i < parts.size(); ++i) {
+ const QString& want = parts[i];
+ const QString next = current.isEmpty() ? want : current + '/' + want;
+
+ if (QFileInfo::exists(next)) {
+ current = next;
+ continue;
+ }
+
+ QDir dir(current.isEmpty() ? QStringLiteral(".") : current);
+ if (!dir.exists()) {
+ return clean;
+ }
+
+ const QDir::Filters filters = (i + 1 < parts.size())
+ ? (QDir::Dirs | QDir::NoDotAndDotDot |
+ QDir::Hidden | QDir::System)
+ : (QDir::Dirs | QDir::Files |
+ QDir::NoDotAndDotDot | QDir::Hidden |
+ QDir::System);
+
+ bool matched = false;
+ for (const QString& entry : dir.entryList(filters)) {
+ if (entry.compare(want, Qt::CaseInsensitive) == 0) {
+ current = current.isEmpty() ? entry : current + '/' + entry;
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ return clean;
+ }
+ }
+
+ return current;
+#endif
+}
+
} // namespace MOBase
diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp
index c0070c6..40774ae 100644
--- a/libs/uibase/src/utility.cpp
+++ b/libs/uibase/src/utility.cpp
@@ -19,6 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <uibase/utility.h>
+#include <uibase/filesystemutilities.h>
#include <uibase/log.h>
#include <uibase/report.h>
#ifndef _WIN32
@@ -44,10 +45,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#include <QtDebug>
#include <cerrno>
#include <cstring>
+#include <filesystem>
#include <format>
#include <iostream>
#include <memory>
#include <sstream>
+#include <system_error>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
@@ -532,18 +535,50 @@ namespace shell
Result Rename(const QFileInfo& src, const QFileInfo& dest, bool copyAllowed)
{
- if (QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath())) {
+ const QString srcPath = src.absoluteFilePath();
+ const QString destPath = dest.absoluteFilePath();
+
+ std::error_code ec;
+ std::filesystem::rename(srcPath.toStdString(), destPath.toStdString(), ec);
+ if (!ec) {
return Result::makeSuccess();
}
+#ifndef _WIN32
+ // Case-sensitivity fallback: the source path may have been computed from a
+ // case-normalized index (e.g. DirectoryEntry's lowercased tree), so try
+ // resolving each component against the real filesystem.
+ if (ec.value() == ENOENT) {
+ const QString resolved = resolvePathCaseInsensitive(srcPath);
+ if (resolved != srcPath && QFileInfo::exists(resolved)) {
+ std::error_code ec2;
+ std::filesystem::rename(resolved.toStdString(), destPath.toStdString(),
+ ec2);
+ if (!ec2) {
+ return Result::makeSuccess();
+ }
+ ec = ec2;
+ }
+ }
+#endif
+
if (copyAllowed) {
- if (QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath())) {
- QFile::remove(src.absoluteFilePath());
+ if (QFile::copy(srcPath, destPath)) {
+ QFile::remove(srcPath);
return Result::makeSuccess();
}
}
- return Result::makeFailure(ERROR_ACCESS_DENIED);
+ // Propagate the real errno text rather than a generic Windows code so the
+ // log line actually reflects what failed.
+ const int err = ec.value();
+ QString msg = QString::fromStdString(ec.message());
+ if (msg.isEmpty()) {
+ msg = QString("rename failed: errno=%1").arg(err);
+ }
+ return Result::makeFailure(err != 0 ? static_cast<DWORD>(err)
+ : ERROR_ACCESS_DENIED,
+ msg);
}
Result CreateDirectories(const QDir& dir)
@@ -1051,7 +1086,18 @@ std::wstring formatSystemMessage(DWORD id)
if (id == 0) {
return L"Success";
}
- // If it looks like an errno value (small numbers), use strerror
+
+ // Try the Windows-style mapping first: our callers use a mix of
+ // ERROR_ACCESS_DENIED/ERROR_FILE_NOT_FOUND/etc., which overlap the errno
+ // number space (e.g. Windows 5 == ACCESS_DENIED, but errno 5 == EIO). If
+ // formatError returns a known mapping, use it; otherwise fall back to
+ // strerror for real errno values.
+ const QString winMsg = shell::formatError(static_cast<int>(id));
+ if (!winMsg.startsWith(QStringLiteral("Unknown error "))) {
+ const std::string s = winMsg.toStdString();
+ return std::wstring(s.begin(), s.end());
+ }
+
if (id < 200) {
const char* msg = strerror(static_cast<int>(id));
if (msg) {