Core: Fix freecad when windows path has non ASCII characters (#28222)

* Core: Fix freecad when Windows username has non ASCII characters

* Update ApplicationDirectories.cpp

* Tests: Add tests of pathToString/stringToPath

Validates the fix for non-ASCII characters in filesystem paths (e.g.
Windows usernames with umlauts). Includes Windows-specific tests that
verify pathToString produces UTF-8 from wide-string paths and that
the naive fs::path::string() does not. On Linux these functions are
effectively a no-op, so the tests themselves are only really useful
on Windows.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Chris Hennes <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
(cherry picked from commit eabd052c17)
This commit is contained in:
PaddleStroke
2026-03-16 21:46:10 +01:00
committed by Max Wilfinger
parent 35ef0a3fd3
commit 6a299fb293
3 changed files with 111 additions and 6 deletions
+3 -1
View File
@@ -2512,7 +2512,9 @@ void processProgramOptions(const boost::program_options::variables_map& vm, std:
void Application::initConfig(int argc, char ** argv)
{
// find the home path....
mConfig["AppHomePath"] = ApplicationDirectories::findHomePath(argv[0]).string();
mConfig["AppHomePath"] = Base::FileInfo::pathToString(
ApplicationDirectories::findHomePath(argv[0])
);
// Version of the application extracted from SubWCRef into src/Build/Version.h
// We only set these keys if not yet defined. Therefore it suffices to search
+9 -5
View File
@@ -79,12 +79,16 @@ const fs::path& ApplicationDirectories::getTempPath() const {
return this->_temp;
}
fs::path ApplicationDirectories::getTempFileName(const std::string & filename) const {
fs::path ApplicationDirectories::getTempFileName(const std::string & filename) const
{
auto tempPath = Base::FileInfo::pathToString(getTempPath());
if (filename.empty()) {
return Base::FileInfo::getTempFileName(nullptr, tempPath.c_str());
return Base::FileInfo::stringToPath(Base::FileInfo::getTempFileName(nullptr, tempPath.c_str())
);
}
return Base::FileInfo::getTempFileName(filename.c_str(), tempPath.c_str());
return Base::FileInfo::stringToPath(
Base::FileInfo::getTempFileName(filename.c_str(), tempPath.c_str())
);
}
const fs::path& ApplicationDirectories::getUserCachePath() const
@@ -181,7 +185,7 @@ void ApplicationDirectories::configurePaths(std::map<std::string,std::string>& m
bool keepDeprecatedPaths = mConfig.contains("KeepDeprecatedPaths");
// std paths
_home = fs::path(mConfig.at("AppHomePath"));
_home = Base::FileInfo::stringToPath(mConfig.at("AppHomePath"));
mConfig["BinPath"] = mConfig.at("AppHomePath") + "bin" + PATHSEP;
mConfig["DocPath"] = mConfig.at("AppHomePath") + "doc" + PATHSEP;
@@ -297,7 +301,7 @@ void ApplicationDirectories::configureResourceDirectory(const std::map<std::stri
_resource = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path;
}
#else
_resource = fs::path(mConfig.at("AppHomePath"));
_resource = Base::FileInfo::stringToPath(mConfig.at("AppHomePath"));
#endif
}
+99
View File
@@ -2,6 +2,7 @@
#include <Base/FileInfo.h>
#include <Base/Stream.h>
#include <Base/TimeInfo.h>
#include <filesystem>
class FileInfoTest: public ::testing::Test
{
@@ -147,3 +148,101 @@ TEST_F(FileInfoTest, TestCopyFile)
EXPECT_TRUE(file.copyTo(copy.filePath().c_str()));
EXPECT_TRUE(copy.deleteFile());
}
// Tests for pathToString / stringToPath UTF-8 round-trip (PR #28222)
class FileInfoPathConversionTest: public ::testing::Test
{
};
TEST_F(FileInfoPathConversionTest, RoundTripAsciiPath)
{
std::string utf8 = "/some/simple/path";
auto fsPath = Base::FileInfo::stringToPath(utf8);
std::string result = Base::FileInfo::pathToString(fsPath);
EXPECT_EQ(result, utf8);
}
TEST_F(FileInfoPathConversionTest, RoundTripNonAsciiPath)
{
// German umlaut, common in Windows usernames (the exact bug scenario)
std::string utf8 = "/home/m\xc3\xbcller/Documents"; // müller in UTF-8
auto fsPath = Base::FileInfo::stringToPath(utf8);
std::string result = Base::FileInfo::pathToString(fsPath);
EXPECT_EQ(result, utf8);
}
TEST_F(FileInfoPathConversionTest, RoundTripChineseCharacters)
{
// CJK characters: 用户 (user) in UTF-8
std::string utf8 = "/home/\xe7\x94\xa8\xe6\x88\xb7/data";
auto fsPath = Base::FileInfo::stringToPath(utf8);
std::string result = Base::FileInfo::pathToString(fsPath);
EXPECT_EQ(result, utf8);
}
TEST_F(FileInfoPathConversionTest, RoundTripAccentedCharacters)
{
// French accented characters: café in UTF-8
std::string utf8 = "/tmp/caf\xc3\xa9/file.txt";
auto fsPath = Base::FileInfo::stringToPath(utf8);
std::string result = Base::FileInfo::pathToString(fsPath);
EXPECT_EQ(result, utf8);
}
TEST_F(FileInfoPathConversionTest, RoundTripEmptyString)
{
std::string utf8;
auto fsPath = Base::FileInfo::stringToPath(utf8);
std::string result = Base::FileInfo::pathToString(fsPath);
EXPECT_EQ(result, utf8);
}
TEST_F(FileInfoPathConversionTest, PathToStringPreservesUtf8)
{
// Construct a path from a wide string directly and verify pathToString produces valid UTF-8
std::filesystem::path p = Base::FileInfo::stringToPath("/tmp/\xc3\xa4\xc3\xb6\xc3\xbc"); // äöü
std::string result = Base::FileInfo::pathToString(p);
// Verify the UTF-8 bytes are preserved
EXPECT_NE(result.find("\xc3\xa4"), std::string::npos); // ä
EXPECT_NE(result.find("\xc3\xb6"), std::string::npos); // ö
EXPECT_NE(result.find("\xc3\xbc"), std::string::npos); // ü
}
TEST_F(FileInfoPathConversionTest, StringToPathProducesValidPath)
{
// Verify that stringToPath produces a path that can be appended to
std::string utf8 = "/home/\xc3\xbc\x73\x65r"; // üser
auto fsPath = Base::FileInfo::stringToPath(utf8);
auto child = fsPath / "subdir";
std::string childStr = Base::FileInfo::pathToString(child);
// The child path should contain both the parent with non-ASCII and the appended segment
EXPECT_NE(childStr.find("\xc3\xbc"), std::string::npos);
EXPECT_NE(childStr.find("subdir"), std::string::npos);
}
#ifdef _WIN32 // NOTE FC_OS_WIN32 is not available in the test code
TEST_F(FileInfoPathConversionTest, WidePathToUtf8)
{
// Simulate a path obtained from the Windows OS (e.g. GetModuleFileNameW), which arrives as a
// UTF-16 wide string. Verify pathToString encodes it as UTF-8.
// L"C:\\Users\\müller" -- ü is U+00FC
std::filesystem::path widePath(L"C:\\Users\\m\u00FCller\\Documents");
std::string result = Base::FileInfo::pathToString(widePath);
// Must contain the UTF-8 encoding of ü (0xC3 0xBC), not the ANSI mangled version
EXPECT_NE(result.find("\xc3\xbc"), std::string::npos);
EXPECT_NE(result.find("Documents"), std::string::npos);
}
TEST_F(FileInfoPathConversionTest, NaivePathStringLosesNonAscii)
{
// Demonstrate the actual bug: on Windows, fs::path::string() converts to the ANSI codepage,
// which mangles non-ASCII characters. This is what the old code did (before PR #28222) and why
// pathToString is needed.
std::filesystem::path widePath(L"C:\\Users\\m\u00FCller");
std::string naive = widePath.string(); // ANSI codepage on Windows
std::string safe = Base::FileInfo::pathToString(widePath); // UTF-8
// The naive .string() result will differ from the correct UTF-8 encoding
EXPECT_NE(naive, safe);
}
#endif