diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index f0cf467f6b..393fa79eef 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -488,7 +488,7 @@ void mj_saveModel(const mjModel* m, const char* filename, void* buffer, int buff // open file for writing if no buffer if (!buffer) { - fp = fopen(filename, "wb"); + fp = mju_fopen(filename, "wb"); if (!fp) { mju_warning("Could not open file '%s'", filename); return; diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index a8f407d934..c3e395b304 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -540,7 +540,7 @@ void mj_printFormattedModel(const mjModel* m, const char* filename, const char* // get file FILE* fp; if (filename) { - fp = fopen(filename, "wt"); + fp = mju_fopen(filename, "wt"); } else { fp = stdout; } @@ -1229,7 +1229,7 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena // get file FILE* fp; if (filename) { - fp = fopen(filename, "wt"); + fp = mju_fopen(filename, "wt"); } else { fp = stdout; } @@ -1744,7 +1744,7 @@ void mj_printFormattedScene(const mjvScene* s, const char* filename, const char* // get file FILE* fp; if (filename) { - fp = fopen(filename, "wt"); + fp = mju_fopen(filename, "wt"); } else { fp = stdout; } diff --git a/src/engine/engine_util_errmem.c b/src/engine/engine_util_errmem.c index f44e2a24ae..00d76e0416 100644 --- a/src/engine/engine_util_errmem.c +++ b/src/engine/engine_util_errmem.c @@ -20,6 +20,10 @@ #include #include +#ifdef _WIN32 +#include +#endif + #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) #include #endif @@ -45,6 +49,37 @@ static inline void mju_alignedFree(void* ptr) { #endif } +//------------------------- UTF-8 file open -------------------------------------------------------- + +// open file with UTF-8 filename support +FILE* mju_fopen(const char* filename, const char* mode) { +#ifdef _WIN32 + // convert UTF-8 filename to wide string + int wlen = MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + int wmlen = MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); + if (wlen == 0 || wmlen == 0) { + return NULL; + } + + wchar_t* wfilename = (wchar_t*)malloc(wlen * sizeof(wchar_t)); + wchar_t* wmode = (wchar_t*)malloc(wmlen * sizeof(wchar_t)); + if (!wfilename || !wmode) { + free(wfilename); + free(wmode); + return NULL; + } + + MultiByteToWideChar(CP_UTF8, 0, filename, -1, wfilename, wlen); + MultiByteToWideChar(CP_UTF8, 0, mode, -1, wmode, wmlen); + + FILE* fp = _wfopen(wfilename, wmode); + free(wfilename); + free(wmode); + return fp; +#else + return fopen(filename, mode); +#endif +} //------------------------- default user handlers -------------------------------------------------- @@ -92,7 +127,7 @@ void _mjPRIVATE__set_tls_warning_fn(callback_fn h) { void mju_writeLog(const char* type, const char* msg) { time_t rawtime; struct tm timeinfo; - FILE* fp = fopen("MUJOCO_LOG.TXT", "a+t"); + FILE* fp = mju_fopen("MUJOCO_LOG.TXT", "a+t"); if (fp) { // get time time(&rawtime); diff --git a/src/engine/engine_util_errmem.h b/src/engine/engine_util_errmem.h index 10623c93d6..89246e3d94 100644 --- a/src/engine/engine_util_errmem.h +++ b/src/engine/engine_util_errmem.h @@ -82,6 +82,11 @@ MJAPI void mju_writeLog(const char* type, const char* msg); mju_error_raw(_errbuf); \ } +//------------------------------ file operations --------------------------------------------------- + +// open file with UTF-8 filename support (uses _wfopen on Windows) +MJAPI FILE* mju_fopen(const char* filename, const char* mode); + //------------------------------ malloc and free --------------------------------------------------- // allocate memory; byte-align on 8; pad size to multiple of 8 diff --git a/src/user/user_util.cc b/src/user/user_util.cc index 0099dbcc3d..9a9d74371b 100644 --- a/src/user/user_util.cc +++ b/src/user/user_util.cc @@ -33,6 +33,7 @@ #include #include "engine/engine_crossplatform.h" +#include "engine/engine_util_errmem.h" // workaround with locale bug on some MacOS machines @@ -1116,7 +1117,7 @@ std::string FilePath::StrLower() const { // read file into memory buffer std::vector FileToMemory(const char* filename) { - FILE* fp = fopen(filename, "rb"); + FILE* fp = mju_fopen(filename, "rb"); if (!fp) { return {}; } diff --git a/src/user/user_vfs.cc b/src/user/user_vfs.cc index c9e15660b7..4a8db4f519 100644 --- a/src/user/user_vfs.cc +++ b/src/user/user_vfs.cc @@ -16,6 +16,7 @@ #include #if defined(_WIN32) && !defined(__MINGW32__) +#include #define stat _stat #endif @@ -47,9 +48,29 @@ struct ResourceFileData { bool is_read = false; }; +// stat with UTF-8 path support on Windows +int mju_stat(const char* filename, struct stat* file_stat) { +#ifdef _WIN32 + int wlen = MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + if (wlen == 0) { + return -1; + } + wchar_t* wfilename = (wchar_t*)malloc(wlen * sizeof(wchar_t)); + if (!wfilename) { + return -1; + } + MultiByteToWideChar(CP_UTF8, 0, filename, -1, wfilename, wlen); + int result = _wstat(wfilename, file_stat); + free(wfilename); + return result; +#else + return stat(filename, file_stat); +#endif +} + int OpenFile(const char* filename, mjResource* resource) { struct stat file_stat; - if (stat(filename, &file_stat) == 0) { + if (mju_stat(filename, &file_stat) == 0) { ResourceFileData* data = new ResourceFileData(); resource->data = data; diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index 5fab047511..fc84ec4d68 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -26,6 +25,7 @@ #include #include #include "engine/engine_io.h" +#include "engine/engine_util_errmem.h" #include "user/user_resource.h" #include "xml/xml.h" #include "xml/xml_global.h" @@ -76,7 +76,7 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, int mj_saveLastXML(const char* filename, const mjModel* m, char* error, int error_sz) { FILE *fp = stdout; if (filename != nullptr && filename[0] != '\0') { - fp = fopen(filename, "w"); + fp = mju_fopen(filename, "w"); if (!fp) { mjCopyError(error, "File not found", error_sz); return 0; @@ -114,10 +114,11 @@ int mj_printSchema(const char* filename, char* buffer, int buffer_sz, int flg_ht // filename given: write to file if (filename) { - std::ofstream file; - file.open(filename); - file << str.str(); - file.close(); + FILE* file = mju_fopen(filename, "w"); + if (file) { + fputs(str.str().c_str(), file); + fclose(file); + } } // buffer given: write to buffer @@ -178,10 +179,13 @@ int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz) return -1; } - std::ofstream file; - file.open(filename); - file << result; - file.close(); + FILE* file = mju_fopen(filename, "w"); + if (!file) { + mjCopyError(error, "Could not open file for writing", error_sz); + return -1; + } + fputs(result.c_str(), file); + fclose(file); return 0; } diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index b224a372e4..5dbf9f6c83 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -222,9 +222,14 @@ void mjCopyError(char* dst, const char* src, int maxlen) { } void mju_getXMLDependencies(const char* filename, mjStringVec* dependencies) { - // load XML file or parse string + // load XML file tinyxml2::XMLDocument doc; - doc.LoadFile(filename); + FILE* fp = mju_fopen(filename, "rb"); + if (!fp) { + mju_error("Could not open XML file '%s'", filename); + } + doc.LoadFile(fp); + fclose(fp); // error checking if (doc.Error()) { diff --git a/test/engine/engine_util_errmem_test.cc b/test/engine/engine_util_errmem_test.cc index 1b398f5dd0..92cbebb836 100644 --- a/test/engine/engine_util_errmem_test.cc +++ b/test/engine/engine_util_errmem_test.cc @@ -14,7 +14,9 @@ // Tests for engine/engine_util_errmem.c. +#include #include +#include // NOLINT #include #include @@ -140,5 +142,81 @@ TEST_F(MujocoErrorAndWarningTest, MjuErrorInternal) { EXPECT_EQ(std::string(ErrorMessageBuffer()), funcname + ": foobar 123"); } +TEST(MjuFopenTest, AsciiPath) { + std::filesystem::path filepath = + std::filesystem::temp_directory_path() / "mju_fopen_ascii_test.txt"; + const char* content = "hello mujoco"; + + // write + FILE* fp = mju_fopen(filepath.string().c_str(), "w"); + ASSERT_NE(fp, nullptr); + fputs(content, fp); + fclose(fp); + + // read back + fp = mju_fopen(filepath.string().c_str(), "r"); + ASSERT_NE(fp, nullptr); + char buf[64] = {0}; + fgets(buf, sizeof(buf), fp); + fclose(fp); + + EXPECT_STREQ(buf, content); + std::filesystem::remove(filepath); +} + +TEST(MjuFopenTest, NonexistentFileReturnsNull) { + std::filesystem::path filepath = + std::filesystem::temp_directory_path() / "mju_fopen_nonexistent_12345.txt"; + // make sure it doesn't exist + std::filesystem::remove(filepath); + + FILE* fp = mju_fopen(filepath.string().c_str(), "r"); + EXPECT_EQ(fp, nullptr); +} + +// helper: get UTF-8 string from a filesystem path +std::string PathToUtf8(const std::filesystem::path& p) { + auto u8str = p.u8string(); + return std::string(reinterpret_cast(u8str.data()), u8str.size()); +} + +TEST(MjuFopenTest, Utf8Path) { + // create a temp directory with non-ASCII characters, here im using Japanese + std::filesystem::path utf8_dir = + std::filesystem::temp_directory_path() / u8"mju_fopen_テスト"; + + std::error_code ec; + std::filesystem::create_directories(utf8_dir, ec); + if (ec) { + GTEST_SKIP() << "Could not create directory with UTF-8 name: " + << ec.message(); + } + + std::filesystem::path filepath = utf8_dir / u8"テスト_file.txt"; + std::string filepath_u8 = PathToUtf8(filepath); + const char* content = "utf8 content test"; + + // write using mju_fopen + FILE* fp = mju_fopen(filepath_u8.c_str(), "w"); + if (!fp) { + std::filesystem::remove_all(utf8_dir); + GTEST_SKIP() << "mju_fopen could not create file with UTF-8 path " + "(filesystem may not support UTF-8)"; + } + fputs(content, fp); + fclose(fp); + + // read back using mju_fopen + fp = mju_fopen(filepath_u8.c_str(), "r"); + ASSERT_NE(fp, nullptr) << "mju_fopen failed to open existing UTF-8 path"; + char buf[64] = {0}; + fgets(buf, sizeof(buf), fp); + fclose(fp); + + EXPECT_STREQ(buf, content); + + std::filesystem::remove_all(utf8_dir); +} + } // namespace } // namespace mujoco