diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 36b39f99..c307dc1e 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -81,8 +81,7 @@ void ReadPluginConfigs(tinyxml2::XMLElement* elem, mjCPlugin* pp) { //---------------------------------- MJCF schema --------------------------------------------------- -static const int nMJCF = 227; -static const char* MJCF[nMJCF][mjXATTRNUM] = { +const char* MJCF[nMJCF][mjXATTRNUM] = { {"mujoco", "!", "1", "model"}, {"<"}, {"compiler", "*", "20", "autolimits", "boundmass", "boundinertia", "settotalmass", @@ -774,12 +773,6 @@ const mjMap flexself_map[5] = { // constructor mjXReader::mjXReader() : schema(MJCF, nMJCF) { - // check for schema construction error - if (!schema.GetError().empty()) { - throw mjXError(0, "Schema construction error: %s", - schema.GetError().c_str()); - } - readingdefaults = false; } diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index 459d0a06..43b0caab 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -17,10 +17,15 @@ #include -#include "user/user_model.h" -#include "xml/xml_base.h" #include "tinyxml2.h" +#include +#include "user/user_api.h" +#include "user/user_model.h" +#include "user/user_objects.h" +#include "xml/xml_base.h" +#include "xml/xml_util.h" + class mjXReader : public mjXBase { public: mjXReader(); // constructor @@ -74,7 +79,11 @@ class mjXReader : public mjXBase { mjCDef* GetClass(tinyxml2::XMLElement* section); // get default class name static void GetXMLPos(tinyxml2::XMLElement* elem, mjCBase* obj); // get xml position - bool readingdefaults; // true while reading defaults + bool readingdefaults; // true while reading defaults }; +// MJCF schema +#define nMJCF 227 +extern const char* MJCF[nMJCF][mjXATTRNUM]; + #endif // MUJOCO_SRC_XML_XML_NATIVE_READER_H_ diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index a58487d6..df80e533 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -157,56 +157,7 @@ XMLElement* NextSiblingElement(XMLElement* e, const char* name) { } // constructor -mjXSchema::mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow, bool checkptr) { - if (schema[0][0][0] == '<' || schema[0][0][0] == '>') { - throw "expected element, found bracket"; - } - - // check entire schema for null pointers - if (checkptr) { - char msg[100]; - - for (int i=0; i mjXATTRNUM-3) { - mju::sprintf_arr(msg, - "invalid number of attributes in row %d, element %s", i, schema[i][0]); - throw msg; - } - - // attribute pointers - for (int j=0; j1) { - // check for bracketed block - if (schema[1][0][0]!='<' || schema[nrow-1][0][0]!='>') { - throw "expected brackets after complex element"; - } - + if (nrow > 1) { // 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]=='<') { + if (schema[start+1][0][0] == '<') { // look for corresponding closing bracket int cnt = 0; while (end <= nrow-1) { - if (schema[end][0][0]=='<') { + if (schema[end][0][0] == '<') { cnt++; - } else if (schema[end][0][0]=='>') { + } else if (schema[end][0][0] == '>') { cnt--; - if (cnt==0) { + if (cnt == 0) { break; } } end++; } - - // closing bracket not found - if (end > nrow-1) { - throw "matching closing bracket not found"; - } } - // add element, check for error - subschema_.emplace_back(schema+start, end-start+1, false); + // add child element + subschema_.emplace_back(schema+start, end-start+1); // proceed with next subelement start = end+1; diff --git a/src/xml/xml_util.h b/src/xml/xml_util.h index 43f71290..875eb5a4 100644 --- a/src/xml/xml_util.h +++ b/src/xml/xml_util.h @@ -56,7 +56,7 @@ class [[nodiscard]] mjXError { // Custom XML file validation class mjXSchema { public: - mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow, bool checkptr = true); + mjXSchema(const char* schema[][mjXATTRNUM], unsigned nrow); std::string GetError(); // return error void Print(std::stringstream& str, int level) const; // print schema diff --git a/test/xml/xml_schema_test.cc b/test/xml/xml_schema_test.cc new file mode 100644 index 00000000..6b92747e --- /dev/null +++ b/test/xml/xml_schema_test.cc @@ -0,0 +1,135 @@ +// Copyright 2024 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 "src/xml/xml_native_reader.h" +#include "src/xml/xml_util.h" +#include "test/fixture.h" +#include +#include + +namespace mujoco { +namespace { + + +using XMLSchemaTest = MujocoTest; + +static std::optional IsValidSchemaFormat( + const char* schema[][mjXATTRNUM], unsigned nrow) { + if (schema[0][0][0] == '<' || schema[0][0][0] == '>') { + return "expected element, found bracket"; + } + + // check entire schema for null pointers + for (int i = 0; i < nrow; i++) { + // base pointers + if (!schema[i][0]) { + return "null pointer found in row " + std::to_string(i); + } + + // detect element + if (schema[i][0][0] != '<' && schema[i][0][0] != '>') { + // first 3 pointers required + if (!schema[i][1] || !schema[i][2]) { + return absl::StrFormat("expected element, found null pointers" + "in row %d, element %s", i, schema[i][0]); + } + + // check type + if (schema[i][1][0] != '!' && schema[i][1][0] != '?' && + schema[i][1][0] != '*' && schema[i][1][0] != 'R') { + return absl::StrFormat("invalid type in row %d, element %s", + i, schema[i][0]); + } + + // number of attributes + int nattr = 0; + if (!absl::SimpleAtoi(schema[i][2], &nattr)) { + return absl::StrFormat("unparseable number of attributes in" + " row %d, element %s", i, schema[i][0]); + } else if (nattr < 0 || nattr > mjXATTRNUM - 3) { + return absl::StrFormat("invalid number of attributes in" + " row %d, element %s", i, schema[i][0]); + } + + // attribute pointers + for (int j = 0; j < nattr; j++) { + if (!schema[i][3 + j]) { + return absl::StrFormat("null attribute %d in" + " row %d, element %s", j, i, schema[i][0]); + } + } + } + } + + // process sub-elements of complex element + if (nrow > 1) { + // check for bracketed block + if (schema[1][0][0] != '<' || schema[nrow - 1][0][0] != '>') { + return "expected brackets after complex element"; + } + + // parse block into simple and complex elements + 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) { + return "matching closing bracket not found"; + } + } + + // recursively check for error + auto error = IsValidSchemaFormat(schema + start, end - start + 1); + if (error.has_value()) { + return error; + } + + // proceed with next subelement + start = end + 1; + } + } + return std::nullopt; +} + +TEST_F(XMLSchemaTest, MjcfSchemaTest) { + auto error = IsValidSchemaFormat(MJCF, nMJCF); + auto error_msg = error.value_or(""); + + EXPECT_EQ(error_msg, ""); + ASSERT_FALSE(error.has_value()); +} + +} // namespace +} // namespace mujoco