// Copyright 2021 DeepMind Technologies Limited // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cc/array_safety.h" #include "engine/engine_util_errmem.h" #include "xml/xml_util.h" #include "xml/xml_numeric_format.h" namespace { using std::istringstream; using std::size_t; using std::string; using std::stringstream; using std::vector; using tinyxml2::XMLAttribute; using tinyxml2::XMLElement; namespace mju = ::mujoco::util; template std::optional ParseInfOrNan(const std::string& s) { const char* str = s.c_str(); if constexpr (std::is_floating_point_v) { T sign = 1; if (s.size() == 4 && s[0] == '-') { sign = -1; ++str; } else if (s.size() != 3) { return std::nullopt; } if (std::numeric_limits::has_infinity && (str[0] == 'i' || str[0] == 'I') && (str[1] == 'n' || str[1] == 'N') && (str[2] == 'f' || str[2] == 'F')) { return sign * std::numeric_limits::infinity(); } else if (std::numeric_limits::has_quiet_NaN && (str[0] == 'n' || str[0] == 'N') && (str[1] == 'a' || str[1] == 'A') && (str[2] == 'n' || str[2] == 'N')) { return sign * std::numeric_limits::quiet_NaN(); } } return std::nullopt; } } // namespace //---------------------------------- utility functions --------------------------------------------- // error string copy void mjCopyError(char* dst, const char* src, int maxlen) { if (dst && maxlen>0) { strncpy(dst, src, maxlen); dst[maxlen-1] = 0; } } // error constructor mjXError::mjXError(const XMLElement* elem, const char* msg, const char* str, int pos) { char temp[500]; // construct error message mju::sprintf_arr(message, "XML Error"); if (msg) { mju::sprintf_arr(temp, msg, str, pos); mju::strcat_arr(message, ": "); mju::strcat_arr(message, temp); } // append element, line numbers if (elem) { mju::sprintf_arr(temp, "\nElement '%s', line %d\n", elem->Value(), elem->GetLineNum()); mju::strcat_arr(message, temp); } } //---------------------------------- class mjXSchema implementation -------------------------------- // constructor mjXSchema::mjXSchema(const char* schema[][mjXATTRNUM], int nrow, bool checkptr) { // clear fields name.clear(); type = '?'; child.clear(); attr.clear(); error.clear(); // checks nrow and first element if (nrow<1) { error = "number of rows must be positive"; return; } if (schema[0][0][0]=='<' || schema[0][0][0]=='>') { error = "expected element, found bracket"; return; } // check entire schema for null pointers if (checkptr) { char msg[100]; for (int i=0; imjXATTRNUM-3) { mju::sprintf_arr(msg, "invalid number of attributes in row %d, element %s", i, schema[i][0]); error = msg; return; } // attribute pointers for (int j=0; j1) { // check for bracketed block if (schema[1][0][0]!='<' || schema[nrow-1][0][0]!='>') { error = "expected brackets after complex element"; return; } // parse block into simple and complex elements, create children int start = 2; while (start < nrow-1) { int end = start; // look for bracketed block at start+1 if (schema[start+1][0][0]=='<') { // look for corresponding closing bracket int cnt = 0; while (end <= nrow-1) { if (schema[end][0][0]=='<') { cnt++; } else if (schema[end][0][0]=='>') { cnt--; if (cnt==0) { break; } } end++; } // closing bracket not found if (end > nrow-1) { error = "matching closing bracket not found"; return; } } // add element, check for error mjXSchema* elem = new mjXSchema(schema+start, end-start+1, false); child.push_back(elem); if (!elem->error.empty()) { error = elem->error; return; } // proceed with next subelement start = end+1; } } } // destructor mjXSchema::~mjXSchema() { // delete children recursively for (unsigned int i=0; ib) { return a; } else { return b; } } // print schema as text void mjXSchema::Print(std::stringstream& str, int level) { // replace body with (world)body string name1 = (name=="body" ? "(world)body" : name); // space, name, type printspace(str, 3*level, " "); str << name1 << " (" << type << ")"; int baselen = 3*level + (int)name1.size() + 4; if (baselen<30) { printspace(str, 30-baselen, " "); } // attributes int cnt = _max(baselen, 30); for (int i=0; i<(int)attr.size(); i++) { if (cnt>60) { str << "\n"; printspace(str, (cnt = _max(30, baselen)), " "); } str << attr[i] << " "; cnt += (int)attr[i].size() + 1; } str << "\n"; // children for (int i=0; i<(int)child.size(); i++) { child[i]->Print(str, level+1); } } // print schema as HTML table void mjXSchema::PrintHTML(std::stringstream& str, int level, bool pad) { // replace body with (world)body string name1 = (name=="body" ? "(world)body" : name); // open table if (level==0) { str << "\n"; } // name: with HTML padding if (pad) { str << "\n\t\n"; } // name: with   for browsers that ignore padding else { str << "\n\t\n"; } // type str << "\t\n"; // attributes str << "\t\n\n"; // children for (int i=0; i<(int)child.size(); i++) { child[i]->PrintHTML(str, level+1, pad); } // close table if (level==0) { str << "
" << name1 << "
"; if (level) { printspace(str, 4*level, " "); } str << name1 << "" << type << ""; if (!attr.empty()) { for (int i=0; i<(int)attr.size(); i++) { str << attr[i] << " "; } } else { str << "no attributes"; } str << "
\n"; } } // check for name match bool mjXSchema::NameMatch(XMLElement* elem, int level) { // special handling of body and worldbody if (name=="body") { if (level==1 && !strcmp(elem->Value(), "worldbody")) { return true; } if (level!=1 && !strcmp(elem->Value(), "body")) { return true; } return false; } // regular check return (name==elem->Value()); } // validator XMLElement* mjXSchema::Check(XMLElement* elem, int level) { bool missing; char msg[100]; XMLElement *bad, *sub; error.clear(); if (!elem) { return 0; // SHOULD NOT OCCUR } // check name (already done by parent, but hard to avoid) if (!NameMatch(elem, level)) { error = "unrecognized element"; return elem; } // check attributes const XMLAttribute* attribute = elem->FirstAttribute(); while (attribute) { missing = true; for (int i=0; i<(int)attr.size(); i++) { if (attr[i]==attribute->Name()) { missing = false; break; } } if (missing) { error = "unrecognized attribute: '" + string(attribute->Name()) + "'"; return elem; } // next attribute attribute = attribute->Next(); } // handle recursion if (type=='R') { // loop over sub-elements with same name sub = elem->FirstChildElement((const char*)name.c_str()); while (sub) { // check sub-tree if ((bad = Check(sub, level+1))) { return bad; } // advance to next sub-element with same name sub = sub->NextSiblingElement((const char*)name.c_str()); } } // clear reference counts for (int i=0; i<(int)child.size(); i++) { child[i]->refcnt = 0; } // check sub-elements, update refcnt sub = elem->FirstChildElement(); while (sub) { // find in child array, update refcnt missing = true; for (int i=0; i<(int)child.size(); i++) { if (child[i]->NameMatch(sub, level+1)) { // check sub-tree if ((bad = child[i]->Check(sub, level+1))) { error = child[i]->error; return bad; } // mark found missing = false; child[i]->refcnt++; break; } } // missing, unless recursive if (missing && !(type=='R' && NameMatch(sub, level+1))) { error = "unrecognized element"; return sub; } // advance to next sub-element sub = sub->NextSiblingElement(); } // enforce sub-element types msg[0] = 0; for (int i=0; i<(int)child.size(); i++) { switch (child[i]->type) { case '!': if (child[i]->refcnt != 1) mju::sprintf_arr(msg, "required sub-element '%s' found %d time(s)", child[i]->name.c_str(), child[i]->refcnt); break; case '?': if (child[i]->refcnt > 1) mju::sprintf_arr(msg, "unique sub-element '%s' found %d time(s)", child[i]->name.c_str(), child[i]->refcnt); break; default: break; } } // handle error if (msg[0]) { error = msg; return elem; } else { return 0; } } //---------------------------------- class mjXUtil implementation ---------------------------------- // compare two vectors template bool mjXUtil::SameVector(const T* vec1, const T* vec2, int n) { if (!vec1 || !vec2) { return false; } bool same = true; for (int i=0; i std::numeric_limits::epsilon()) { same = false; } } return same; } template bool mjXUtil::SameVector(const double* vec1, const double* vec2, int n); template bool mjXUtil::SameVector(const float* vec1, const float* vec2, int n); template bool mjXUtil::SameVector(const int* vec1, const int* vec2, int n); template bool mjXUtil::SameVector(const mjtByte* vec1, const mjtByte* vec2, int n); // find string in map, return corresponding integer (-1: not found) int mjXUtil::FindKey(const mjMap* map, int mapsz, string key) { for (int i=0; i int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, T* data, string& text, bool required, bool exact) { const char* pstr = elem->Attribute(attr); // check if attribute exists if (!pstr) { if (required) { throw mjXError(elem, "required attribute missing: '%s'", attr); } else { return 0; } } // convert to string text = string(pstr); // get input stream istringstream strm(text); std::string token; // read numbers int i = 0; while (!strm.eof() && i < len) { strm >> token; istringstream token_strm(token); token_strm >> data[i]; if (token_strm.fail() || !token_strm.eof()) { // C++ standard libraries do not always parse inf and nan as valid floating point values. std::optional maybe_result = ParseInfOrNan(token); if (maybe_result.has_value()) { data[i] = *maybe_result; } else { throw mjXError(elem, "problem reading attribute '%s'", attr); } } if constexpr (std::is_floating_point_v) { if (std::isnan(data[i])) { mju_warning("XML contains a 'NaN'. Please check it carefully."); } } ++i; } strm >> std::ws; // check if there is not enough data if (exact && i < len) { throw mjXError(elem, "attribute '%s' does not have enough data", attr); } // check if there is too much data if (!strm.eof()) { throw mjXError(elem, "attribute '%s' has too much data", attr); } return i; } template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, double* data, string& text, bool required, bool exact); template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, float* data, string& text, bool required, bool exact); template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, int* data, string& text, bool required, bool exact); template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, mjtByte* data, string& text, bool required, bool exact); // read DOUBLE array into C++ vector, return number read int mjXUtil::ReadVector(XMLElement* elem, const char* attr, vector& vec, string& text, bool required) { double buffer[1000]; int n = ReadAttr(elem, attr, 1000, buffer, text, required, false); if (n>0) { vec.resize(n); memcpy(vec.data(), buffer, n*sizeof(double)); } return n; } // read text field bool mjXUtil::ReadAttrTxt(XMLElement* elem, const char* attr, string& text, bool required) { const char* pstr = elem->Attribute(attr); // check if attribute exists if (!pstr) { if (required) { throw mjXError(elem, "required attribute missing: '%s'", attr); } else { return false; } } // read text text = string(pstr); return true; } // read single int bool mjXUtil::ReadAttrInt(XMLElement* elem, const char* attr, int* data, bool required) { const char* pstr = elem->Attribute(attr); // check if attribute exists if (!pstr) { if (required) { throw mjXError(elem, "required attribute missing: '%s'", attr); } else { return false; } } // convert to int, check int buffer[2] = {0, 0}; if (sscanf(pstr, "%d", buffer) != 1) { throw mjXError(elem, "single int expected in attribute %s", attr); } // copy data *data = buffer[0]; return true; } // read vector from string void mjXUtil::String2Vector(const string& txt, vector& vec) { stringstream strm(txt); vec.clear(); while (!strm.eof()) { float num; strm >> num; if (strm.fail()) { break; } else { vec.push_back(num); } } } // read vector from string void mjXUtil::String2Vector(const string& txt, vector& vec) { stringstream strm(txt); vec.clear(); while (!strm.eof()) { int num; strm >> num; if (strm.fail()) { break; } else { vec.push_back(num); } } } // write vector to string void mjXUtil::Vector2String(string& txt, const vector& vec) { stringstream strm; for (size_t i=0; i0) { strm << " "; } strm << vec[i]; } txt = strm.str(); } // write vector to string void mjXUtil::Vector2String(string& txt, const vector& vec) { stringstream strm; for (size_t i=0; i0) { strm << " "; } strm << vec[i]; } txt = strm.str(); } // find subelement with given name, make sure it is unique XMLElement* mjXUtil::FindSubElem(XMLElement* elem, string name, bool required) { XMLElement* subelem = 0; XMLElement* iter = elem->FirstChildElement(); while (iter) { // identify elements with given name if (name == iter->Value()) { // make sure name is not repeated if (subelem) { throw mjXError(subelem, "repeated element: '%s'", name.c_str()); } // save found element subelem = iter; } // advance to next element iter = iter->NextSiblingElement(); } if (required && !subelem) { throw mjXError(elem, "missing element: '%s'", name.c_str()); } return subelem; } // find attribute, translate key, return int value bool mjXUtil::MapValue(XMLElement* elem, const char* attr, int* data, const mjMap* map, int mapSz, bool required) { // get attribute text string text; if (!ReadAttrTxt(elem, attr, text, required)) { return false; } // find keyword in map int value = FindKey(map, mapSz, text); if (value<0) { throw mjXError(elem, "invalid keyword: '%s'", text.c_str()); } // copy *data = value; return true; } //---------------------------------- write functions ----------------------------------------------- // check if double is int static bool isint(double x) { return ((std::abs(x - floor(x)) < 1E-12) || (std::abs(x - ceil(x)) < 1E-12)); } // round to nearest int static int Round(double x) { if (std::abs(x - floor(x)) < std::abs(x - ceil(x))) { return (int)floor(x); } else { return (int)ceil(x); } } // write attribute template void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, T* data, const T* def) { // make sure all are defined if constexpr (std::is_floating_point_v) { for (int i=0; i0) { stream << " "; } // append number if (isint(data[i])) { stream << Round(data[i]); } else { stream << data[i]; } } // set attribute as string WriteAttrTxt(elem, name, stream.str()); } template void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, double* data, const double* def); template void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, float* data, const float* def); template void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, int* data, const int* def); template void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, mjtByte* data, const mjtByte* def); // write vector attribute, default = zero array void mjXUtil::WriteVector(XMLElement* elem, string name, vector& vec) { // proceed only if non-zero found bool ok = false; for (size_t i=0; i attribute, default with same size void mjXUtil::WriteVector(XMLElement* elem, string name, vector& vec, vector& def) { // proceed only if non-zero found bool ok = false; for (size_t i=0; iSetAttribute(name.c_str(), value.c_str()); } // write attribute- single int void mjXUtil::WriteAttrInt(XMLElement* elem, string name, int data, int def) { // skip default if (data==def) { return; } elem->SetAttribute(name.c_str(), data); } // write attribute- keyword void mjXUtil::WriteAttrKey(XMLElement* elem, string name, const mjMap* map, int mapsz, int data, int def) { // skip default if (data==def) { return; } WriteAttrTxt(elem, name, FindValue(map, mapsz, data)); }