diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 8908d3f2..0e302c5c 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -1046,7 +1046,8 @@ mj_addFileVFS .. mujoco-include:: mj_addFileVFS -Add file to VFS, return 0: success, 1: full, 2: repeated name, -1: failed to load. +Add file to VFS. The directory argument is optional and can be NULL or empty. Returns 0 on success, 1 when VFS is full, +2 on name collision, or -1 when an internal error occurs. .. _mj_makeEmptyFileVFS: diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index 121e2b1b..21027e3f 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -22,6 +22,11 @@ this data structure. The common usage pattern is to first clear it with mj_defau mj_addFileVFS (which allocates memory buffers and loads the file content in memory), then call mj_loadXML or mj_loadModel, and then clear everything with mj_deleteVFS. +.. _mj_addFileVFS: + +Add file to VFS. The directory argument is optional and can be NULL or empty. Returns 0 on success, 1 when VFS is full, +2 on name collision, or -1 when an internal error occurs. + .. _Parseandcompile: The key function here is :ref:`mj_loadXML`. It invokes the built-in parser and compiler, and either returns a pointer to diff --git a/src/engine/engine_util_misc.c b/src/engine/engine_util_misc.c index 44dca4b4..ab382299 100644 --- a/src/engine/engine_util_misc.c +++ b/src/engine/engine_util_misc.c @@ -1177,6 +1177,51 @@ char* mju_strncpy(char *dst, const char *src, int n) { +// assemble full filename from directory and filename, return 0 on success +int mju_makefullname(char* full, size_t nfull, const char* dir, const char* file) { + int dirlen = (!dir) ? 0 : strlen(dir); + int filelen = (!file) ? 0 : strlen(file); + char* filepos = full + dirlen; + + // missing filename + if (!filelen) { + return -1; + } + + // no directory then just copy filename over + if (!dirlen) { + // make sure full has space + if (filelen >= nfull) { + return -1; + } + strcpy(full, file); + return 0; + } + + // make sure full has space + if (dirlen + filelen >= nfull) { + return -1; + } + + // dir doesn't end with a slash + if (dir[dirlen - 1] != '\\' && dir[dirlen - 1] != '/') { + // need extra space for forward slash + if ((dirlen + filelen + 1) >= nfull) { + return -1; + } + + // add forward slash + *filepos++ = '/'; + } + + // copy directory and file over + memcpy(full, dir, sizeof(char) * dirlen); + strcpy(filepos, file); + return 0; +} + + + // sigmoid function over 0<=x<=1 using quintic polynomial mjtNum mju_sigmoid(mjtNum x) { // fast return diff --git a/src/engine/engine_util_misc.h b/src/engine/engine_util_misc.h index a2b24372..6ac874fc 100644 --- a/src/engine/engine_util_misc.h +++ b/src/engine/engine_util_misc.h @@ -49,7 +49,7 @@ MJAPI mjtNum mju_muscleDynamics(mjtNum ctrl, mjtNum act, const mjtNum prm[3]); // all 3 semi-axes of a geom MJAPI void mju_geomSemiAxes(const mjModel* m, int geom_id, mjtNum semiaxes[3]); -//------------------------------ misclellaneous ---------------------------------------------------- +//------------------------------ miscellaneous ---------------------------------------------------- // convert contact force to pyramid representation MJAPI void mju_encodePyramid(mjtNum* pyramid, const mjtNum* force, @@ -130,6 +130,10 @@ MJAPI mjtNum mju_Halton(int index, int base); // call strncpy, then set dst[n-1] = 0 MJAPI char* mju_strncpy(char *dst, const char *src, int n); +// assemble full filename from directory and filename, return 0 on success +MJAPI int mju_makefullname(char* full, size_t nfull, + const char* dir, const char* file); + // sigmoid function over 0<=x<=1 using quintic polynomial MJAPI mjtNum mju_sigmoid(mjtNum x); diff --git a/src/engine/engine_vfs.c b/src/engine/engine_vfs.c index 5dcf1ad2..e4246bb7 100644 --- a/src/engine/engine_vfs.c +++ b/src/engine/engine_vfs.c @@ -15,7 +15,6 @@ #include "engine/engine_vfs.h" #include -#include #include "engine/engine_array_safety.h" #include "engine/engine_plugin.h" @@ -72,11 +71,8 @@ int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename) { // make full name char fullname[1000]; - if (directory) { - mjSTRNCPY(fullname, directory); - mjSTRNCAT(fullname, filename); - } else { - mjSTRNCPY(fullname, filename); + if (mju_makefullname(fullname, sizeof(fullname), directory, filename)) { + return -1; } // strip path diff --git a/test/engine/engine_util_misc_test.cc b/test/engine/engine_util_misc_test.cc index cc85335d..5181e67d 100644 --- a/test/engine/engine_util_misc_test.cc +++ b/test/engine/engine_util_misc_test.cc @@ -24,8 +24,10 @@ namespace mujoco { namespace { -using ::testing::HasSubstr; using ::testing::DoubleNear; +using ::testing::HasSubstr; +using ::testing::Ne; +using ::testing::StrEq; TEST_F(MujocoTest, PrintsMemoryWarning) { EXPECT_THAT(mju_warningText(mjWARN_CNSTRFULL, pow(2, 10)), @@ -127,6 +129,91 @@ TEST_F(MujocoTest, SmoothMuscleDynamics) { } } +TEST_F(MujocoTest, mju_makefullname) { + char buffer[1000]; + constexpr char path[] = "engine/testdata/"; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + ASSERT_THAT(buffer, StrEq("engine/testdata/file")); + EXPECT_THAT(n, 0); +} + +TEST_F(MujocoTest, mju_makefullname2) { + char buffer[1000]; + constexpr char path[] = "engine\\testdata\\"; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + ASSERT_THAT(buffer, StrEq("engine\\testdata\\file")); + EXPECT_THAT(n, 0); +} + + +TEST_F(MujocoTest, mju_makefullname_missingSlash) { + char buffer[1000]; + constexpr char path[] = "engine/testdata"; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + ASSERT_THAT(buffer, StrEq("engine/testdata/file")); + EXPECT_THAT(n, 0); +} + +TEST_F(MujocoTest, mju_makefullname_withoutDir) { + char buffer[1000]; + constexpr char *path = NULL; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + ASSERT_THAT(buffer, StrEq("file")); + EXPECT_THAT(n, 0); +} + +TEST_F(MujocoTest, mju_makefullname_withoutDir2) { + char buffer[1000]; + constexpr char path[] = ""; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + ASSERT_THAT(buffer, StrEq("file")); + EXPECT_THAT(n, 0); +} + +TEST_F(MujocoTest, mju_makefullname_error) { + char buffer[1000]; + constexpr char path[] = "engine/testdata"; + constexpr char *file = NULL; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + EXPECT_THAT(n, Ne(0)); +} + +TEST_F(MujocoTest, mju_makefullname_error2) { + char buffer[1000]; + constexpr char path[] = "engine/testdata"; + constexpr char file[] = ""; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + EXPECT_THAT(n, Ne(0)); +} + +TEST_F(MujocoTest, mju_makefullname_error3) { + char buffer[20]; + constexpr char path[] = "engine/testdata/"; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + EXPECT_THAT(n, Ne(0)); +} + +TEST_F(MujocoTest, mju_makefullname_error4) { + char buffer[20]; + constexpr char path[] = "engine/testdata"; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + EXPECT_THAT(n, Ne(0)); +} + +TEST_F(MujocoTest, mju_makefullname_error5) { + char buffer[4]; + constexpr char path[] = ""; + constexpr char file[] = "file"; + int n = mju_makefullname(buffer, sizeof(buffer), path, file); + EXPECT_THAT(n, Ne(0)); +} } // namespace } // namespace mujoco