Files
Mujoco_WASM/src/xml/xml_util.h
T
Yuval Tassa 790f8fac30 Generate the MJCF grammar table and enforce its constraints.
The hand-written MJCF[] table in xml_native_reader.cc is replaced by
mjcf_table.inc, emitted from mjcf.schema by generate_mjcf_table.py and
checked for freshness by doc_test. nMJCF is now self-sizing. The two
tables are identical as trees of (tag, cardinality, attribute-set);
within-row attribute order changes where the schema factors shared
groups and projects default-context rows, and top-level rows follow
the schema's dependency order -- neither affects validation, which is
set-based, nor XMLschema.rst, whose generator orders sections itself
(regenerated here, reading the .inc instead of the reader source).

The schema's constraint declarations become enforcement: the emitter
writes a companion MJCF_constraints[] array (row-indexed into MJCF[]),
and mjXSchema::Check evaluates each element's constraints after its
attribute check, with uniform messages derived from the declaration:
"at most one of 'fovy', 'sensorsize' can be specified", "attributes
'reftype', 'refname' must be specified together", and so on.
Multi-attribute bundles render as ('site1', 'site2').

Fifteen hand-written co-occurrence checks across fourteen elements are
deleted -- connect/weld semantics mixing and completeness, the actuator
transmission mutex, camera fovy/sensorsize, light directional/type,
inertial fullinertia-versus-orientation, rangefinder and the distance
family, contact's matching criteria, user-sensor pairing, the frame
family's reftype/refname, size memory exclusivities, mesh builtin
exclusions, and attach body/frame (newly declared). Tests assert the
uniform messages.

Two findings along the way: sensorsize-requires-resolution is a
value-level compiler rule (positive resolution), not a presence rule --
a presence constraint would be wrong and is not declared; and Size()'s
nstack/njmax range checks tested the spec value before assignment, so
they never validated the parsed value -- now they do.

Verified by compiling all 81 models in the model/ corpus.

PiperOrigin-RevId: 958064622
Change-Id: I802cf5c0aee08a62926e36a281320ff9e34c0668
2026-08-02 16:30:02 -07:00

247 lines
9.1 KiB
C++

