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:
committed by
Max Wilfinger
parent
35ef0a3fd3
commit
6a299fb293
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user