diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index 4c3c5526..d47f9dba 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -12,12 +12,14 @@ // 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 @@ -44,7 +46,7 @@ using tinyxml2::XMLElement; namespace mju = ::mujoco::util; template -std::optional ParseInfOrNan(const std::string& s) { +static std::optional ParseInfOrNan(const std::string& s) { const char* str = s.c_str(); if constexpr (std::is_floating_point_v) { T sign = 1; @@ -270,15 +272,6 @@ static void printspace(std::stringstream& str, int n, const char* space) { } -// max -static int _max(int a, int b) { - if (a>b) { - return a; - } else { - return b; - } -} - // print schema as text void mjXSchema::Print(std::stringstream& str, int level) { @@ -294,11 +287,11 @@ void mjXSchema::Print(std::stringstream& str, int level) { } // attributes - int cnt = _max(baselen, 30); + int cnt = std::max(baselen, 30); for (int i=0; i<(int)attr.size(); i++) { if (cnt>60) { str << "\n"; - printspace(str, (cnt = _max(30, baselen)), " "); + printspace(str, (cnt = std::max(30, baselen)), " "); } @@ -508,6 +501,63 @@ XMLElement* mjXSchema::Check(XMLElement* elem, int level) { //---------------------------------- class mjXUtil implementation ---------------------------------- +// helper function to read multiple numerical values from an attribute +// return false if the entire attribute wasn't read (max was reached) +// throw error if syntax error while trying to read numerical data +template +bool mjXUtil::ReadAttrValues(XMLElement* elem, const char* attr, + std::function push, int max) { + const char* pstr = elem->Attribute(attr); + T item; + + if (pstr == nullptr) { + return true; + } + + // get input stream + std::string str = std::string(pstr); + std::istringstream strm(str); + std::string token; + + // read numbers + for (int i = 0; (max < 0 || i < max) && !strm.eof(); ++i) { + strm >> token; + std::istringstream token_strm(token); + token_strm >> item; + 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()) { + item = maybe_result.value(); + } else { + throw mjXError(elem, "problem reading attribute '%s'", attr); + } + } + + push(i, item); + if constexpr (std::is_floating_point_v) { + if (std::isnan(item)) { + mju_warning("XML contains a 'NaN'. Please check it carefully."); + } + } + // clear any trailing whitespace + strm >> std::ws; + } + + return strm.eof(); +} + +template bool mjXUtil::ReadAttrValues(XMLElement* elem, const char* attr, + std::function push, int max); +template bool mjXUtil::ReadAttrValues(XMLElement* elem, const char* attr, + std::function push, int max); +template bool mjXUtil::ReadAttrValues(XMLElement* elem, const char* attr, + std::function push, int max); +template bool mjXUtil::ReadAttrValues(XMLElement* elem, const char* attr, + std::function push, int max); + + + // compare two vectors template bool mjXUtil::SameVector(const T* vec1, const T* vec2, int n) { @@ -557,6 +607,76 @@ string mjXUtil::FindValue(const mjMap* map, int mapsz, int value) { +// if attribute is present, return vector of numerical data +template +std::optional> mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, + bool required) { + std::vector vec; + ReadAttrValues(elem, attr, [&](int i, T num) { vec.push_back(num); }); + if (!vec.size()) { + if (required) { + throw mjXError(elem, "required attribute missing: '%s'", attr); + } else { + return std::nullopt; + } + } + + return vec; +} + +template std::optional> +mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required); +template std::optional> +mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required); +template std::optional> +mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required); +template std::optional> +mjXUtil::ReadAttrVec(XMLElement* elem, const char* attr, bool required); + + + +// if attribute is present, return attribute as a string +std::optional mjXUtil::ReadAttrStr(XMLElement* elem, const char* attr, + bool required) { + const char* pstr = elem->Attribute(attr); + + // check if attribute exists + if (pstr == nullptr) { + if (required) { + throw mjXError(elem, "required attribute missing: '%s'", attr); + } else { + return std::nullopt; + } + } + + return std::string(pstr); +} + + + +// if attribute is present, return numerical value of attribute +template +std::optional mjXUtil::ReadAttrNum(XMLElement* elem, const char* attr, + bool required) { + auto maybe_arr = ReadAttrArr(elem, attr, required); + if (!maybe_arr.has_value()) { + return std::nullopt; + } + + return maybe_arr.value()[0]; +} + +template std::optional +mjXUtil::ReadAttrNum(XMLElement* elem, const char* attr, bool required); +template std::optional +mjXUtil::ReadAttrNum(XMLElement* elem, const char* attr, bool required); +template std::optional +mjXUtil::ReadAttrNum(XMLElement* elem, const char* attr, bool required); +template std::optional +mjXUtil::ReadAttrNum(XMLElement* elem, const char* attr, bool required); + + + // read attribute "attr" of element "elem" // "len" is the number of floats or doubles to be read // the content is returned in "text", the numeric data in "data" @@ -564,59 +684,23 @@ string mjXUtil::FindValue(const mjMap* map, int mapsz, int value) { template 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; - } + auto maybe_vec = ReadAttrVec(elem, attr, required); + if (!maybe_vec.has_value()) { + 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) { + if (exact && maybe_vec->size() < len) { throw mjXError(elem, "attribute '%s' does not have enough data", attr); } // check if there is too much data - if (!strm.eof()) { + if (maybe_vec->size() > len) { throw mjXError(elem, "attribute '%s' has too much data", attr); } - return i; + std::copy(maybe_vec->begin(), maybe_vec->end(), data); + return maybe_vec->size(); } template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len, @@ -636,59 +720,36 @@ template int mjXUtil::ReadAttr(XMLElement* elem, const char* attr, const int len // 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)); + auto maybe_vec = ReadAttrVec(elem, attr, required); + if (!maybe_vec.has_value()) { + return 0; } - return n; + + vec = std::move(maybe_vec.value()); + return vec.size(); } // 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; - } +bool mjXUtil::ReadAttrTxt(tinyxml2::XMLElement* elem, const char* attr, string& text, bool required) { + auto maybe_str = ReadAttrStr(elem, attr, required); + if (!maybe_str.has_value()) { + return false; } - // read text - text = string(pstr); - + text = maybe_str.value(); 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; - } + auto maybe_int = ReadAttrNum(elem, attr, required); + if (!maybe_int.has_value()) { + 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]; + *data = maybe_int.value(); return true; } @@ -796,15 +857,15 @@ XMLElement* mjXUtil::FindSubElem(XMLElement* elem, string name, bool required) { 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)) { + auto maybe_text = ReadAttrStr(elem, attr, required); + if (!maybe_text.has_value()) { return false; } // find keyword in map - int value = FindKey(map, mapSz, text); - if (value<0) { - throw mjXError(elem, "invalid keyword: '%s'", text.c_str()); + int value = FindKey(map, mapSz, maybe_text.value()); + if (value < 0) { + throw mjXError(elem, "invalid keyword: '%s'", maybe_text->c_str()); } // copy diff --git a/src/xml/xml_util.h b/src/xml/xml_util.h index 02c210a3..10b6f2db 100644 --- a/src/xml/xml_util.h +++ b/src/xml/xml_util.h @@ -16,7 +16,9 @@ #ifndef MUJOCO_SRC_XML_XML_UTIL_H_ #define MUJOCO_SRC_XML_XML_UTIL_H_ -// stl +#include +#include +#include #include #include #include @@ -98,21 +100,60 @@ class mjXUtil { // find value in map, return key ("": not found) static std::string FindValue(const mjMap* map, int mapsz, int value); - // read any type from attribute, return number read + // if attribute is present, return vector of numerical data + template + static std::optional> ReadAttrVec(tinyxml2::XMLElement* elem, const char* attr, + bool required = false); + + // if attribute is present, return attribute as a string + static std::optional ReadAttrStr(tinyxml2::XMLElement* elem, const char* attr, + bool required = false); + + // if attribute is present, return numerical value of attribute + template + static std::optional ReadAttrNum(tinyxml2::XMLElement* elem, const char* attr, + bool required = false); + + // if attribute is present, return array of numerical data + // N should be small as data is allocated on the stack + template + static std::optional> ReadAttrArr(tinyxml2::XMLElement* elem, const char* attr, + bool required = false) { + std::array arr; + int n = 0; + if (!ReadAttrValues(elem, attr, [&](int i, T num) { arr[i] = num; n++; }, N)) { + throw mjXError(elem, "attribute '%s' has too much data", attr); + } + + if (!n) { + if (required) { + throw mjXError(elem, "required attribute missing: '%s'", attr); + } else { + return std::nullopt; + } + } + + if (n < N) { + throw mjXError(elem, "attribute '%s' does not have enough data", attr); + } + return arr; + } + + // deprecated: use ReadAttrVec or ReadAttrArr template static int ReadAttr(tinyxml2::XMLElement* elem, const char* attr, const int len, T* data, std::string& text, bool required = false, bool exact = true); - // read DOUBLE array into C++ vector, return number read + // deprecated: use ReadAttrVec static int ReadVector(tinyxml2::XMLElement* elem, const char* attr, std::vector& vec, std::string& text, bool required = false); - // read text attribute + // deprecated: use ReadAttrStr static bool ReadAttrTxt(tinyxml2::XMLElement* elem, const char* attr, std::string& text, bool required = false); - // read int attribute + // deprecated: use ReadAttrNum static bool ReadAttrInt(tinyxml2::XMLElement* elem, const char* attr, int* data, bool required = false); @@ -155,6 +196,11 @@ class mjXUtil { // write attribute- keyword static void WriteAttrKey(tinyxml2::XMLElement* elem, std::string name, const mjMap* map, int mapsz, int data, int def = -12345); + + private: + template + static bool ReadAttrValues(tinyxml2::XMLElement* elem, const char* attr, + std::function push, int max = -1); }; #endif // MUJOCO_SRC_XML_XML_UTIL_H_ diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index f6c31bc3..8f53a99c 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -317,7 +317,7 @@ TEST_F(XMLReaderTest, InvalidArrayLength) { - +