diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 373593c7..66566fe7 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -16,7 +16,9 @@ #include +#include #include +#include #include #include #include @@ -84,6 +86,22 @@ constexpr auto XArrayShapeImpl(const std::string_view dim1_str) { inline std::size_t NConMax(const mjData* d) { return d->narena / sizeof(mjContact); } + +// strip path prefix from filename and make lowercase +std::string StripPath(const char* name) { + std::string filename(name); + size_t start = filename.find_last_of("/\\"); + + // get name without path + if (start != std::string::npos) { + filename = filename.substr(start + 1, filename.size() - start - 1); + } + + // make lowercase + std::transform(filename.begin(), filename.end(), filename.begin(), + [](unsigned char c) { return std::tolower(c); }); + return filename; +} } // namespace // ==================== MJOPTION =============================================== @@ -323,8 +341,9 @@ static raw::MjModel* LoadModelFileImpl( mj_defaultVFS(&vfs); vfs_ptr = &vfs; for (const auto& asset : assets) { + std::string buffer_name = StripPath(asset.name); const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( - vfs_ptr, asset.name, asset.content, asset.content_size); + vfs_ptr, buffer_name.c_str(), asset.content, asset.content_size); if (vfs_error) { throw py::value_error("assets dict is too big"); } diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 1662fbd8..5dfc6943 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -900,12 +900,12 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, char* error, int error_sz) { } // load resource - std::string filename = mjuu_combinePaths(mjs_getString(model->spec.modelfiledir), - mjs_getString(model->spec.meshdir), file); + std::string filename = mjuu_combinePaths(mjs_getString(model->spec.meshdir), file); mjResource* resource = nullptr; try { - resource = mjCBase::LoadResource(filename, 0); + resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir), + filename, 0); } catch (mjCError err) { return comperr(error, err.message, error_sz); } @@ -999,12 +999,11 @@ bool mjCFlexcomp::MakeGMSH(mjCModel* model, char* error, int error_sz) { } // open resource - std::string filename = mjuu_combinePaths(mjs_getString(model->spec.modelfiledir), - mjs_getString(model->spec.meshdir), file); mjResource* resource = nullptr; - try { - resource = mjCBase::LoadResource(filename, 0); + std::string filename = mjuu_combinePaths(mjs_getString(model->spec.meshdir), file); + resource = mjCBase::LoadResource(mjs_getString(model->spec.modelfiledir), + filename, 0); } catch (mjCError err) { return comperr(error, err.message, error_sz); } diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 1b2d1022..0d455eb9 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -402,8 +402,8 @@ void mjCMesh::Compile(const mjVFS* vfs) { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "model/stl") { @@ -2095,8 +2095,8 @@ void mjCSkin::Compile(const mjVFS* vfs) { throw mjCError(this, "Unknown skin file type: %s", file_.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { LoadSKN(resource); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index c647f4b3..9ff645b2 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -701,10 +701,12 @@ void mjCBase::NameSpace(const mjCModel* m) { // load resource if found (fallback to OS filesystem) -mjResource* mjCBase::LoadResource(std::string filename, const mjVFS* vfs) { +mjResource* mjCBase::LoadResource(const std::string& modelfiledir, + const std::string& filename, + const mjVFS* vfs) { // try reading from provided VFS or fallback to OS filesystem std::array error; - mjResource* resource = mju_openResource(filename.c_str(), vfs, + mjResource* resource = mju_openResource(modelfiledir.c_str(), filename.c_str(), vfs, error.data(), error.size()); if (!resource) { throw mjCError(nullptr, "%s", error.data()); @@ -3159,8 +3161,8 @@ void mjCHField::Compile(const mjVFS* vfs) { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->meshdir_, file_); - mjResource* resource = LoadResource(filename, vfs); + std::string filename = mjuu_combinePaths(model->meshdir_, file_); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "image/png") { @@ -3594,7 +3596,7 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } - mjResource* resource = LoadResource(filename, vfs); + mjResource* resource = LoadResource(model->modelfiledir_, filename, vfs); try { if (asset_type == "image/png") { @@ -3797,7 +3799,7 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { } // make filename - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->texturedir_, cubefiles_[i]); + std::string filename = mjuu_combinePaths(model->texturedir_, cubefiles_[i]); // load PNG or custom unsigned int w, h; @@ -3895,7 +3897,7 @@ void mjCTexture::Compile(const mjVFS* vfs) { } // make filename - std::string filename = mjuu_combinePaths(model->modelfiledir_, model->texturedir_, file_); + std::string filename = mjuu_combinePaths(model->texturedir_, file_); // dispatch if (type==mjTEXTURE_2D) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 4e508b04..18d9b886 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -181,7 +181,8 @@ class mjCBase : public mjCBase_ { public: // load resource if found (fallback to OS filesystem) - static mjResource* LoadResource(std::string filename, const mjVFS* vfs); + static mjResource* LoadResource(const std::string& modelfiledir, + const std::string& filename, const mjVFS* vfs); // Get and sanitize content type from raw_text if not empty, otherwise parse // content type from resource_name; throw on failure diff --git a/src/user/user_resource.cc b/src/user/user_resource.cc index 805a3156..980d7ee9 100644 --- a/src/user/user_resource.cc +++ b/src/user/user_resource.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) @@ -117,8 +118,8 @@ int FileModified(const mjResource* resource, const char*timestamp) { // open the given resource; if the name doesn't have a prefix matching with a // resource provider, then the OS filesystem is used -mjResource* mju_openResource(const char* name, const mjVFS* vfs, - char* error, size_t nerror) { +mjResource* mju_openResource(const char* dir, const char* name, + const mjVFS* vfs, char* error, size_t nerror) { // no error so far if (error) { error[0] = '\0'; @@ -136,8 +137,10 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, // clear out resource memset(resource, 0, sizeof(mjResource)); - // copy name - resource->name = (char*) mju_malloc(sizeof(char) * (strlen(name) + 1)); + // make space for filename + std::string fullname = mjuu_combinePaths(dir, name); + std::size_t n = fullname.size(); + resource->name = (char*) mju_malloc(sizeof(char) * (n + 1)); if (resource->name == nullptr) { if (error) { strncpy(error, "could not allocate memory", nerror); @@ -146,10 +149,11 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, mju_closeResource(resource); return nullptr; } - memcpy(resource->name, name, sizeof(char) * (strlen(name) + 1)); // first priority is to check the VFS if (vfs != nullptr) { + memcpy(resource->name, name, + sizeof(char) * (std::strlen(name) + 1)); const mjpResourceProvider* provider = GetVfsResourceProvider(); resource->data = (void*) vfs; resource->provider = provider; @@ -158,8 +162,11 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, } } + // copy full path over + memcpy(resource->name, fullname.c_str(), sizeof(char) * (n + 1)); + // find provider based off prefix of name - const mjpResourceProvider* provider = mjp_getResourceProvider(name); + const mjpResourceProvider* provider = mjp_getResourceProvider(resource->name); if (provider != nullptr) { resource->provider = provider; resource->data = nullptr; @@ -170,7 +177,7 @@ mjResource* mju_openResource(const char* name, const mjVFS* vfs, if (error) { snprintf(error, nerror, "could not open '%s'" "using a resource provider matching prefix '%s'", - name, provider->prefix); + resource->name, provider->prefix); } mju_closeResource(resource); diff --git a/src/user/user_resource.h b/src/user/user_resource.h index e64874cb..82a70ed6 100644 --- a/src/user/user_resource.h +++ b/src/user/user_resource.h @@ -18,8 +18,6 @@ #define MUJOCO_SRC_ENGINE_ENGINE_RESOURCE_H_ #include -#include -#include #include #include @@ -30,8 +28,8 @@ extern "C" { // open the given resource; if the name doesn't have a prefix matching with a // resource provider, then the OS filesystem is used -MJAPI mjResource* mju_openResource(const char* name, const mjVFS* vfs, - char* error, std::size_t nerror); +MJAPI mjResource* mju_openResource(const char* dir, const char* name, + const mjVFS* vfs, char* error, size_t nerror); // close the given resource; no-op if resource is NULL MJAPI void mju_closeResource(mjResource* resource); diff --git a/src/user/user_vfs.cc b/src/user/user_vfs.cc index 8bc484a2..02c8829d 100644 --- a/src/user/user_vfs.cc +++ b/src/user/user_vfs.cc @@ -142,9 +142,13 @@ int Open(mjResource* resource) { const VFS* cvfs = GetVFSImpl(vfs); const VFSFile* file = cvfs->GetFile(StripPath(resource->name)); if (file == nullptr) { - return 0; + file = cvfs->GetFile(FilePath(resource->name)); + if (file == nullptr) { + return 0; + } } + resource->data = (void*) file; resource->timestamp[0] = '\0'; if (file->filestamp) { mju_encodeBase64(resource->timestamp, (uint8_t*) &file->filestamp, @@ -160,8 +164,7 @@ int Read(mjResource* resource, const void** buffer) { return -1; } - const VFS* vfs = GetVFSImpl(static_cast(resource->data)); - const VFSFile* file = vfs->GetFile(StripPath(resource->name)); + const VFSFile* file = static_cast(resource->data); if (file == nullptr) { *buffer = nullptr; return -1; @@ -193,8 +196,7 @@ int Modified(const mjResource* resource, const char* timestamp) { if (!filestamp) return 3; // no hash (assume modified) if (resource) { - const VFS* cvfs = GetVFSImpl(static_cast(resource->data)); - const VFSFile* file = cvfs->GetFile(StripPath(resource->name)); + const VFSFile* file = static_cast(resource->data); if (file == nullptr) return 4; // missing file (assume modified) if (!file->filestamp) return 5; // missing filestamp (assume modified) @@ -245,7 +247,7 @@ int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, std::vector inbuffer; VFS* cvfs = GetVFSImpl(vfs); VFSFile* file; - if (!(file = cvfs->AddFile(StripPath(name), std::move(inbuffer), 0))) { + if (!(file = cvfs->AddFile(FilePath(name), std::move(inbuffer), 0))) { return 2; // AddFile failed, repeated name } file->filedata.reserve(nbuffer); @@ -256,7 +258,10 @@ int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, // delete file from VFS, return 0: success, -1: not found in VFS int mj_deleteFileVFS(mjVFS* vfs, const char* filename) { VFS* cvfs = GetVFSImpl(vfs); - return cvfs->DeleteFile(StripPath(filename)); + if (cvfs->DeleteFile(StripPath(filename))) { + return cvfs->DeleteFile(FilePath(filename)); + } + return 0; } // delete all files from VFS diff --git a/src/xml/xml.cc b/src/xml/xml.cc index 8459902f..e299cea2 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -156,8 +156,8 @@ void IncludeXML(mjXReader& reader, XMLElement* elem, } // get filename - auto file_attr = mjXUtil::ReadAttrFile(elem, "file", reader.ModelFileDir(), - true); + auto file_attr = mjXUtil::ReadAttrFile(elem, "file", vfs, + reader.ModelFileDir(), true); if (!file_attr.has_value()) { throw mjXError(elem, "Include element missing file attribute"); } @@ -171,17 +171,17 @@ void IncludeXML(mjXReader& reader, XMLElement* elem, // TODO: b/325905702 - We have a messy wrapper here to remain backwards // compatible, which will be removed in the near future. - FilePath fullname = reader.ModelFileDir() + filename; - // legacy behavior: try to load in top level directory std::array error; - mjResource *resource = mju_openResource(fullname.c_str(), vfs, + mjResource *resource = mju_openResource(reader.ModelFileDir().c_str(), + filename.c_str(), vfs, error.data(), error.size()); if (resource == nullptr) { // new behavior: try to load in relative directory if (!filename.IsAbs()) { - fullname = dir + filename; - resource = mju_openResource(fullname.c_str(), vfs, error.data(), error.size()); + FilePath fullname = dir + filename; + resource = mju_openResource(reader.ModelFileDir().c_str(), + fullname.c_str(), vfs, error.data(), error.size()); } } @@ -195,7 +195,6 @@ void IncludeXML(mjXReader& reader, XMLElement* elem, int ninclude_dir = 0; mju_getResourceDir(resource, &include_dir, &ninclude_dir); FilePath next_dir = FilePath(std::string(include_dir, ninclude_dir)); - next_dir = dir + next_dir; elem->SetAttribute("dir", next_dir.c_str()); const char* xmlstring = nullptr; @@ -285,7 +284,7 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs, // get data source const char* xmlstring = nullptr; std::array rerror; - mjResource* resource = mju_openResource(filename, vfs, + mjResource* resource = mju_openResource("", filename, vfs, rerror.data(), rerror.size()); if (resource == nullptr) { std::snprintf(error, nerror, "ParseXML: %s", rerror.data()); @@ -352,7 +351,7 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs, std::unordered_set included = {filename}; mjXReader parser; parser.SetModelFileDir(mjs_getString(spec->modelfiledir)); - IncludeXML(parser, root, parser.ModelFileDir(), vfs, included); + IncludeXML(parser, root, FilePath(), vfs, included); // parse MuJoCo model parser.SetModel(spec); diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index 2107a80e..31deeec8 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -192,7 +192,7 @@ int mj_printSchema(const char* filename, char* buffer, int buffer_sz, int flg_ht // load model from binary MJB resource mjModel* mj_loadModel(const char* filename, const mjVFS* vfs) { std::array error; - mjResource* resource = mju_openResource(filename, vfs, + mjResource* resource = mju_openResource("", filename, vfs, error.data(), error.size()); if (resource == nullptr) { mju_warning("%s", error.data()); diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 3a87a939..2dc7138b 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -893,7 +893,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { readingdefaults = true; for (XMLElement* section = FirstChildElement(root, "default"); section; section = NextSiblingElement(section, "default")) { - Default(section, nullptr); + Default(section, nullptr, vfs); } readingdefaults = false; @@ -919,7 +919,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { for (XMLElement* section = FirstChildElement(root, "deformable"); section; section = NextSiblingElement(section, "deformable")) { - Deformable(section); + Deformable(section, vfs); } for (XMLElement* section = FirstChildElement(root, "equality"); section; @@ -949,7 +949,7 @@ void mjXReader::Parse(XMLElement* root, const mjVFS* vfs) { for (XMLElement* section = FirstChildElement(root, "worldbody"); section; section = NextSiblingElement(section, "worldbody")) { - Body(section, mjs_findBody(spec, "world"), nullptr); + Body(section, mjs_findBody(spec, "world"), nullptr, vfs); } } @@ -1389,7 +1389,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjsFlex* pflex) { // mesh element parser -void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { +void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh, const mjVFS* vfs) { int n; string text, name, content_type; @@ -1400,7 +1400,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { if (ReadAttrTxt(elem, "content_type", content_type)) { *pmesh->content_type = content_type; } - auto file = ReadAttrFile(elem, "file", MeshDir()); + auto file = ReadAttrFile(elem, "file", vfs, MeshDir()); if (file) { mjs_setString(pmesh->file, file->c_str()); } @@ -1461,7 +1461,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjsMesh* pmesh) { // skin element parser -void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin) { +void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin, const mjVFS* vfs) { string text, name, material; float data[4]; @@ -1469,7 +1469,7 @@ void mjXReader::OneSkin(XMLElement* elem, mjsSkin* pskin) { if (ReadAttrTxt(elem, "name", name)) { mjs_setString(pskin->name, name.c_str()); } - auto file = ReadAttrFile(elem, "file", AssetDir()); + auto file = ReadAttrFile(elem, "file", vfs, AssetDir()); if (file.has_value()) { mjs_setString(pskin->file, file->c_str()); } @@ -2536,7 +2536,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjsBody* pbody, mjsDefault* def) // make flexcomp -void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody) { +void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody, const mjVFS* vfs) { string text, material; int n; @@ -2554,7 +2554,7 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* pbody) { ReadAttr(elem, "scale", 3, fcomp.scale, text); ReadAttr(elem, "mass", 1, &fcomp.mass, text); ReadAttr(elem, "inertiabox", 1, &fcomp.inertiabox, text); - auto maybe_file = ReadAttrFile(elem, "file", modelfiledir_); + auto maybe_file = ReadAttrFile(elem, "file", vfs, modelfiledir_); if (maybe_file.has_value()) { fcomp.file = std::move(maybe_file.value().Str()); } else { @@ -2690,7 +2690,7 @@ void mjXReader::OnePlugin(XMLElement* elem, mjsPlugin* plugin) { //------------------ MJCF-specific sections -------------------------------------------------------- // default section parser -void mjXReader::Default(XMLElement* section, const mjsDefault* def) { +void mjXReader::Default(XMLElement* section, const mjsDefault* def, const mjVFS* vfs) { XMLElement* elem; string text, name; @@ -2721,7 +2721,7 @@ void mjXReader::Default(XMLElement* section, const mjsDefault* def) { name = elem->Value(); // read mesh - if (name=="mesh") OneMesh(elem, def->mesh); + if (name=="mesh") OneMesh(elem, def->mesh, vfs); // read material else if (name=="material") OneMaterial(elem, def->material); @@ -2775,7 +2775,7 @@ void mjXReader::Default(XMLElement* section, const mjsDefault* def) { // read default if (name=="default") { - Default(elem, def); + Default(elem, def, vfs); } // advance @@ -3136,7 +3136,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjs_setString(ptex->content_type, content_type.c_str()); } - auto file = ReadAttrFile(elem, "file", TextureDir()); + auto file = ReadAttrFile(elem, "file", vfs, TextureDir()); if (file.has_value()) { mjs_setString(ptex->file, file->c_str()); } @@ -3182,7 +3182,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { "fileup", "filedown", "filefront", "fileback"}; for (int i = 0; i < cubefiles.size(); i++) { - auto maybe_file = ReadAttrFile(elem, cubefile_names[i].c_str(), + auto maybe_file = ReadAttrFile(elem, cubefile_names[i].c_str(), vfs, TextureDir()); if (maybe_file.has_value()) { cubefiles[i] = maybe_file.value().Str(); @@ -3204,14 +3204,14 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { else if (name=="mesh") { // create mesh and parse mjsMesh* pmesh = mjs_addMesh(spec, def); - OneMesh(elem, pmesh); + OneMesh(elem, pmesh, vfs); } // skin sub-element... deprecate ??? else if (name=="skin") { // create skin and parse mjsSkin* pskin = mjs_addSkin(spec); - OneSkin(elem, pskin); + OneSkin(elem, pskin, vfs); } // hfield sub-element @@ -3230,7 +3230,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (ReadAttrTxt(elem, "content_type", content_type)) { mjs_setString(phf->content_type, content_type.c_str()); } - auto file = ReadAttrFile(elem, "file", AssetDir()); + auto file = ReadAttrFile(elem, "file", vfs, AssetDir()); if (file.has_value()) { mjs_setString(phf->file, file->c_str()); } @@ -3274,7 +3274,7 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { // model sub-element else if (name=="model") { - auto filename = modelfiledir_ + ReadAttrFile(elem, "file").value(); + auto filename = modelfiledir_ + ReadAttrFile(elem, "file", vfs).value(); // parse the child std::array error; @@ -3301,7 +3301,8 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { // body/world section parser; recursive -void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { +void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame, + const mjVFS* vfs) { string text, name; XMLElement* elem; int n; @@ -3427,7 +3428,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { // flexcomp sub-element else if (name=="flexcomp") { // parse flexcomp - OneFlexcomp(elem, pbody); + OneFlexcomp(elem, pbody, vfs); } // frame sub-element @@ -3459,7 +3460,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { ReadQuat(elem, "quat", pframe->quat, text); ReadAlternative(elem, pframe->alt); - Body(elem, pbody, pframe); + Body(elem, pbody, pframe, vfs); } // replicate sub-element @@ -3518,7 +3519,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { UpdateString(suffix, count, i); // process subtree - Body(elem, subtree, pframe); + Body(elem, subtree, pframe, vfs); // attach to parent if (mjs_attachFrame(pbody, pframe, /*prefix=*/"", suffix.c_str()) != 0) { @@ -3577,7 +3578,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* pbody, mjsFrame* frame) { mjs_setFrame(pchild->element, frame); // make recursive call - Body(elem, pchild, nullptr); + Body(elem, pchild, nullptr, vfs); } // attachment @@ -3695,7 +3696,7 @@ void mjXReader::Equality(XMLElement* section) { // deformable section parser -void mjXReader::Deformable(XMLElement* section) { +void mjXReader::Deformable(XMLElement* section, const mjVFS* vfs) { string name; XMLElement* elem; @@ -3722,7 +3723,7 @@ void mjXReader::Deformable(XMLElement* section) { else if (name=="skin") { // create skin and parse mjsSkin* pskin = mjs_addSkin(spec); - OneSkin(elem, pskin); + OneSkin(elem, pskin, vfs); } // advance to next element diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index 4d622f5a..cfa87961 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -49,16 +49,17 @@ class mjXReader : public mjXBase { private: // XML section specific to MJCF - void Default(tinyxml2::XMLElement* section, const mjsDefault* def); // default section + void Default(tinyxml2::XMLElement* section, const mjsDefault* def, + const mjVFS* vfs); // default section void Extension(tinyxml2::XMLElement* section); // extension section void Custom(tinyxml2::XMLElement* section); // custom section void Visual(tinyxml2::XMLElement* section); // visual section void Statistic(tinyxml2::XMLElement* section); // statistic section void Asset(tinyxml2::XMLElement* section, const mjVFS* vfs); // asset section void Body(tinyxml2::XMLElement* section, mjsBody* pbody, - mjsFrame* pframe); // body/world section + mjsFrame* pframe, const mjVFS* vfs); // body/world section void Contact(tinyxml2::XMLElement* section); // contact section - void Deformable(tinyxml2::XMLElement* section); // deformable section + void Deformable(tinyxml2::XMLElement* section, const mjVFS* vfs); // deformable section void Equality(tinyxml2::XMLElement* section); // equality section void Tendon(tinyxml2::XMLElement* section); // tendon section void Actuator(tinyxml2::XMLElement* section); // actuator section @@ -67,8 +68,8 @@ class mjXReader : public mjXBase { // single element parsers, used in defaults and main body void OneFlex(tinyxml2::XMLElement* elem, mjsFlex* pflex); - void OneMesh(tinyxml2::XMLElement* elem, mjsMesh* pmesh); - void OneSkin(tinyxml2::XMLElement* elem, mjsSkin* pskin); + void OneMesh(tinyxml2::XMLElement* elem, mjsMesh* pmesh, const mjVFS* vfs); + void OneSkin(tinyxml2::XMLElement* elem, mjsSkin* pskin, const mjVFS* vfs); void OneMaterial(tinyxml2::XMLElement* elem, mjsMaterial* pmaterial); void OneJoint(tinyxml2::XMLElement* elem, mjsJoint* pjoint); void OneGeom(tinyxml2::XMLElement* elem, mjsGeom* pgeom); @@ -80,7 +81,7 @@ class mjXReader : public mjXBase { void OneTendon(tinyxml2::XMLElement* elem, mjsTendon* ptendon); void OneActuator(tinyxml2::XMLElement* elem, mjsActuator* pactuator); void OneComposite(tinyxml2::XMLElement* elem, mjsBody* pbody, mjsDefault* def); - void OneFlexcomp(tinyxml2::XMLElement* elem, mjsBody* pbody); + void OneFlexcomp(tinyxml2::XMLElement* elem, mjsBody* pbody, const mjVFS* vfs); void OnePlugin(tinyxml2::XMLElement* elem, mjsPlugin* plugin); mjXSchema schema; // schema used for validation diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index 48ad62de..cd2fe3ed 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -76,7 +76,7 @@ static std::optional ParseInfOrNan(const std::string& s) { } FilePath ResolveFilePath(XMLElement* e, const FilePath& filename, - const FilePath& dir) { + const FilePath& dir, const mjVFS* vfs) { std::string path = ""; if (filename.IsAbs()) { return filename; @@ -85,7 +85,7 @@ FilePath ResolveFilePath(XMLElement* e, const FilePath& filename, // TODO(kylebayes): We first look in the base model directory for files to // remain backwards compatible. FilePath fullname = dir + filename; - mjResource *resource = mju_openResource(fullname.c_str(), nullptr, + mjResource *resource = mju_openResource("", fullname.c_str(), vfs, nullptr, 0); if (resource != nullptr) { mju_closeResource(resource); @@ -623,14 +623,14 @@ mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, bool required) { // if attribute is present, return attribute as a filename std::optional -mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr, +mjXUtil::ReadAttrFile(XMLElement* elem, const char* attr, const mjVFS* vfs, const FilePath& dir, bool required) { auto maybe_str = ReadAttrStr(elem, attr, required); if (!maybe_str.has_value()) { return std::nullopt; } FilePath filename(maybe_str.value()); - return ResolveFilePath(elem, filename, dir); + return ResolveFilePath(elem, filename, dir, vfs); } // if attribute is present, return numerical value of attribute diff --git a/src/xml/xml_util.h b/src/xml/xml_util.h index 7ef5913e..86d61948 100644 --- a/src/xml/xml_util.h +++ b/src/xml/xml_util.h @@ -26,6 +26,7 @@ #include "tinyxml2.h" +#include #include "user/user_util.h" // error string copy @@ -114,6 +115,7 @@ class mjXUtil { // if attribute is present, return attribute as a filename static std::optional ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr, + const mjVFS* vfs, const mujoco::user::FilePath& dir = mujoco::user::FilePath(), bool required = false); diff --git a/test/user/user_cache_test.cc b/test/user/user_cache_test.cc index 5b353eac..f226827b 100644 --- a/test/user/user_cache_test.cc +++ b/test/user/user_cache_test.cc @@ -47,7 +47,7 @@ void CacheText(mjCCache& cache, const std::string& model, mjVFS vfs; mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, name.c_str(), text.data(), text.size()); - mjResource* resource = mju_openResource(name.c_str(), &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", name.c_str(), &vfs, nullptr, 0); std::shared_ptr data(&text, +[](const void* data) {}); cache.Insert(model, resource, data, text.size()); mju_closeResource(resource); @@ -61,7 +61,7 @@ GetCachedText(mjCCache& cache, const std::string& model, mjVFS vfs; mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, name.c_str(), text.data(), std::strlen(text.c_str())); - mjResource* resource = mju_openResource(name.c_str(), &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", name.c_str(), &vfs, nullptr, 0); bool inserted = cache.PopulateData(resource, [&cached_text](const void* data) { cached_text = *(static_cast(data)); diff --git a/test/user/user_resource_test.cc b/test/user/user_resource_test.cc index 3f243641..357e75e4 100644 --- a/test/user/user_resource_test.cc +++ b/test/user/user_resource_test.cc @@ -233,7 +233,7 @@ TEST_F(ResourceTest, GeneralTest) { EXPECT_GT(i, 0); // open resource - mjResource* resource = mju_openResource("str:file", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "str:file", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); const char* buffer = NULL; @@ -256,7 +256,7 @@ TEST_F(ResourceTest, GeneralFailureTest) { static std::array error; // open resource - mjResource* resource = mju_openResource("str:notfound", nullptr, + mjResource* resource = mju_openResource("", "str:notfound", nullptr, error.data(), error.size()); ASSERT_THAT(resource, IsNull()); @@ -281,7 +281,7 @@ TEST_F(ResourceTest, NameWithValidPrefix) { }; // open resource - mjResource* resource = mju_openResource("nop:found", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "nop:found", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); mju_closeResource(resource); } @@ -304,7 +304,7 @@ TEST_F(ResourceTest, NameWithUpperCasePrefix) { }; // open resource - mjResource* resource = mju_openResource("NOP:found", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "NOP:found", nullptr, nullptr, 0); ASSERT_THAT(resource, NotNull()); mju_closeResource(resource); } @@ -327,7 +327,7 @@ TEST_F(ResourceTest, NameWithInvalidPrefix) { }; // open resource - mjResource* resource = mju_openResource("nopfound", nullptr, nullptr, 0); + mjResource* resource = mju_openResource("", "nopfound", nullptr, nullptr, 0); ASSERT_THAT(resource, IsNull()); } @@ -338,7 +338,7 @@ TEST_F(ResourceTest, OSFilesystemTimestamps) { const char* const file = "engine/testdata/collision_box/boxbox_deep.xml"; const std::string xml_path = GetTestDataFilePath(file); - mjResource* resource = mju_openResource(xml_path.c_str(), nullptr, + mjResource* resource = mju_openResource("", xml_path.c_str(), nullptr, nullptr, 0); mju_decodeBase64((uint8_t*) &t, resource->timestamp); diff --git a/test/user/user_vfs_test.cc b/test/user/user_vfs_test.cc index 36378130..2210c8d6 100644 --- a/test/user/user_vfs_test.cc +++ b/test/user/user_vfs_test.cc @@ -30,7 +30,7 @@ using ::testing::NotNull; using UserVfsTest = MujocoTest; static bool HasFile(const mjVFS* vfs, const std::string& filename) { - mjResource* resource = mju_openResource(filename.c_str(), vfs, nullptr, 0); + mjResource* resource = mju_openResource("", filename.c_str(), vfs, nullptr, 0); bool result = resource != nullptr; mju_closeResource(resource); return result; @@ -196,14 +196,13 @@ TEST_F(UserVfsTest, AddBufferRepeat) { mj_deleteVFS(&vfs); } -TEST_F(UserVfsTest, BufferStripPath) { +TEST_F(UserVfsTest, BufferPath) { mjVFS vfs; mj_defaultVFS(&vfs); std::string buffer = ""; const void* ptr = static_cast(buffer.c_str()); mj_addBufferVFS(&vfs, "dir/model", ptr, buffer.size()); - EXPECT_TRUE(HasFile(&vfs, "MODEL")); - EXPECT_TRUE(HasFile(&vfs, "dir\\model")); + EXPECT_TRUE(HasFile(&vfs, "files/../dir/model")); mj_deleteVFS(&vfs); } @@ -222,7 +221,7 @@ TEST_F(UserVfsTest, Timestamps) { mj_defaultVFS(&vfs); mj_addBufferVFS(&vfs, "cube.obj", cube, sizeof(cube)); - mjResource* resource = mju_openResource("cube.obj", &vfs, nullptr, 0); + mjResource* resource = mju_openResource("", "cube.obj", &vfs, nullptr, 0); // same timestamps EXPECT_EQ(mju_isModifiedResource(resource, resource->timestamp), 0); diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 22fc75b8..674f3578 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -15,7 +15,6 @@ // Tests for xml/xml_native_reader.cc. #include -#include #include #include #include @@ -613,6 +612,18 @@ static constexpr unsigned char kTinyPng[] = { 0x82 }; +// mesh OBJ file of a cube +static constexpr char kTinyObj[] = R"( + v -1 -1 1 + v 1 -1 1 + v -1 1 1 + v 1 1 1 + v -1 1 -1 + v 1 1 -1 + v -1 -1 -1 + v 1 -1 -1)"; + + TEST_F(XMLReaderTest, IncludeTest) { static constexpr char xml[] = R"( @@ -726,24 +737,22 @@ TEST_F(XMLReaderTest, IncludePathTest) { )"; - MockFilesystem fs("IncludePathTest"); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); - - fs.ChangeDirectory("submodels/"); - fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); - fs.AddFile("model2.xml", (const unsigned char*) xml2, sizeof(xml2)); - fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, - sizeof(xml3)); - fs.ChangeDirectory("/"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); + mj_addBufferVFS(&vfs, "submodels/model1.xml", xml1, sizeof(xml1)); + mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2)); + mj_addBufferVFS(&vfs, "submodels/subsubmodels/model3.xml", xml3, + sizeof(xml3)); std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error.data(), + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, FallbackIncludePathTest) { @@ -772,22 +781,21 @@ TEST_F(XMLReaderTest, FallbackIncludePathTest) { )"; - MockFilesystem fs("FallbackIncludePathTest"); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); - - fs.AddFile("model1.xml", (const unsigned char*) xml1, sizeof(xml1)); - fs.AddFile("submodels/model2.xml", (const unsigned char*) xml2, sizeof(xml2)); - fs.AddFile("subsubmodels/model3.xml", (const unsigned char*) xml3, - sizeof(xml3)); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); + mj_addBufferVFS(&vfs, "model1.xml", xml1, sizeof(xml1)); + mj_addBufferVFS(&vfs, "submodels/model2.xml", xml2, sizeof(xml2)); + mj_addBufferVFS(&vfs, "subsubmodels/model3.xml", xml3, sizeof(xml3)); std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "ball"), 2); EXPECT_EQ(mj_name2id(model, mjOBJ_GEOM, "another_box"), 3); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, MaterialTextureTest) { @@ -809,14 +817,14 @@ TEST_F(XMLReaderTest, MaterialTextureTest) { )"; - MockFilesystem fs("MaterialTextureTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); char error[1024]; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error, 1024); + mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024); EXPECT_THAT(model, NotNull()) << error; EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1); @@ -825,6 +833,7 @@ TEST_F(XMLReaderTest, MaterialTextureTest) { EXPECT_EQ(model->mat_texid[mjTEXROLE_OCCLUSION], 0); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, LegacyMaterialTextureTest) { @@ -841,19 +850,20 @@ TEST_F(XMLReaderTest, LegacyMaterialTextureTest) { )"; - MockFilesystem fs("LegacyMaterialTextureTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "tiny0.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "tiny1.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); char error[1024]; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error, 1024); + mjModel* model = mj_loadXML("model.xml", &vfs, error, 1024); EXPECT_THAT(model, NotNull()) << error; EXPECT_EQ(model->mat_texid[mjTEXROLE_RGB], 1); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, MaterialTextureFailTest) { @@ -873,12 +883,6 @@ TEST_F(XMLReaderTest, MaterialTextureFailTest) { )"; - MockFilesystem fs("MaterialTextureFailTest"); - fs.AddFile("tiny0.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("tiny1.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); - std::array error; mjModel* m = LoadModelFromString(xml, error.data(), error.size()); EXPECT_THAT(m, IsNull()); @@ -907,28 +911,32 @@ TEST_F(XMLReaderTest, IncludeAssetsTest) { static constexpr char subassets[] = R"( + )"; - MockFilesystem fs("IncludeAssetsTest"); - fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/subassets/subtiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/assets.xml", (const unsigned char*) assets, - sizeof(assets)); - fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, - sizeof(subassets)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "assets/subassets/subtiny.png", kTinyPng, + sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "assets/subassets/cube.obj", kTinyObj, + sizeof(kTinyObj)); + mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets)); + mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets, + sizeof(subassets)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); // loading the file should be successful std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, error.data(), + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) { @@ -956,26 +964,26 @@ TEST_F(XMLReaderTest, FallbackIncludeAssetsTest) { )"; - MockFilesystem fs("FallbackIncludeAssetsTest"); - fs.AddFile("assets/tiny.png", kTinyPng, sizeof(kTinyPng)); + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addBufferVFS(&vfs, "assets/tiny.png", kTinyPng, sizeof(kTinyPng)); // need to fallback for backwards compatibility - fs.AddFile("subtiny.png", kTinyPng, sizeof(kTinyPng)); + mj_addBufferVFS(&vfs, "subtiny.png", kTinyPng, sizeof(kTinyPng)); - fs.AddFile("assets/assets.xml", (const unsigned char*) assets, - sizeof(assets)); - fs.AddFile("assets/subassets/assets.xml", (const unsigned char*) subassets, - sizeof(subassets)); - fs.AddFile("model.xml", (const unsigned char*) xml, sizeof(xml)); - std::string modelpath = fs.FullPath("model.xml"); + mj_addBufferVFS(&vfs, "assets/assets.xml", assets, sizeof(assets)); + mj_addBufferVFS(&vfs, "assets/subassets/assets.xml", subassets, + sizeof(subassets)); + mj_addBufferVFS(&vfs, "model.xml", xml, sizeof(xml)); // loading the file should be successful std::array error; - mjModel* model = mj_loadXML(modelpath.c_str(), nullptr, + mjModel* model = mj_loadXML("model.xml", &vfs, error.data(), error.size()); ASSERT_THAT(model, NotNull()) << error.data(); mj_deleteModel(model); + mj_deleteVFS(&vfs); } TEST_F(XMLReaderTest, IncludeAbsoluteTest) {