Refactor 'ParseXML' and 'ParseSpecFromString' to use a shared helper function.

This allows us to remove the special "LoadModelFromString" resource provider.

PiperOrigin-RevId: 854207845
Change-Id: I6d00f2ab99b4351866e1d39dbeed96f29aaaf915
This commit is contained in:
Haroon Qureshi
2026-01-09 08:39:35 -08:00
committed by Copybara-Service
parent 398e3dcf15
commit 45e164bf8f
2 changed files with 72 additions and 78 deletions
+58 -78
View File
@@ -98,26 +98,6 @@ class LocaleOverride {
}; };
#endif #endif
void RegisterResourceProvider() {
// register string resource provider if not registered before
if (mjp_getResourceProvider("LoadModelFromString:") == nullptr) {
mjpResourceProvider resourceProvider;
mjp_defaultResourceProvider(&resourceProvider);
resourceProvider.prefix = "LoadModelFromString";
resourceProvider.open = +[](mjResource* resource) {
resource->data = &(resource->name[strlen("LoadModelFromString:")]);
return 1;
};
resourceProvider.read =
+[](mjResource* resource, const void** buffer) {
*buffer = resource->data;
return (int) strlen((const char*) resource->data);
};
resourceProvider.close = +[](mjResource* resource) {};
mjp_registerResourceProvider(&resourceProvider);
}
}
// find include elements recursively, replace them with subtree from xml file // find include elements recursively, replace them with subtree from xml file
void IncludeXML(mjXReader& reader, XMLElement* elem, void IncludeXML(mjXReader& reader, XMLElement* elem,
const FilePath& dir, const mjVFS* vfs, const FilePath& dir, const mjVFS* vfs,
@@ -259,57 +239,21 @@ void IncludeXML(mjXReader& reader, XMLElement* elem,
} }
} }
} // namespace
// Main parser function // Main parser function
mjSpec* ParseXML(const char* filename, const mjVFS* vfs, mjSpec* SpecFromXML(std::string_view xml, std::string_view dir,
char* error, int nerror) { std::string_view filename, const mjVFS* vfs, char* error,
int nerror) {
LocaleOverride locale_override; LocaleOverride locale_override;
// check arguments
if (!filename) {
if (error) {
std::snprintf(error, nerror, "ParseXML: filename argument required\n");
}
return nullptr;
}
// clear // clear
mjSpec* spec = nullptr; mjSpec* spec = nullptr;
if (error) { if (error) {
error[0] = '\0'; error[0] = '\0';
} }
// get data source
const char* xmlstring = nullptr;
std::array<char, 1024> rerror;
mjResource* resource = mju_openResource("", filename, vfs,
rerror.data(), rerror.size());
if (resource == nullptr) {
std::snprintf(error, nerror, "ParseXML: %s", rerror.data());
return nullptr;
}
int buffer_size = mju_readResource(resource, (const void**) &xmlstring);
if (buffer_size < 0) {
if (error) {
std::snprintf(error, nerror,
"ParseXML: error reading file '%s'", filename);
}
mju_closeResource(resource);
return nullptr;
} else if (!buffer_size) {
if (error) {
std::snprintf(error, nerror, "ParseXML: empty file '%s'", filename);
}
mju_closeResource(resource);
return nullptr;
}
// load XML file or parse string // load XML file or parse string
XMLDocument doc; XMLDocument doc;
doc.Parse(xmlstring, buffer_size); doc.Parse(xml.data(), xml.size());
// error checking // error checking
if (doc.Error()) { if (doc.Error()) {
@@ -317,37 +261,26 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs,
snprintf(error, nerror, "XML parse error %d:\n%s\n", snprintf(error, nerror, "XML parse error %d:\n%s\n",
doc.ErrorID(), doc.ErrorStr()); doc.ErrorID(), doc.ErrorStr());
} }
mju_closeResource(resource);
return nullptr; return nullptr;
} }
// get top-level element // get top-level element
XMLElement* root = doc.RootElement(); XMLElement* root = doc.RootElement();
if (!root) { if (!root) {
mju_closeResource(resource);
mjCopyError(error, "XML root element not found", nerror); mjCopyError(error, "XML root element not found", nerror);
return nullptr; return nullptr;
} }
// create model, set filedir // create model, set filedir
spec = mj_makeSpec(); spec = mj_makeSpec();
const char* dir; mjs_setString(spec->modelfiledir, std::string(dir).c_str());
int ndir = 0;
mju_getResourceDir(resource, &dir, &ndir);
if (dir != nullptr) {
mjs_setString(spec->modelfiledir, std::string(dir, ndir).c_str());
} else {
mjs_setString(spec->modelfiledir, "");
}
// close resource
mju_closeResource(resource);
// parse with exceptions // parse with exceptions
try { try {
if (!strcasecmp(root->Value(), "mujoco")) { if (!strcasecmp(root->Value(), "mujoco")) {
// find include elements, replace them with subtree from xml file // find include elements, replace them with subtree from xml file
std::unordered_set<std::string> included = {filename}; std::unordered_set<std::string> included = {std::string(filename)};
mjXReader parser; mjXReader parser;
parser.SetModelFileDir(mjs_getString(spec->modelfiledir)); parser.SetModelFileDir(mjs_getString(spec->modelfiledir));
IncludeXML(parser, root, FilePath(), vfs, included); IncludeXML(parser, root, FilePath(), vfs, included);
@@ -385,12 +318,59 @@ mjSpec* ParseXML(const char* filename, const mjVFS* vfs,
return spec; return spec;
} }
} // namespace
mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs, char* error, int nerror) { mjSpec* ParseXML(const char* filename, const mjVFS* vfs, char* error,
RegisterResourceProvider(); int nerror) {
std::string xml2 = {xml.begin(), xml.end()}; // check arguments
std::string str = "LoadModelFromString:" + xml2; if (!filename) {
return ParseXML(str.c_str(), vfs, error, nerror); if (error) {
std::snprintf(error, nerror, "ParseXML: filename argument required\n");
}
return nullptr;
}
// get data source
const char* xml = nullptr;
std::array<char, 1024> rerror;
mjResource* resource = mju_openResource("", filename, vfs,
rerror.data(), rerror.size());
if (resource == nullptr) {
std::snprintf(error, nerror, "ParseXML: %s", rerror.data());
return nullptr;
}
int buffer_size = mju_readResource(resource, (const void**) &xml);
if (buffer_size < 0) {
if (error) {
std::snprintf(error, nerror,
"ParseXML: error reading file '%s'", filename);
}
mju_closeResource(resource);
return nullptr;
} else if (!buffer_size) {
if (error) {
std::snprintf(error, nerror, "ParseXML: empty file '%s'", filename);
}
mju_closeResource(resource);
return nullptr;
}
const char* dir;
int ndir = 0;
mju_getResourceDir(resource, &dir, &ndir);
std::string_view directory(dir, ndir);
mjSpec* spec = SpecFromXML({xml, xml + buffer_size}, directory,
filename, vfs, error, nerror);
mju_closeResource(resource);
return spec;
}
mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs, char* error,
int nerror) {
return SpecFromXML(xml, "", "", vfs, error, nerror);
} }
// Main writer function - calls mjXWrite // Main writer function - calls mjXWrite
+14
View File
@@ -115,6 +115,20 @@ TEST_F(SaveLastXmlTest, EmptyModel) {
mj_deleteModel(model); mj_deleteModel(model);
} }
TEST_F(LoadXmlTest, NullFileFails) {
std::array<char, 1000> error;
mjSpec* spec = mj_parseXML(nullptr, nullptr, error.data(), error.size());
EXPECT_THAT(spec, IsNull()) << "Expected model loading to fail.";
EXPECT_THAT(error.data(), HasSubstr("filename argument required"));
}
TEST_F(LoadXmlTest, InvalidFileFails) {
std::array<char, 1000> error;
mjSpec* spec = mj_parseXML("invalid", nullptr, error.data(), error.size());
EXPECT_THAT(spec, IsNull()) << "Expected model loading to fail.";
EXPECT_THAT(error.data(), HasSubstr("No such file or directory"));
}
TEST_F(MujocoTest, SaveXmlShortString) { TEST_F(MujocoTest, SaveXmlShortString) {
std::array<char, 1000> error; std::array<char, 1000> error;