From 67f0f5154ff4eeaaaa690d2a66cec2d3a8ed386e Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Fri, 5 May 2023 07:02:51 -0700 Subject: [PATCH] Add various improvements for resource providers. PiperOrigin-RevId: 529706437 Change-Id: I20385e031446674584349c301982dbe13b812477 --- doc/APIreference/APItypes.rst | 70 ++++++++++++ doc/changelog.rst | 3 +- doc/includes/references.h | 14 ++- doc/programming/extension.rst | 131 +++++++++++++++++++++- include/mujoco/mjmodel.h | 3 + include/mujoco/mjplugin.h | 17 ++- src/engine/engine_io.c | 4 +- src/engine/engine_plugin.cc | 86 +++++++++++---- src/engine/engine_resource.c | 65 +++++++++-- src/engine/engine_resource.h | 6 + src/engine/engine_vfs.c | 13 +++ src/user/user_mesh.cc | 8 +- src/user/user_model.cc | 14 +-- src/user/user_model.h | 6 +- src/user/user_objects.cc | 28 ++--- src/user/user_objects.h | 16 +-- src/user/user_util.cc | 26 ++--- src/user/user_util.h | 3 - src/xml/xml.cc | 36 +++--- src/xml/xml.h | 2 +- src/xml/xml_api.cc | 6 +- test/engine/engine_resource_test.cc | 164 ++++++++++++++++++++++------ test/fixture.cc | 48 +++----- 23 files changed, 576 insertions(+), 193 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index f24b71b1..b0329c49 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -671,6 +671,17 @@ information is then filled-in by the simulator. .. mujoco-include:: mjContact +.. _mjResource: + +mjResource +~~~~~~~~~~ + +A resource is an abstraction of a file in a filesystem. The name field is the unique name of the resource while the +other fields are populated by a :ref:`resource provider `. + +.. mujoco-include:: mjResource + + .. _mjVFS: mjVFS @@ -960,7 +971,15 @@ triggered by the compiler and the engine during various phases of the computatio .. mujoco-include:: mjpPlugin +.. _mjpResourceProvider: +mjpResourceProvider +~~~~~~~~~~~~~~~~~~~ + +This data structure contains the definition of a :ref:`resource provider `. It contains a set of callbacks +used for opening and reading resources. + +.. mujoco-include:: mjpResourceProvider .. _tyFunction: @@ -1074,6 +1093,57 @@ mjfItemEnable This is the function type of the predicate function used by the UI framework to determine if each item is enabled or disabled. +.. _tyRPCallbacks: + +Resource Provider Callbacks +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These callbacks are used by :ref:`resource providers`. + +.. _mjfOpenResource: + +mjfOpenResource +~~~~~~~~~~~~~~~ + +.. code-block:: C + + typedef int (*mjfOpenResource)(mjResource* resource); + +This callback is for opeing a resource; returns zero on failure. + +.. _mjfReadResource: + +mjfReadResource +~~~~~~~~~~~~~~~ + +.. code-block:: C + + typedef int (*mjfReadResource)(mjResource* resource, const void** buffer); + +This callback is for reading a resource. Returns number of bytes stored in buffer and returns -1 on error. + +.. _mjfCloseResource: + +mjfCloseResource +~~~~~~~~~~~~~~~~ + +.. code-block:: C + + typedef void (*mjfCloseResource)(mjResource* resource); + +This callback is for closing a resource, and is responsible for freeing any allocated memory. + +.. _mjfGetResourceDir: + +mjfGetResourceDir +~~~~~~~~~~~~~~~~~ + +.. code-block:: C + + typedef void (*mjfGetResourceDir)(mjResource* resource, const char** dir, int* ndir); + +This callback is for returning the directory of a resource, by setting dir to the directory string with ndir being size +of directory string. .. _tyNotes: diff --git a/doc/changelog.rst b/doc/changelog.rst index cf34d0d9..c99e8008 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -26,6 +26,7 @@ General - Added :ref:`mj_multiRay` function for intersecting multiple rays emanating from a single point. This is significantly faster than calling :ref:`mj_ray` multiple times. - Increased ``mjMAXUIITEM`` (maximum number of UI elements per section in Simulate) to 100. +- Added :ref:`documentation` for resource providers. Version 2.3.5 (April 25, 2023) ------------------------------ @@ -66,7 +67,7 @@ General #. Added midphase and broadphase collision statistics to :ref:`mjData`. #. Added documentation for :ref:`engine plugins`. #. Added struct information to the ``introspect`` module. -#. Added a new extension mechanism called "resource provider" . This extensible mechanism allows MuJoCo +#. Added a new extension mechanism called :ref:`resource providers`. This extensible mechanism allows MuJoCo to read assets from data sources other than the local OS filesystem or the :ref:`Virtual file system`. diff --git a/doc/includes/references.h b/doc/includes/references.h index decaea18..0a8ae066 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -624,6 +624,9 @@ struct mjResource_ { // closing callback from resource provider void (*close)(struct mjResource_* resource); + + // getdir callback from resource provider + void (*getdir)(struct mjResource_* resource, const char** dir, int* ndir); }; typedef struct mjResource_ mjResource; struct mjOption_ { // physics options @@ -1198,11 +1201,12 @@ struct mjModel_ { }; typedef struct mjModel_ mjModel; struct mjpResourceProvider_ { - const char* prefix; // prefix for match against a resource name - mjfOpenResource open; // opening callback - mjfReadResource read; // reading callback - mjfCloseResource close; // closing callback - void* data; // opaque data pointer (resource invariant) + const char* prefix; // prefix for match against a resource name + mjfOpenResource open; // opening callback + mjfReadResource read; // reading callback + mjfCloseResource close; // closing callback + mjfGetResourceDir getdir; // getdir callback (optional) + void* data; // opaque data pointer (resource invariant) }; typedef struct mjpResourceProvider_ mjpResourceProvider; typedef enum mjtPluginCapabilityBit_ { diff --git a/doc/programming/extension.rst b/doc/programming/extension.rst index 8a264508..22b3ce4a 100644 --- a/doc/programming/extension.rst +++ b/doc/programming/extension.rst @@ -3,8 +3,8 @@ Extensions ---------- -This section describes MuJoCo's mechanisms for user-authored extensions. At present, extensibility is provided only -via **engine plugins**. +This section describes MuJoCo's mechanisms for user-authored extensions. At present, extensibility is provided by +via :ref:`engine plugins` and :ref:`resource providers`. .. _exPlugin: @@ -246,3 +246,130 @@ A future version of this section will include: * How to declare custom MJCF attributes for a plugin. * Things that developers need to keep in mind in order to ensure that plugins function correctly when :ref:`mjData` is copied, stepped, or reset. + +.. _exProvider: + +Resource providers +~~~~~~~~~~~~~~~~~~ + +Resource providers extend MuJoCo to load assets (XML files, meshes, textures, and etc.) that don't necessarily come from +the OS filesystem or the Virtual File System (:ref:`mjVFS`). For example, downloading assets from the Internet could be +implemented as a resource provider. These extensions are handle abstractly in MuJoCo via the :ref:`mjResource` struct. + +.. _exProviderStructure: + +Overview +^^^^^^^^ + +Creating a new resource provider works by registering a :ref:`mjpResourceProvider` struct via +:ref:`mjp_registerResourceProvider` in a global table. Once a resource provider is registered it can be used by all +loading functions. The :ref:`mjpResourceProvider` struct stores three types of fields: + +.. _Uniform Resource Identifier: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier + +Resource prefix + + Resources are identified by prefixes in their name. The chosen prefix should have a valid `Uniform Resource + Identifier`_ (URI) scheme syntax. Resource names should also have a valid URI syntax, however this isn't enforced. A + resource name with the syntax ``{prefix}:{filename}`` will match a provider using the scheme ``prefix``. For + instance, a resource provider accessing assets via the Internet might use ``http`` as its scheme. In this case a + resource with the name ``http://www.example.com/myasset.obj`` would match against this resource provider. Schemes are + case-insensitive so that ``HTTP://www.example.com/myasset.obj`` will also match. Note the importance of the colon. + URI syntax requires that a colon follows the prefix in a resource name in order to match against a scheme. For example + ``https://www.example.com/myasset.obj`` would NOT be a match since the scheme is designated as ``https``. + +Callbacks + There are three callbacks that a resource provider is required to implement: :ref:`open`, + :ref:`read`, and :ref:`close`. A fourth callback :ref:`getdir` + which is optional. More details on these callbacks are given below. + +Data Pointer + Lastly, there's an opaque data pointer for the provider to pass data into the callbacks. This data pointer is constant + within a given model. + +Resource providers work via callbacks: + +- :ref:`mjfOpenResource`: The open callback takes a single parameter of type :ref:`mjResource`. The + name field of the resource should be used to verify that the resource exists and populate the resource data field with + any extra information needed for the resource. On failure this callback should return 0 (false) or else 1 (true). +- :ref:`mjfReadResource`: The read callback takes as arguments a :ref:`mjResource` and a pointer to a + void pointer called the ``buffer``. The read callback should point the ``buffer`` pointer to the location of where the + bytes of the resource can be read and return the number of bytes pointed to in the ``buffer``. On failure, this + callback should return -1. +- :ref:`mjfCloseResource`: This callback takes a single parameter of type :ref:`mjResource`, and + should be used to free any memory allocated in the data field in the supplied resource. +- :ref:`mjfGetResourceDir`: This callback is optional and is used to extract the directory from a + resource name. For example, the resource name ``http://www.example.com/myasset.obj`` would have + ``http://www.example.com/`` as its directory. +.. _exProviderUsage: + +Usage +^^^^^ + +When a resource provider is registered, it can be used immediately to open assets. If the asset filename has a prefix +that matches with the prefix of a registered provider, then that provider will be used to load the asset. + +.. _exProviderExample: + +Example +""""""" + +.. _data URI scheme: https://en.wikipedia.org/wiki/Data_URI_scheme + +This section provides a basic example of a resource provider that reads from a `data URI scheme`_. First we implement +the callbacks: + +.. code-block:: C + + int data_open_callback(mjResource* resource) { + // call some util function to validate + if (!is_valid_data_uri(resource->name)) { + return 0; // return failure + } + + // some upper bound for the data + resource->data = mju_malloc(get_data_uri_size(resource->name)); + if (resource->data == NULL) { + return 0; // return failure + } + + // fill data from string (some util function) + get_data_uri(resource->name, &data); + } + + int str_read_callback(mjResource* resource, const void** buffer) { + *buffer = resource->data; + return get_data_uri_size(resource->name); + } + + void str_close_callback(mjResource* resource) { + mju_free(resource->data); + } + +Next we create the resource provider and register it with MuJoCo: + +.. code-block:: C + + mjpResourceProvider resourceProvider = { + .prefix = "data", + .open = str_open_callback, + .read = str_read_callback, + .close = str_close_callback, + .getdir = NULL + }; + + // return positive number on success + if (!mjp_registerResourceProvider(&resourceProvider)) { + // ... + // return failure + } + +Now we can write assets as strings in our MJCF files: + +.. code-block:: xml + + + + + ... + diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 3eb59be9..9fff6995 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -390,6 +390,9 @@ struct mjResource_ { // closing callback from resource provider void (*close)(struct mjResource_* resource); + + // getdir callback from resource provider + void (*getdir)(struct mjResource_* resource, const char** dir, int* ndir); }; typedef struct mjResource_ mjResource; diff --git a/include/mujoco/mjplugin.h b/include/mujoco/mjplugin.h index e0fe7d24..11acf84f 100644 --- a/include/mujoco/mjplugin.h +++ b/include/mujoco/mjplugin.h @@ -22,7 +22,7 @@ //---------------------------------- Resource Provider --------------------------------------------- -#define mjVFS_PREFIX "vfs://" // prefix for VFS providers +#define mjVFS_PREFIX "vfs" // prefix for VFS providers // callback for opeing a resource, returns zero on failure typedef int (*mjfOpenResource)(mjResource* resource); @@ -34,13 +34,18 @@ typedef int (*mjfReadResource)(mjResource* resource, const void** buffer); // callback for closing a resource (responsible for freeing any allocated memory) typedef void (*mjfCloseResource)(mjResource* resource); +// callback for returning the directory of a resource +// sets dir to directory string with ndir being size of directory string +typedef void (*mjfGetResourceDir)(mjResource* resource, const char** dir, int* ndir); + // struct describing a single resource provider struct mjpResourceProvider_ { - const char* prefix; // prefix for match against a resource name - mjfOpenResource open; // opening callback - mjfReadResource read; // reading callback - mjfCloseResource close; // closing callback - void* data; // opaque data pointer (resource invariant) + const char* prefix; // prefix for match against a resource name + mjfOpenResource open; // opening callback + mjfReadResource read; // reading callback + mjfCloseResource close; // closing callback + mjfGetResourceDir getdir; // getdir callback (optional) + void* data; // opaque data pointer (resource invariant) }; typedef struct mjpResourceProvider_ mjpResourceProvider; diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 1cb806cf..a0af0e01 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -621,7 +621,7 @@ void mj_saveModel(const mjModel* m, const char* filename, void* buffer, int buff // load model from binary MJB resource -static mjModel* _mj_loadModel(const char* filename, int default_provider) { +static mjModel* _mj_loadModel(const char* filename, int vfs_provider) { int header[4] = {0}; int expected_header[4] = {ID, sizeof(mjtNum), getnint(), getnptr()}; int info[2000]; @@ -629,7 +629,7 @@ static mjModel* _mj_loadModel(const char* filename, int default_provider) { mjModel *m = 0; mjResource* r = NULL; - if((r = mju_openResource(filename, default_provider)) == NULL) { + if((r = mju_openResource(filename, vfs_provider)) == NULL) { return NULL; } diff --git a/src/engine/engine_plugin.cc b/src/engine/engine_plugin.cc index d6375856..50379d12 100644 --- a/src/engine/engine_plugin.cc +++ b/src/engine/engine_plugin.cc @@ -21,6 +21,7 @@ #include "engine/engine_plugin.h" #include +#include #include #include #include @@ -266,14 +267,53 @@ bool PluginsAreIdentical(const mjpPlugin& plugin1, const mjpPlugin& plugin2) { return !std::memcmp(ptr1, ptr2, remaining_size); } +// does case insensitive comparison +bool PrefixesAreIdentical(const char* p1, const char* p2) { + int i = 0; + for (; p1[i] != '\0'; i++) { + if (std::tolower(p1[i]) != std::tolower(p2[i])) { + return false; + } + } + + return p2[i] == '\0'; +} + // check if two resource providers are identical bool ResourceProvidersAreIdentical(const mjpResourceProvider* p1, const mjpResourceProvider* p2) { - return (!std::strcmp(p1->prefix, p2->prefix) && + return (PrefixesAreIdentical(p1->prefix, p2->prefix) && p1->open == p2->open && p1->read == p2->read && p1->close == p2->close && p1->data == p2->data); } + +// check if prefix is a valid URI scheme format +bool IsValidURISchemeFormat(const char* prefix) { + int len; + + // prefix is NULL or empty + if (prefix == nullptr || !(len = std::strlen(prefix))) { + return false; + } + + // first character must be a letter + if (!std::isalpha(prefix[0])) { + return false; + } + + for (int i = 1; i < len; i++) { + // each following character must be a letter, digit, '+', '.', or '-' + if (!std::isalnum(prefix[i]) && + (prefix[i] != '+') && + (prefix[i] != '.') && + (prefix[i] != '-')) { + return false; + } + } + return true; +} + } // namespace // globally register a plugin (thread-safe), return new slot id @@ -508,12 +548,7 @@ void mjp_defaultResourceProvider(mjpResourceProvider* provider) { // globally register a resource provider (thread-safe), return new slot id int mjp_registerResourceProvider(const mjpResourceProvider* provider) { // check against reserved prefixes - int n = std::strlen(provider->prefix), - m = std::strlen(kVfsPrefix); - - // one of the prefixes is a subprefix of the other - if (!std::strncmp(kVfsPrefix, provider->prefix, n) || - !std::strncmp(kVfsPrefix, provider->prefix, m)) { + if (PrefixesAreIdentical(kVfsPrefix, provider->prefix)) { mju_warning("provider->prefix is '%s' which is reserved", provider->prefix); return -1; } @@ -523,8 +558,10 @@ int mjp_registerResourceProvider(const mjpResourceProvider* provider) { // internal version of mjp_registerResourceProvider without prechecks on reserved prefixes int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) { - if (!provider->prefix || provider->prefix[0] == '\0') { - mju_warning("provider->prefix is an empty string"); + // check if prefix is valid URI scheme format + if (!IsValidURISchemeFormat(provider->prefix)) { + mju_warning("provider->prefix is '%s' which is not a valid URI scheme format", + provider->prefix); return -1; } @@ -544,7 +581,7 @@ int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) { std::unique_ptr prefix; // check if this is a VFS provider - if (!std::strcmp(mjVFS_PREFIX, provider->prefix)) { + if (PrefixesAreIdentical(mjVFS_PREFIX, provider->prefix)) { vfs_provider = true; } @@ -589,13 +626,8 @@ int mjp_registerResourceProviderInternal(const mjpResourceProvider* provider) { } if (!vfs_provider && existing.prefix != nullptr) { - int n = std::strlen(provider->prefix); - int m = std::strlen(existing.prefix); - - // one of the prefixes is a subprefix of the other - if (!std::strncmp(existing.prefix, provider->prefix, n) || - !std::strncmp(existing.prefix, provider->prefix, m)) { - // if identical then return slot number + // if identical then return slot number + if (PrefixesAreIdentical(provider->prefix, existing.prefix)) { if (ResourceProvidersAreIdentical(provider, &existing)) { return i; } else { @@ -691,20 +723,34 @@ int mjp_resourceProviderCount() { return GetGlobal().count().load(std::memory_order_acquire); } -// look up a resource provider that matches its prefix against the given resource name +// look up a resource provider that matches its prefix against the given resource scheme const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name) { const int count = mjp_resourceProviderCount(); if (!resource_name || !resource_name[0]) { return nullptr; } + const char* ch = std::strchr(resource_name, ':'); + if (ch == nullptr) { + return nullptr; + } + + int n = ch - resource_name; + std::string file_prefix = std::string(resource_name, n); + + // return NULL if file_prefix doesn't have a valid URI scheme syntax + if (!IsValidURISchemeFormat(file_prefix.c_str())) { + return nullptr; + } + // since multiple VFS resource providers can be registered with the same // prefix, it doesn't make sense to try to match against them - if (!std::strncmp(kVfsPrefix, resource_name, std::strlen(kVfsPrefix))) { + if (PrefixesAreIdentical(kVfsPrefix, file_prefix.c_str())) { return nullptr; } Global& global = GetGlobal(); + auto lock = global.lock_mutex_exclusively(); PluginTable* table = &global.table(); int found_slot = 0; @@ -717,7 +763,7 @@ const mjpResourceProvider* mjp_getResourceProvider(const char* resource_name) { const char *prefix = provider.prefix; if (prefix != nullptr && - !std::strncmp(prefix, resource_name, std::strlen(prefix))) { + PrefixesAreIdentical(prefix, file_prefix.c_str())) { return &provider; } } diff --git a/src/engine/engine_resource.c b/src/engine/engine_resource.c index c81f5aea..948d13a6 100644 --- a/src/engine/engine_resource.c +++ b/src/engine/engine_resource.c @@ -30,6 +30,21 @@ typedef struct { int nbuffer; } file_buffer; +// helper function to fill data from resource provider into provider +static void fillResource(const mjpResourceProvider* provider, mjResource* resource) { + if (provider == NULL) { + resource->read = NULL; + resource->close = NULL; + resource->getdir = NULL; + resource->provider_data = NULL; + } else { + resource->read = provider->read; + resource->close = provider->close; + resource->getdir = provider->getdir; + resource->provider_data = provider->data; + } +} + // open the given resource; if the name doesn't have a prefix matching with a // resource provider, then the default_provider is used @@ -54,9 +69,7 @@ mjResource* mju_openResource(const char* name, int default_provider) { // find provider based off prefix of name provider = mjp_getResourceProvider(name); if (provider != NULL) { - resource->read = provider->read; - resource->close = provider->close; - resource->provider_data = provider->data; + fillResource(provider, resource); if (provider->open(resource)) { return resource; } @@ -79,9 +92,8 @@ mjResource* mju_openResource(const char* name, int default_provider) { mju_free(resource); return NULL; } - resource->read = provider->read; - resource->close = provider->close; - resource->provider_data = provider->data; + + fillResource(provider, resource); if (provider->open(resource)) { return resource; } @@ -96,9 +108,7 @@ mjResource* mju_openResource(const char* name, int default_provider) { // lastly fallback to OS filesystem else { - resource->read = NULL; - resource->close = NULL; - resource->provider_data = NULL; + fillResource(NULL, resource); resource->data = mju_malloc(sizeof(file_buffer)); file_buffer* fb = (file_buffer*) resource->data; fb->buffer = mju_fileToMemory(name, &(fb->nbuffer)); @@ -152,7 +162,7 @@ int mju_readResource(mjResource* resource, const void** buffer) { } - // if provider is NULL, then OS filesystem is used + // if provider read callback is NULL, then OS filesystem is used const file_buffer* fb = (file_buffer*) resource->data; *buffer = fb->buffer; return fb->nbuffer; @@ -160,6 +170,41 @@ int mju_readResource(mjResource* resource, const void** buffer) { +// get directory path of resource +void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) { + *dir = NULL; + *ndir = 0; + + if (resource == NULL) { + return; + } + + // provider is not OS filesystem + if (resource->read) { + if (resource->getdir) { + resource->getdir(resource, dir, ndir); + } + } else { + mju_getfiledir(resource->name, dir, ndir); + } +} + + + +// get directory path of file in OS filesystem +void mju_getfiledir(const char* filename, const char** dir, int* ndir) { + *dir = NULL; + *ndir = 0; + char* ch; + + if ((filename != NULL) && + ((ch = strrchr(filename, '/')) || + (ch = strrchr(filename, '\\')))) { + *dir = filename; + *ndir = (int)(ch - filename) + 1; + } +} + // read file into memory buffer (allocated here with mju_malloc) void* mju_fileToMemory(const char* filename, int* filesize) { // open file diff --git a/src/engine/engine_resource.h b/src/engine/engine_resource.h index 0214fed7..a29d4de8 100644 --- a/src/engine/engine_resource.h +++ b/src/engine/engine_resource.h @@ -34,6 +34,12 @@ MJAPI void mju_closeResource(mjResource* resource); // return negative value if error MJAPI int mju_readResource(mjResource* resource, const void** buffer); +// sets for a resource with a name partitioned as {dir}{filename}, the dir and ndir pointers +MJAPI void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir); + +// get directory of a file in the OS filesystem +void mju_getfiledir(const char* filename, const char** dir, int* ndir); + // read file into memory buffer (allocated here with mju_malloc) void* mju_fileToMemory(const char* filename, int* filesize); diff --git a/src/engine/engine_vfs.c b/src/engine/engine_vfs.c index 75b63125..570472cc 100644 --- a/src/engine/engine_vfs.c +++ b/src/engine/engine_vfs.c @@ -251,6 +251,18 @@ static void vfs_close_callback(mjResource* resource) { +// getdir callback for the VFS resource provider +static void vfs_getdir_callback(mjResource* resource, const char** dir, int* ndir) { + *dir = NULL; + *ndir = 0; + + if (resource) { + mju_getfiledir(resource->name, dir, ndir); + } +} + + + // registers a VFS resource provider; returns the index of the provider int mj_registerVfsProvider(const mjVFS* vfs) { mjpResourceProvider provider = { @@ -258,6 +270,7 @@ int mj_registerVfsProvider(const mjVFS* vfs) { .open = &vfs_open_callback, .read = &vfs_read_callback, .close = &vfs_close_callback, + .getdir = &vfs_getdir_callback, .data = (void*) vfs }; diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 614c65ed..ed382f5c 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -176,7 +176,7 @@ template static T* VecToArray(std::vector& vector, bool clear = // compiler -void mjCMesh::Compile(int default_provider) { +void mjCMesh::Compile(int vfs_provider) { // load file if (!file.empty()) { // remove path from file if necessary @@ -193,7 +193,7 @@ void mjCMesh::Compile(int default_provider) { } string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); - mjResource* resource = LoadResource(filename, default_provider); + mjResource* resource = LoadResource(filename, vfs_provider); try { if (!strcasecmp(ext.c_str(), ".stl")) { @@ -1535,7 +1535,7 @@ mjCSkin::~mjCSkin() { // compiler -void mjCSkin::Compile(int default_provider) { +void mjCSkin::Compile(int vfs_provider) { // load file if (!file.empty()) { @@ -1564,7 +1564,7 @@ void mjCSkin::Compile(int default_provider) { } string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); - mjResource* resource = LoadResource(filename, default_provider); + mjResource* resource = LoadResource(filename, vfs_provider); try { LoadSKN(resource); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index e486cdcd..831473f1 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2393,7 +2393,7 @@ static void warninghandler(const char* msg) { // compiler -mjModel* mjCModel::Compile(int default_provider) { +mjModel* mjCModel::Compile(int vfs_provider) { // The volatile keyword is necessary to prevent a possible memory leak due to // an interaction between longjmp and compiler optimization. Specifically, at // the point where the setjmp takes places, these pointers have never been @@ -2424,7 +2424,7 @@ mjModel* mjCModel::Compile(int default_provider) { // TryCompile resulted in an mju_error which was converted to a longjmp. throw mjCError(0, "engine error: %s", errortext); } - TryCompile(*const_cast(&m), *const_cast(&data), default_provider); + TryCompile(*const_cast(&m), *const_cast(&data), vfs_provider); } catch (mjCError err) { // deallocate everything allocated in Compile mj_deleteModel(m); @@ -2450,7 +2450,7 @@ mjModel* mjCModel::Compile(int default_provider) { } -void mjCModel::TryCompile(mjModel*& m, mjData*& d, int default_provider) { +void mjCModel::TryCompile(mjModel*& m, mjData*& d, int vfs_provider) { // check if nan test works double test = mjNAN; if (mjuu_defined(test)) { @@ -2527,7 +2527,7 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, int default_provider) { // compile meshes (needed for geom compilation) for (int i=0; iCompile(default_provider); + meshes[i]->Compile(vfs_provider); } // automatically set nuser fields @@ -2586,9 +2586,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, int default_provider) { } // compile all other objects except for keyframes - for (int i=0; iCompile(default_provider); - for (int i=0; iCompile(default_provider); - for (int i=0; iCompile(default_provider); + for (int i=0; iCompile(vfs_provider); + for (int i=0; iCompile(vfs_provider); + for (int i=0; iCompile(vfs_provider); for (int i=0; iCompile(); for (int i=0; iCompile(); for (int i=0; iCompile(); diff --git a/src/user/user_model.h b/src/user/user_model.h index 41cc614c..bd82b99b 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -65,7 +65,7 @@ class mjCModel { mjCModel(); // constructor ~mjCModel(); // destructor - mjModel* Compile(int default_provider = 0); // COMPILER: construct mjModel + mjModel* Compile(int vfs_provider = 0); // COMPILER: construct mjModel bool CopyBack(const mjModel*); // DECOMPILER: copy numeric back void FuseStatic(void); // fuse static bodies with parent void FuseReindex(mjCBody* body); // reindex elements during fuse @@ -163,8 +163,8 @@ class mjCModel { int nuser_sensor; // number of mjtNums in sensor_user private: - void TryCompile(mjModel*& m, mjData*& d, int default_provider); - mjModel* _Compile(int default_provider); + void TryCompile(mjModel*& m, mjData*& d, int vfs_provider); + mjModel* _Compile(int vfs_provider); void Clear(void); // clear objects allocated by Compile diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 3289d0cf..71c99ad4 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -2114,7 +2114,7 @@ void mjCHField::LoadPNG(mjResource* resource) { // compiler -void mjCHField::Compile(int default_provider) { +void mjCHField::Compile(int vfs_provider) { // check size parameters for (int i=0; i<4; i++) if (size[i]<=0) @@ -2136,7 +2136,7 @@ void mjCHField::Compile(int default_provider) { // make filename string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); - mjResource* resource = LoadResource(filename, default_provider); + mjResource* resource = LoadResource(filename, vfs_provider); // load depending on format string ext = mjuu_getext(filename); @@ -2530,12 +2530,12 @@ void mjCTexture::LoadCustom(mjResource* resource, // load from PNG or custom file, flip if specified -void mjCTexture::LoadFlip(string filename, int default_provider, +void mjCTexture::LoadFlip(string filename, int vfs_provider, std::vector& image, unsigned int& w, unsigned int& h) { // dispatch to PNG or Custom loaded string ext = mjuu_getext(filename); - mjResource* resource = LoadResource(filename, default_provider); + mjResource* resource = LoadResource(filename, vfs_provider); try { if (!strcasecmp(ext.c_str(), ".png")) { @@ -2597,11 +2597,11 @@ void mjCTexture::LoadFlip(string filename, int default_provider, // load 2D -void mjCTexture::Load2D(string filename, int default_provider) { +void mjCTexture::Load2D(string filename, int vfs_provider) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, default_provider, image, w, h); + LoadFlip(filename, vfs_provider, image, w, h); // assign size width = w; @@ -2620,7 +2620,7 @@ void mjCTexture::Load2D(string filename, int default_provider) { // load cube or skybox from single file (repeated or grid) -void mjCTexture::LoadCubeSingle(string filename, int default_provider) { +void mjCTexture::LoadCubeSingle(string filename, int vfs_provider) { // check gridsize if (gridsize[0]<1 || gridsize[1]<1 || gridsize[0]*gridsize[1]>12) { throw mjCError(this, @@ -2631,7 +2631,7 @@ void mjCTexture::LoadCubeSingle(string filename, int default_provider) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, default_provider, image, w, h); + LoadFlip(filename, vfs_provider, image, w, h); // check gridsize for compatibility if (w/gridsize[1]!=h/gridsize[0] || (w%gridsize[1]) || (h%gridsize[0])) { @@ -2720,7 +2720,7 @@ void mjCTexture::LoadCubeSingle(string filename, int default_provider) { // load cube or skybox from separate file -void mjCTexture::LoadCubeSeparate(int default_provider) { +void mjCTexture::LoadCubeSeparate(int vfs_provider) { // keep track of which faces were defined int loaded[6] = {0, 0, 0, 0, 0, 0}; @@ -2738,7 +2738,7 @@ void mjCTexture::LoadCubeSeparate(int default_provider) { // load PNG or custom unsigned int w, h; std::vector image; - LoadFlip(filename, default_provider, image, w, h); + LoadFlip(filename, vfs_provider, image, w, h); // PNG must be square if (w!=h) { @@ -2792,7 +2792,7 @@ void mjCTexture::LoadCubeSeparate(int default_provider) { // compiler -void mjCTexture::Compile(int default_provider) { +void mjCTexture::Compile(int vfs_provider) { // builtin if (builtin!=mjBUILTIN_NONE) { // check size @@ -2835,9 +2835,9 @@ void mjCTexture::Compile(int default_provider) { // dispatch if (type==mjTEXTURE_2D) { - Load2D(filename, default_provider); + Load2D(filename, vfs_provider); } else { - LoadCubeSingle(filename, default_provider); + LoadCubeSingle(filename, vfs_provider); } } @@ -2865,7 +2865,7 @@ void mjCTexture::Compile(int default_provider) { } // only cube and skybox - LoadCubeSeparate(default_provider); + LoadCubeSeparate(vfs_provider); } // make sure someone allocated data; SHOULD NOT OCCUR diff --git a/src/user/user_objects.h b/src/user/user_objects.h index fa5a9635..8b4188b8 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -543,7 +543,7 @@ class mjCMesh: public mjCBase { private: mjCMesh(mjCModel* = 0, mjCDef* = 0); // constructor ~mjCMesh(); // destructor - void Compile(int default_provider); // compiler + void Compile(int vfs_provider); // compiler void LoadOBJ(mjResource* resource); // load mesh in wavefront OBJ format void LoadSTL(mjResource* resource); // load mesh in STL BIN format void LoadMSH(mjResource* resource); // load mesh in MSH BIN format @@ -625,7 +625,7 @@ class mjCSkin: public mjCBase { private: mjCSkin(mjCModel* = 0); // constructor ~mjCSkin(); // destructor - void Compile(int default_provider); // compiler + void Compile(int vfs_provider); // compiler void LoadSKN(mjResource* resource); // load skin in SKN BIN format int matid; // material id @@ -651,7 +651,7 @@ class mjCHField : public mjCBase { private: mjCHField(mjCModel* model); // constructor ~mjCHField(); // destructor - void Compile(int default_provider); // compiler + void Compile(int vfs_provider); // compiler void LoadCustom(mjResource* resource); // load from custom format void LoadPNG(mjResource* resource); // load from PNG format @@ -695,15 +695,15 @@ class mjCTexture : public mjCBase { private: mjCTexture(mjCModel*); // constructor ~mjCTexture(); // destructior - void Compile(int default_provider); // compiler + void Compile(int vfs_provider); // compiler void Builtin2D(void); // make builtin 2D void BuiltinCube(void); // make builtin cube - void Load2D(std::string filename, int default_provider); // load 2D from file - void LoadCubeSingle(std::string filename, int default_provider); // load cube from single file - void LoadCubeSeparate(int default_provider); // load cube from separate files + void Load2D(std::string filename, int vfs_provider); // load 2D from file + void LoadCubeSingle(std::string filename, int vfs_provider); // load cube from single file + void LoadCubeSeparate(int vfs_provider); // load cube from separate files - void LoadFlip(std::string filename, int default_provider, // load and flip + void LoadFlip(std::string filename, int vfs_provider, // load and flip std::vector& image, unsigned int& w, unsigned int& h); diff --git a/src/user/user_util.cc b/src/user/user_util.cc index a239094f..70e1c855 100644 --- a/src/user/user_util.cc +++ b/src/user/user_util.cc @@ -22,6 +22,7 @@ #include #include +#include #include "engine/engine_macro.h" #include "engine/engine_util_spatial.h" @@ -558,6 +559,12 @@ bool mjuu_isabspath(string path) { return false; } + // path is scheme:filename which we consider an absolute path + // e.g. file URI's are always absolute paths + if (mjp_getResourceProvider(path.c_str()) != nullptr) { + return true; + } + // check first char const char* str = path.c_str(); if (str[0]=='\\' || str[0]=='/') { @@ -573,25 +580,6 @@ bool mjuu_isabspath(string path) { } -// get directory path of file -string mjuu_getfiledir(string filename) { - // no filename - if (filename.empty()) { - return ""; - } - - // find last pathsymbol - size_t last = filename.find_last_of("/\\"); - - // no pathsymbol: unknown dir - if (last==string::npos) { - return ""; - } - - // extract path from filename - return filename.substr(0, last+1); -} - // assemble full filename string mjuu_makefullname(string filedir, string meshdir, string filename) { diff --git a/src/user/user_util.h b/src/user/user_util.h index 4575dbfb..2e14a015 100644 --- a/src/user/user_util.h +++ b/src/user/user_util.h @@ -149,9 +149,6 @@ std::string mjuu_getext(std::string_view filename); // check if path is absolute bool mjuu_isabspath(std::string path); -// get path from filename -std::string mjuu_getfiledir(std::string filename); - // assemble full filename std::string mjuu_makefullname(std::string filedir, std::string meshdir, std::string filename); diff --git a/src/xml/xml.cc b/src/xml/xml.cc index c68c3ea0..b15a3a8a 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -129,7 +129,7 @@ bool mjWriteXML(mjCModel* model, string filename, char* error, int error_sz) { // find include elements recursively, replace them with subtree from xml file static XMLElement* mjIncludeXML(XMLElement* elem, string dir, - int default_provider, vector& included) { + int vfs_provider, vector& included) { // include element: process if (!strcasecmp(elem->Value(), "include")) { // make sure include has no children @@ -152,9 +152,9 @@ static XMLElement* mjIncludeXML(XMLElement* elem, string dir, // get data source mjResource *resource = nullptr; const char* xmlstring = nullptr; - if ((resource = mju_openResource(filename.c_str(), default_provider)) == nullptr) { + if ((resource = mju_openResource(filename.c_str(), vfs_provider)) == nullptr) { // load from OS filesystem - if (!default_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) { + if (!vfs_provider || (resource = mju_openResource(filename.c_str(), 0)) == nullptr) { throw mjXError(elem, "Could not open file '%s'", filename.c_str()); } } @@ -215,14 +215,14 @@ static XMLElement* mjIncludeXML(XMLElement* elem, string dir, } // run XMLInclude on first new child - return mjIncludeXML(first->ToElement(), dir, default_provider, included); + return mjIncludeXML(first->ToElement(), dir, vfs_provider, included); } // otherwise check all child elements, return self else { XMLElement* child = elem->FirstChildElement(); while (child) { - child = mjIncludeXML(child, dir, default_provider, included); + child = mjIncludeXML(child, dir, vfs_provider, included); if (child) { child = child->NextSiblingElement(); } @@ -234,7 +234,7 @@ static XMLElement* mjIncludeXML(XMLElement* elem, string dir, // Main parser function -mjCModel* mjParseXML(const char* filename, int default_provider, char* error, int error_sz) { +mjCModel* mjParseXML(const char* filename, int vfs_provider, char* error, int error_sz) { LocaleOverride locale_override; // check arguments @@ -254,9 +254,9 @@ mjCModel* mjParseXML(const char* filename, int default_provider, char* error, in // get data source mjResource* resource = nullptr; const char* xmlstring = nullptr; - if ((resource = mju_openResource(filename, default_provider)) == nullptr) { + if ((resource = mju_openResource(filename, vfs_provider)) == nullptr) { // load from OS filesystem - if (!default_provider || (resource = mju_openResource(filename, 0)) == nullptr) { + if (!vfs_provider || (resource = mju_openResource(filename, 0)) == nullptr) { if (error) { snprintf(error, error_sz, "mjParseXML: could not open file '%s'", filename); } @@ -284,29 +284,37 @@ mjCModel* mjParseXML(const char* filename, int default_provider, char* error, in XMLDocument doc; doc.Parse(xmlstring, buffer_size); - // close resource - mju_closeResource(resource); - - // error checking if (doc.Error()) { if (error) { snprintf(error, error_sz, "XML parse error %d:\n%s\n", doc.ErrorID(), doc.ErrorStr()); } + mju_closeResource(resource); return nullptr; } // get top-level element XMLElement* root = doc.RootElement(); if (!root) { + mju_closeResource(resource); mjCopyError(error, "XML root element not found", error_sz); return nullptr; } // create model, set filedir model = new mjCModel; - model->modelfiledir = mjuu_getfiledir(filename); + const char* dir; + int ndir = 0; + mju_getResourceDir(resource, &dir, &ndir); + if (dir != nullptr) { + model->modelfiledir = std::string(dir, ndir); + } else { + model->modelfiledir = ""; + } + + // close resource + mju_closeResource(resource); // parse with exceptions try { @@ -314,7 +322,7 @@ mjCModel* mjParseXML(const char* filename, int default_provider, char* error, in // find include elements, replace them with subtree from xml file vector included; included.push_back(filename); - mjIncludeXML(root, model->modelfiledir, default_provider, included); + mjIncludeXML(root, model->modelfiledir, vfs_provider, included); // parse MuJoCo model mjXReader parser; diff --git a/src/xml/xml.h b/src/xml/xml.h index 345135be..8b39ed35 100644 --- a/src/xml/xml.h +++ b/src/xml/xml.h @@ -26,7 +26,7 @@ bool mjWriteXML(mjCModel* model, std::string filename, char* error, int error_sz); // Main parser function -mjCModel* mjParseXML(const char* filename, int default_provider, char* error, int error_sz); +mjCModel* mjParseXML(const char* filename, int vfs_provider, char* error, int error_sz); #endif // MUJOCO_SRC_XML_XML_H_ diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index ff0a0daf..9ac6532b 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -83,19 +83,19 @@ void mj_deactivate(void) { // mj_loadXML helper function -mjModel* _loadXML(const char* filename, int default_provider, +mjModel* _loadXML(const char* filename, int vfs_provider, char* error, int error_sz) { // serialize access to themodel std::lock_guard lock(themutex); // parse new model - mjCModel* newmodel = mjParseXML(filename, default_provider, error, error_sz); + mjCModel* newmodel = mjParseXML(filename, vfs_provider, error, error_sz); if (!newmodel) { return nullptr; } // compile new model - mjModel* m = newmodel->Compile(default_provider); + mjModel* m = newmodel->Compile(vfs_provider); if (!m) { mjCopyError(error, newmodel->GetError().message, error_sz); delete newmodel; diff --git a/test/engine/engine_resource_test.cc b/test/engine/engine_resource_test.cc index 2405a7a9..686c297d 100644 --- a/test/engine/engine_resource_test.cc +++ b/test/engine/engine_resource_test.cc @@ -24,6 +24,7 @@ #include #include "src/engine/engine_plugin.h" #include "src/engine/engine_resource.h" +#include "src/engine/engine_vfs.h" #include "test/fixture.h" namespace mujoco { @@ -41,7 +42,7 @@ int open_nop(mjResource* resource) { } int open_str(mjResource* resource) { - if (std::strcmp(resource->name, "str://file")) { + if (std::strcmp(resource->name, "str:file")) { return 0; } @@ -67,7 +68,7 @@ void close_str(mjResource* resource) { } TEST_F(ResourceTest, RegisterProviderSuccess) { - mjpResourceProvider provider = {"myprefix", open_nop, read_nop, close_nop, + mjpResourceProvider provider = {"my-prefix.123+45", open_nop, read_nop, close_nop, nullptr}; int count1 = mjp_resourceProviderCount(); @@ -97,7 +98,7 @@ TEST_F(ResourceTest, RegisterProviderMissingCallbacks) { } TEST_F(ResourceTest, RegisterProviderMissingPrefix) { - mjpResourceProvider provider = {"", open_nop, read_nop, close_nop, nullptr}; + mjpResourceProvider provider = {"", open_nop, read_nop, close_nop, nullptr, nullptr}; // install warning handler static char warning[1024]; @@ -113,12 +114,8 @@ TEST_F(ResourceTest, RegisterProviderMissingPrefix) { EXPECT_LT(i, 1); } -TEST_F(ResourceTest, RegisterProviderSubPrefix) { - mjpResourceProvider provider = {"prefix", open_nop, read_nop, close_nop, - nullptr}; - - mjpResourceProvider provider2 = {"pre", open_nop, read_nop, close_nop, - nullptr}; +TEST_F(ResourceTest, RegisterProviderInvalidPrefix1) { + mjpResourceProvider provider = {"1invalid", open_nop, read_nop, close_nop, nullptr, nullptr}; // install warning handler static char warning[1024]; @@ -128,21 +125,14 @@ TEST_F(ResourceTest, RegisterProviderSubPrefix) { }; int i = mjp_registerResourceProvider(&provider); - int j = mjp_registerResourceProvider(&provider2); - - // warning message related to an error - EXPECT_THAT(warning, HasSubstr("cannot be register")); - EXPECT_GT(i, 0); - EXPECT_LT(j, 1); + // warning message related to missing prefix + EXPECT_THAT(warning, HasSubstr("prefix")); + EXPECT_LT(i, 1); } -TEST_F(ResourceTest, RegisterProviderSuperPrefix) { - mjpResourceProvider provider = {"prefix", open_nop, read_nop, close_nop, - nullptr}; - - mjpResourceProvider provider2 = {"prefix2", open_nop, read_nop, close_nop, - nullptr}; +TEST_F(ResourceTest, RegisterProviderInvalidPrefix2) { + mjpResourceProvider provider = {"invalid:", open_nop, read_nop, close_nop, nullptr, nullptr}; // install warning handler static char warning[1024]; @@ -152,21 +142,18 @@ TEST_F(ResourceTest, RegisterProviderSuperPrefix) { }; int i = mjp_registerResourceProvider(&provider); - int j = mjp_registerResourceProvider(&provider2); - - // warning message related to an error - EXPECT_THAT(warning, HasSubstr("cannot be register")); - EXPECT_GT(i, 0); - EXPECT_LT(j, 1); + // warning message related to missing prefix + EXPECT_THAT(warning, HasSubstr("prefix")); + EXPECT_LT(i, 1); } TEST_F(ResourceTest, RegisterProviderSame) { mjpResourceProvider provider = {"prefix", open_nop, read_nop, close_nop, - nullptr}; + nullptr, nullptr}; mjpResourceProvider provider2 = {"prefix", open_nop, read_nop, close_nop, - nullptr}; + nullptr, nullptr}; int i1 = mjp_registerResourceProvider(&provider); int count1 = mjp_resourceProviderCount(); @@ -176,19 +163,35 @@ TEST_F(ResourceTest, RegisterProviderSame) { EXPECT_EQ(i1, i2); EXPECT_EQ(count1, count2); +} +TEST_F(ResourceTest, RegisterProviderSameCase) { + mjpResourceProvider provider = {"prefix", open_nop, read_nop, close_nop, + nullptr, nullptr}; + + mjpResourceProvider provider2 = {"PREFIX", open_nop, read_nop, close_nop, + nullptr, nullptr}; + + int i1 = mjp_registerResourceProvider(&provider); + int count1 = mjp_resourceProviderCount(); + + int i2 = mjp_registerResourceProvider(&provider2); + int count2 = mjp_resourceProviderCount(); + + EXPECT_EQ(i1, i2); + EXPECT_EQ(count1, count2); } TEST_F(ResourceTest, GeneralTest) { - mjpResourceProvider provider = {"str://", open_str, read_str, close_str, - nullptr}; + mjpResourceProvider provider = {"str", open_str, read_str, close_str, + nullptr, nullptr}; // register resource provider int i = mjp_registerResourceProvider(&provider); EXPECT_GT(i, 0); // open resource - mjResource* resource = mju_openResource("str://file", 0); + mjResource* resource = mju_openResource("str:file", 0); ASSERT_THAT(resource, NotNull()); const char* buffer = NULL; @@ -200,8 +203,8 @@ TEST_F(ResourceTest, GeneralTest) { } TEST_F(ResourceTest, GeneralTestFailure) { - mjpResourceProvider provider = {"str://", open_str, read_str, close_str, - nullptr}; + mjpResourceProvider provider = {"str", open_str, read_str, close_str, + nullptr, nullptr}; // register resource provider int i = mjp_registerResourceProvider(&provider); @@ -216,11 +219,100 @@ TEST_F(ResourceTest, GeneralTestFailure) { }; // open resource - mjResource* resource = mju_openResource("str://notfound", 0); - ASSERT_THAT(resource, IsNull()); + mjResource* resource = mju_openResource("str:notfound", 0); + ASSERT_THAT(resource, IsNull()); EXPECT_THAT(warning, HasSubstr("could not open")); } +TEST_F(ResourceTest, NameWithValidPrefix) { + mjpResourceProvider provider = {"nop", open_nop, read_nop, close_nop, + nullptr, nullptr}; + + // register resource provider + int i = mjp_registerResourceProvider(&provider); + EXPECT_GT(i, 0); + + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + // open resource + mjResource* resource = mju_openResource("nop:found", 0); + ASSERT_THAT(resource, NotNull()); + mju_closeResource(resource); +} + +TEST_F(ResourceTest, NameWithUpperCasePrefix) { + mjpResourceProvider provider = {"nop", open_nop, read_nop, close_nop, + nullptr, nullptr}; + + // register resource provider + int i = mjp_registerResourceProvider(&provider); + EXPECT_GT(i, 0); + + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + // open resource + mjResource* resource = mju_openResource("NOP:found", 0); + ASSERT_THAT(resource, NotNull()); + mju_closeResource(resource); +} + +TEST_F(ResourceTest, NameWithInvalidPrefix) { + mjpResourceProvider provider = {"nop", open_nop, read_nop, close_nop, + nullptr, nullptr}; + + // register resource provider + int i = mjp_registerResourceProvider(&provider); + EXPECT_GT(i, 0); + + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + // open resource + mjResource* resource = mju_openResource("nopfound", 0); + ASSERT_THAT(resource, IsNull()); +} + +TEST_F(ResourceTest, VFSProvider) { + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + mj_makeEmptyFileVFS(vfs.get(), "file", 1); + + // register resource provider + int i = mj_registerVfsProvider(vfs.get()); + EXPECT_GT(i, 0); + + + // install warning handler + static char warning[1024]; + warning[0] = '\0'; + mju_user_warning = [](const char* msg) { + util::strcpy_arr(warning, msg); + }; + + // open resource + mjResource* resource = mju_openResource("vfs:file", 0); + ASSERT_THAT(resource, IsNull()); + mj_deleteFileVFS(vfs.get(), "file"); +} + } // namespace } // namespace mujoco diff --git a/test/fixture.cc b/test/fixture.cc index 2715f197..10432236 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -68,48 +68,26 @@ const std::string GetModelPath(std::string_view path) { return absl::StrCat("../model/", path); } -int datastr_length(mjResource* resource) { - int len = strlen(resource->name)-1; - - // string should have the format "S(data).ext" - for (; len > 2; len--) { - if (resource->name[len] == ')') { - return len - 2; - } - } - return -1; -} - -int str_open_callback(mjResource* resource) { - if (datastr_length(resource) < 0) { - return 0; - } - resource->data = resource->name+2; - return 1; -} - -int str_read_callback(mjResource* resource, const void** buffer) { - *buffer = resource->data; - return datastr_length(resource); -} - -void str_close_callback(mjResource* resource) { -} - mjModel* LoadModelFromString(std::string_view xml, char* error, int error_size, mjVFS* vfs) { - // register string resource provider if not registered before - if (mjp_getResourceProvider("S(") == nullptr) { + if (mjp_getResourceProvider("LoadModelFromString:") == nullptr) { mjpResourceProvider resourceProvider; - resourceProvider.prefix = "S("; - resourceProvider.open = str_open_callback; - resourceProvider.read = str_read_callback; - resourceProvider.close = str_close_callback; + 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); } std::string xml2 = {xml.begin(), xml.end()}; - std::string str = "S(" + xml2 + ")"; + std::string str = "LoadModelFromString:" + xml2; return mj_loadXML(str.c_str(), vfs, error, error_size); }