Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/engine/engine_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/engine/engine_print.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
37 changes: 36 additions & 1 deletion src/engine/engine_util_errmem.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
#include <stdarg.h>
#include <time.h>

#ifdef _WIN32
#include <windows.h>
#endif

#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
#include <unistd.h>
#endif
Expand All @@ -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 --------------------------------------------------

Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/engine/engine_util_errmem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/user/user_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#include <mujoco/mujoco.h>
#include "engine/engine_crossplatform.h"
#include "engine/engine_util_errmem.h"


// workaround with locale bug on some MacOS machines
Expand Down Expand Up @@ -1116,7 +1117,7 @@ std::string FilePath::StrLower() const {

// read file into memory buffer
std::vector<uint8_t> FileToMemory(const char* filename) {
FILE* fp = fopen(filename, "rb");
FILE* fp = mju_fopen(filename, "rb");
if (!fp) {
return {};
}
Expand Down
23 changes: 22 additions & 1 deletion src/user/user_vfs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <sys/stat.h>
#if defined(_WIN32) && !defined(__MINGW32__)
#include <windows.h>
#define stat _stat
#endif

Expand Down Expand Up @@ -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;

Expand Down
24 changes: 14 additions & 10 deletions src/xml/xml_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
#include <array>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <functional>
#include <memory>
#include <sstream>
Expand All @@ -26,6 +25,7 @@
#include <mujoco/mjmodel.h>
#include <mujoco/mjspec.h>
#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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
9 changes: 7 additions & 2 deletions src/xml/xml_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
78 changes: 78 additions & 0 deletions test/engine/engine_util_errmem_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

// Tests for engine/engine_util_errmem.c.

#include <cstdio>
#include <cstring>
#include <filesystem> // NOLINT
#include <string>

#include <gtest/gtest.h>
Expand Down Expand Up @@ -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<const char*>(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
Loading