// 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.
#ifndef MUJOCO_SRC_XML_XML_UTIL_H_
#define MUJOCO_SRC_XML_XML_UTIL_H_
#include <array>
#include <functional>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <sstream>
#include <mujoco/mujoco.h>
#include "user/user_util.h"
#include "tinyxml2.h"
// error string copy
void mjCopyError(char* dst, const char* src, int maxlen);
using tinyxml2::XMLElement;
XMLElement* FirstChildElement(XMLElement* e, const char* name = nullptr);
XMLElement* NextSiblingElement(XMLElement* e, const char* name = nullptr);
// XML Error info
class [[nodiscard]] mjXError {
public:
mjXError(const tinyxml2::XMLElement* elem = 0,
const char* msg = 0,
const char* str = 0,
int pos = 0);
~mjXError() = default;
char message[1000]; // error message
};
// presence constraint over an element's attributes, generated from the
// schema into the grammar table's companion array. spec holds attribute
// bundles, space-joined within a bundle and '|'-separated between them;
// kind: e=exclusive (at most one bundle present), t=together (all or
// none), r=requires (first needs second), o=oneof (at least one bundle
// complete).
struct mjXConstraintDef {
int row; // index of the element's row in the grammar table
char kind;
const char* spec;
};
// Custom XML file validation
class mjXSchema {
public:
mjXSchema(std::vector<const char*> schema[], unsigned nrow,
const mjXConstraintDef* constraints = nullptr,
int nconstraint = 0, int first_row = 0);
std::string GetError(); // return error
void Print(std::stringstream& str, int level) const; // print schema
void PrintHTML(std::stringstream& str, int level, bool pad) const;
bool NameMatch(tinyxml2::XMLElement* elem, int level); // does name match
tinyxml2::XMLElement* Check(tinyxml2::XMLElement* elem, int level); // validator
private:
std::string name_; // element name
char type_; // element type: '?', '!', '*', 'R'
std::set<std::string> attr_; // allowed attributes
std::vector<mjXSchema> subschema_; // allowed child elements
std::vector<const mjXConstraintDef*> constraints_; // constraints here
tinyxml2::XMLElement* CheckConstraints(tinyxml2::XMLElement* elem);
int refcnt_ = 0; // refcount used for validation
std::string error; // error from constructor or Check
};
// key(string) : value(int) map
struct _mjMap {
const char* key;
int value;
};
typedef struct _mjMap mjMap;
// XML read and write utility functions
class mjXUtil {
public:
mjXUtil() = default;
virtual ~mjXUtil() = default;
// compare two vectors
template<typename T>
static bool SameVector(const T* vec1, const T* vec2, int n);
// find key in map, return value (-1: not found)
static int FindKey(const mjMap* map, int mapsz, std::string key);
// find value in map, return key ("": not found)
static std::string FindValue(const mjMap* map, int mapsz, int value);
// if attribute is present, return vector of numerical data
template<typename T>
static std::optional<std::vector<T>> ReadAttrVec(tinyxml2::XMLElement* elem,
const char* attr,
bool required = false);
// if attribute is present, return attribute as a string
static std::optional<std::string> ReadAttrStr(tinyxml2::XMLElement* elem,
const char* attr,
bool required = false);
// if attribute is present, return attribute as a filename
static std::optional<mujoco::user::FilePath>
ReadAttrFile(tinyxml2::XMLElement* elem, const char* attr,
const mjVFS* vfs,
const mujoco::user::FilePath& dir = mujoco::user::FilePath(),
bool required = false);
// if attribute is present, return numerical value of attribute
template<typename T>
static std::optional<T> 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<typename T, int N>
static std::optional<std::array<T, N>> ReadAttrArr(tinyxml2::XMLElement* elem,
const char* attr,
bool required = false) {
std::array<T, N> arr;
int n = 0;
if (!ReadAttrValues<T>(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<typename T>
static int ReadAttr(tinyxml2::XMLElement* elem, const char* attr, int len,
T* data, std::string& text,
bool required = false, bool exact = true);
static int ReadQuat(tinyxml2::XMLElement* elem, const char* attr, double* data,
std::string& text, bool required = false);
// deprecated: use ReadAttrVec
static int ReadVector(tinyxml2::XMLElement* elem, const char* attr,
std::vector<double>& vec, std::string& text, bool required = false);
// deprecated: use ReadAttrStr
static bool ReadAttrTxt(tinyxml2::XMLElement* elem, const char* attr, std::string& text,
bool required = false);
// deprecated: use ReadAttrNum
static bool ReadAttrInt(tinyxml2::XMLElement* elem, const char* attr, int* data,
bool required = false);
// write vector<float> to string
static void Vector2String(std::string& txt, const std::vector<float>& vec, int ncol = 0);
// find subelement with given name, make sure it is unique
static tinyxml2::XMLElement* FindSubElem(tinyxml2::XMLElement* elem, std::string name,
bool required = false);
// find attribute, translate key, return int value
static bool MapValue(tinyxml2::XMLElement* elem, const char* attr, int* data,
const mjMap* map, int mapSz, bool required = false);
template <typename T>
static bool MapValue(tinyxml2::XMLElement* elem, const char* attr, T* data,
const mjMap* map, int mapSz, bool required = false) {
int value;
if (MapValue(elem, attr, &value, map, mapSz, required)) {
*data = static_cast<T>(value);
return true;
}
return false;
}
// find attribute, translate unique space-separated keys to data, return number of keys found
static int MapValues(tinyxml2::XMLElement* elem, const char* attr, int* data,
const mjMap* map, int mapSz, bool required = false);
// write attribute- any type
template<typename T>
static void WriteAttr(tinyxml2::XMLElement* elem, std::string name, int n, const T* data,
const T* def = 0, bool trim = false);
// write vector<double> attribute, with and without default
static void WriteVector(tinyxml2::XMLElement* elem, std::string name,
const std::vector<double>& vec);
static void WriteVector(tinyxml2::XMLElement* elem, std::string name,
const std::vector<double>& vec, const std::vector<double>& def);
// write attribute- string
static void WriteAttrTxt(tinyxml2::XMLElement* elem, std::string name, std::string value);
// write attribute- single int
static void WriteAttrInt(tinyxml2::XMLElement* elem, std::string name, int data, int def = -12345);
// write attribute- keyword
static void WriteAttrKey(tinyxml2::XMLElement* elem, std::string name,
const mjMap* map, int mapsz, int data, int def = -12345);
// write attribute- space-separated keywords
static void WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map,
int mapsz, int* data, int ndata, int def = -12345);
private:
template<typename T>
static bool ReadAttrValues(tinyxml2::XMLElement* elem, const char* attr,
std::function<void (int, T)> push, int max = -1);
};
#endif // MUJOCO_SRC_XML_XML_UTIL_H_