Refactor mjVFS and resource management.
Resource operations (e.g. mju_openResource, mju_readResource, and mju_closeResource, etc.) are now all handled by a VFS instance. It is now up to the VFS to determine which provider to use in order to handle those operations. This allows us to dynamically add/remove (aka "mount") providers to a VFS to handle special requests. mj_addFileVFS and mj_addBufferVFS have been reimplemented as two such use-cases. Moreover, we expose the mounting behaviour with two new functions: mj_mountVFS and mj_unmountVFS. PiperOrigin-RevId: 861550939 Change-Id: I070eb4bcc2982466c8f368f7918005538baa5185
This commit is contained in:
committed by
Copybara-Service
parent
e977b0d660
commit
0ddbb46fa1
@@ -1357,6 +1357,25 @@ The VFS must first be allocated using :ref:`mj_defaultVFS` and must be freed wit
|
|||||||
|
|
||||||
Initialize an empty VFS, :ref:`mj_deleteVFS` must be called to deallocate the VFS.
|
Initialize an empty VFS, :ref:`mj_deleteVFS` must be called to deallocate the VFS.
|
||||||
|
|
||||||
|
.. _mj_mountVFS:
|
||||||
|
|
||||||
|
`mj_mountVFS <#mj_mountVFS>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mj_mountVFS
|
||||||
|
|
||||||
|
Mount a ResourceProvider to handle file operations under the given path; return 0: success,
|
||||||
|
2: repeated name, -1: invalid resource provider.
|
||||||
|
|
||||||
|
.. _mj_unmountVFS:
|
||||||
|
|
||||||
|
`mj_unmountVFS <#mj_unmountVFS>`__
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. mujoco-include:: mj_unmountVFS
|
||||||
|
|
||||||
|
Unmount a previously mounted ResourceProvider; return 0: success, -1: not found in VFS.
|
||||||
|
|
||||||
.. _mj_addFileVFS:
|
.. _mj_addFileVFS:
|
||||||
|
|
||||||
`mj_addFileVFS <#mj_addFileVFS>`__
|
`mj_addFileVFS <#mj_addFileVFS>`__
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ General
|
|||||||
See :ref:`rangefinder<sensor-rangefinder>` for details.
|
See :ref:`rangefinder<sensor-rangefinder>` for details.
|
||||||
- Cameras now have an :ref:`output<body-camera-output>` attribute, parsed into the ``mjModel.cam_output`` bitfield.
|
- Cameras now have an :ref:`output<body-camera-output>` attribute, parsed into the ``mjModel.cam_output`` bitfield.
|
||||||
Unused by the renderer, it serves as a convenient location to store a camera's supported output types.
|
Unused by the renderer, it serves as a convenient location to store a camera's supported output types.
|
||||||
|
- Added :ref:`mj_mountVFS` and :ref:`mj_unmountVFS` functions for mounting a custom VFS provider. Mounting Allows
|
||||||
|
providers to be used to open/read/close resources dynamically at arbitrary paths.
|
||||||
- Non-breaking ABI changes:
|
- Non-breaking ABI changes:
|
||||||
|
|
||||||
- The type of the ``sig`` (signature) argument of :ref:`mj_stateSize` and related functions has been changed from
|
- The type of the ``sig`` (signature) argument of :ref:`mj_stateSize` and related functions has been changed from
|
||||||
|
|||||||
@@ -1629,6 +1629,7 @@ typedef struct mjModel_ mjModel;
|
|||||||
struct mjResource_ {
|
struct mjResource_ {
|
||||||
char* name; // name of resource (filename, etc)
|
char* name; // name of resource (filename, etc)
|
||||||
void* data; // opaque data pointer
|
void* data; // opaque data pointer
|
||||||
|
mjVFS* vfs; // pointer to the VFS
|
||||||
char timestamp[512]; // timestamp of the resource
|
char timestamp[512]; // timestamp of the resource
|
||||||
const struct mjpResourceProvider* provider; // pointer to the provider
|
const struct mjpResourceProvider* provider; // pointer to the provider
|
||||||
};
|
};
|
||||||
@@ -1638,6 +1639,8 @@ struct mjpResourceProvider {
|
|||||||
mjfOpenResource open; // opening callback
|
mjfOpenResource open; // opening callback
|
||||||
mjfReadResource read; // reading callback
|
mjfReadResource read; // reading callback
|
||||||
mjfCloseResource close; // closing callback
|
mjfCloseResource close; // closing callback
|
||||||
|
mjfMountResource mount; // mounting callback (optional)
|
||||||
|
mjfUnmountResource unmount; // unmounting callback (optional)
|
||||||
mjfResourceModified modified; // resource modified callback (optional)
|
mjfResourceModified modified; // resource modified callback (optional)
|
||||||
void* data; // opaque data pointer (resource invariant)
|
void* data; // opaque data pointer (resource invariant)
|
||||||
};
|
};
|
||||||
@@ -3095,6 +3098,8 @@ typedef struct mjvFigure_ mjvFigure;
|
|||||||
|
|
||||||
//----------------------------- MJAPI FUNCTIONS --------------------------------
|
//----------------------------- MJAPI FUNCTIONS --------------------------------
|
||||||
void mj_defaultVFS(mjVFS* vfs);
|
void mj_defaultVFS(mjVFS* vfs);
|
||||||
|
int mj_mountVFS(mjVFS* vfs, const char* filepath, const mjpResourceProvider* provider);
|
||||||
|
int mj_unmountVFS(mjVFS* vfs, const char* filename);
|
||||||
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
|
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
|
||||||
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, int nbuffer);
|
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer, int nbuffer);
|
||||||
int mj_deleteFileVFS(mjVFS* vfs, const char* filename);
|
int mj_deleteFileVFS(mjVFS* vfs, const char* filename);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
struct mjResource_ {
|
struct mjResource_ {
|
||||||
char* name; // name of resource (filename, etc)
|
char* name; // name of resource (filename, etc)
|
||||||
void* data; // opaque data pointer
|
void* data; // opaque data pointer
|
||||||
|
mjVFS* vfs; // pointer to the VFS
|
||||||
char timestamp[512]; // timestamp of the resource
|
char timestamp[512]; // timestamp of the resource
|
||||||
const struct mjpResourceProvider* provider; // pointer to the provider
|
const struct mjpResourceProvider* provider; // pointer to the provider
|
||||||
};
|
};
|
||||||
@@ -42,6 +43,12 @@ typedef int (*mjfReadResource)(mjResource* resource, const void** buffer);
|
|||||||
// callback for closing a resource (responsible for freeing any allocated memory)
|
// callback for closing a resource (responsible for freeing any allocated memory)
|
||||||
typedef void (*mjfCloseResource)(mjResource* resource);
|
typedef void (*mjfCloseResource)(mjResource* resource);
|
||||||
|
|
||||||
|
// callback for mounting a resource (provider), returns zero on failure
|
||||||
|
typedef int (*mjfMountResource)(mjResource* resource);
|
||||||
|
|
||||||
|
// callback for unmounting a resource (provider), returns zero on failure
|
||||||
|
typedef int (*mjfUnmountResource)(mjResource* resource);
|
||||||
|
|
||||||
// callback for checking if the current resource was modified from the time
|
// callback for checking if the current resource was modified from the time
|
||||||
// specified by the timestamp
|
// specified by the timestamp
|
||||||
// returns 0 if the resource's timestamp matches the provided timestamp
|
// returns 0 if the resource's timestamp matches the provided timestamp
|
||||||
@@ -55,6 +62,8 @@ struct mjpResourceProvider {
|
|||||||
mjfOpenResource open; // opening callback
|
mjfOpenResource open; // opening callback
|
||||||
mjfReadResource read; // reading callback
|
mjfReadResource read; // reading callback
|
||||||
mjfCloseResource close; // closing callback
|
mjfCloseResource close; // closing callback
|
||||||
|
mjfMountResource mount; // mounting callback (optional)
|
||||||
|
mjfUnmountResource unmount; // unmounting callback (optional)
|
||||||
mjfResourceModified modified; // resource modified callback (optional)
|
mjfResourceModified modified; // resource modified callback (optional)
|
||||||
void* data; // opaque data pointer (resource invariant)
|
void* data; // opaque data pointer (resource invariant)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ MJAPI extern const char* mjRNDSTRING[mjNRNDFLAG][3];
|
|||||||
// Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS.
|
// Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS.
|
||||||
MJAPI void mj_defaultVFS(mjVFS* vfs);
|
MJAPI void mj_defaultVFS(mjVFS* vfs);
|
||||||
|
|
||||||
|
// Mount a ResourceProvider to handle file operations under the given path; return 0: success,
|
||||||
|
// 2: repeated name, -1: invalid resource provider.
|
||||||
|
MJAPI int mj_mountVFS(mjVFS* vfs, const char* filepath, const mjpResourceProvider* provider);
|
||||||
|
|
||||||
|
// Unmount a previously mounted ResourceProvider; return 0: success, -1: not found in VFS.
|
||||||
|
MJAPI int mj_unmountVFS(mjVFS* vfs, const char* filename);
|
||||||
|
|
||||||
// Add file to VFS; return 0: success, 2: repeated name, -1: failed to load.
|
// Add file to VFS; return 0: success, 2: repeated name, -1: failed to load.
|
||||||
MJAPI int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
|
MJAPI int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,52 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
|||||||
),
|
),
|
||||||
doc='Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS.', # pylint: disable=line-too-long
|
doc='Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS.', # pylint: disable=line-too-long
|
||||||
)),
|
)),
|
||||||
|
('mj_mountVFS',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mj_mountVFS',
|
||||||
|
return_type=ValueType(name='int'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='vfs',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjVFS'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='filepath',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='char', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='provider',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjpResourceProvider', is_const=True), # pylint: disable=line-too-long
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Mount a ResourceProvider to handle file operations under the given path; return 0: success, 2: repeated name, -1: invalid resource provider.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
|
('mj_unmountVFS',
|
||||||
|
FunctionDecl(
|
||||||
|
name='mj_unmountVFS',
|
||||||
|
return_type=ValueType(name='int'),
|
||||||
|
parameters=(
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='vfs',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='mjVFS'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FunctionParameterDecl(
|
||||||
|
name='filename',
|
||||||
|
type=PointerType(
|
||||||
|
inner_type=ValueType(name='char', is_const=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
doc='Unmount a previously mounted ResourceProvider; return 0: success, -1: not found in VFS.', # pylint: disable=line-too-long
|
||||||
|
)),
|
||||||
('mj_addFileVFS',
|
('mj_addFileVFS',
|
||||||
FunctionDecl(
|
FunctionDecl(
|
||||||
name='mj_addFileVFS',
|
name='mj_addFileVFS',
|
||||||
|
|||||||
+36
-192
@@ -14,216 +14,71 @@
|
|||||||
|
|
||||||
#include "user/user_resource.h"
|
#include "user/user_resource.h"
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
|
||||||
#include <mujoco/mujoco.h>
|
#include <mujoco/mujoco.h>
|
||||||
|
|
||||||
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
#define stat _stat
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <mujoco/mjplugin.h>
|
#include <mujoco/mjplugin.h>
|
||||||
#include "engine/engine_plugin.h"
|
#include "engine/engine_plugin.h"
|
||||||
#include "engine/engine_util_misc.h"
|
|
||||||
#include "user/user_util.h"
|
#include "user/user_util.h"
|
||||||
#include "user/user_vfs.h"
|
#include "user/user_vfs.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using mujoco::user::FileToMemory;
|
|
||||||
|
|
||||||
// file buffer used internally for the OS filesystem
|
|
||||||
struct FileSpec {
|
|
||||||
bool is_read; // set to nonzero if buffer was read into
|
|
||||||
std::vector<uint8_t> buffer; // raw bytes from file
|
|
||||||
time_t mtime; // last modified time
|
|
||||||
};
|
|
||||||
|
|
||||||
// callback for opening a resource, returns zero on failure
|
|
||||||
int FileOpen(mjResource* resource, char* error, size_t nerror) {
|
|
||||||
resource->provider = nullptr;
|
|
||||||
resource->data = new FileSpec;
|
|
||||||
const char* name = resource->name;
|
|
||||||
FileSpec* spec = (FileSpec*) resource->data;
|
|
||||||
spec->is_read = false;
|
|
||||||
struct stat file_stat;
|
|
||||||
if (stat(name, &file_stat) == 0) {
|
|
||||||
memcpy(&spec->mtime, &file_stat.st_mtime, sizeof(time_t));
|
|
||||||
} else {
|
|
||||||
if (error) {
|
|
||||||
snprintf(error, nerror, "Error opening file '%s': %s", name,
|
|
||||||
strerror(errno));
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
mju_encodeBase64(resource->timestamp, (uint8_t*) &spec->mtime,
|
|
||||||
sizeof(time_t));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS filesystem read callback
|
|
||||||
int FileRead(mjResource* resource, const void** buffer) {
|
|
||||||
FileSpec* spec = (FileSpec*) resource->data;
|
|
||||||
|
|
||||||
// only read once from file
|
|
||||||
if (!spec->is_read) {
|
|
||||||
spec->buffer = FileToMemory(resource->name);
|
|
||||||
spec->is_read = true;
|
|
||||||
}
|
|
||||||
*buffer = spec->buffer.data();
|
|
||||||
return spec->buffer.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS filesystem close callback
|
|
||||||
void FileClose(mjResource* resource) {
|
|
||||||
FileSpec* spec = (FileSpec*) resource->data;
|
|
||||||
if (spec) delete spec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OS filesystem modified callback
|
|
||||||
int FileModified(const mjResource* resource, const char*timestamp) {
|
|
||||||
if (mju_isValidBase64(timestamp) != sizeof(time_t)) {
|
|
||||||
return 1; // error (assume modified)
|
|
||||||
}
|
|
||||||
|
|
||||||
time_t time1, time2;
|
|
||||||
mju_decodeBase64((uint8_t*) &time1, timestamp);
|
|
||||||
time2 = ((FileSpec*) resource->data)->mtime;
|
|
||||||
double diff = difftime(time2, time1);
|
|
||||||
if (diff < 0) return -1;
|
|
||||||
if (diff > 0) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// open the given resource; if the name doesn't have a prefix matching with a
|
|
||||||
// resource provider, then the OS filesystem is used
|
|
||||||
mjResource* mju_openResource(const char* dir, const char* name,
|
mjResource* mju_openResource(const char* dir, const char* name,
|
||||||
const mjVFS* vfs, char* error, size_t nerror) {
|
const mjVFS* vfs, char* error, size_t nerror) {
|
||||||
// no error so far
|
// TODO: Update API to use non-const pointer. Unfortunately, while this is
|
||||||
|
// ABI stable, it will cause compiler errors in user code that is const
|
||||||
|
// correct.
|
||||||
|
mjVFS* non_const_vfs = const_cast<mjVFS*>(vfs);
|
||||||
|
|
||||||
|
// TODO: Eventually, we should make `vfs` a required argument. In the
|
||||||
|
// meantime, when passing in a nullptr VFS, we will create a VFS dynamically
|
||||||
|
// that self-destructs when the resource is closed (or if the resource could
|
||||||
|
// not be opened).
|
||||||
|
if (non_const_vfs == nullptr) {
|
||||||
|
mjVFS* local_vfs = (mjVFS*)mju_malloc(sizeof(mjVFS));
|
||||||
|
mj_defaultVFS(local_vfs);
|
||||||
|
mujoco::user::VFS::Upcast(local_vfs)->SetToSelfDestruct([](mjVFS* ptr) {
|
||||||
|
mj_deleteVFS(ptr);
|
||||||
|
mju_free(ptr);
|
||||||
|
});
|
||||||
|
|
||||||
|
non_const_vfs = local_vfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
mjResource* resource =
|
||||||
|
mujoco::user::VFS::Upcast(non_const_vfs)->Open(dir ? dir : "", name);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
error[0] = '\0';
|
if (resource) {
|
||||||
}
|
error[0] = '\0';
|
||||||
|
} else {
|
||||||
mjResource* resource = (mjResource*) mju_malloc(sizeof(mjResource));
|
std::snprintf(error, nerror, "Error opening file '%s'", name);
|
||||||
if (resource == nullptr) {
|
|
||||||
if (error) {
|
|
||||||
strncpy(error, "could not allocate memory", nerror);
|
|
||||||
error[nerror - 1] = '\0';
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear out resource
|
|
||||||
memset(resource, 0, sizeof(mjResource));
|
|
||||||
|
|
||||||
// make space for filename
|
|
||||||
std::string fullname = mjuu_combinePaths(dir, name);
|
|
||||||
std::size_t n = fullname.size();
|
|
||||||
resource->name = (char*) mju_malloc(sizeof(char) * (n + 1));
|
|
||||||
if (resource->name == nullptr) {
|
|
||||||
if (error) {
|
|
||||||
strncpy(error, "could not allocate memory", nerror);
|
|
||||||
error[nerror - 1] = '\0';
|
|
||||||
}
|
|
||||||
mju_closeResource(resource);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// first priority is to check the VFS
|
|
||||||
if (vfs != nullptr) {
|
|
||||||
memcpy(resource->name, name,
|
|
||||||
sizeof(char) * (std::strlen(name) + 1));
|
|
||||||
const mjpResourceProvider* provider = GetVfsResourceProvider();
|
|
||||||
resource->data = (void*) vfs;
|
|
||||||
resource->provider = provider;
|
|
||||||
if (provider->open(resource)) {
|
|
||||||
return resource;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy full path over
|
return resource;
|
||||||
memcpy(resource->name, fullname.c_str(), sizeof(char) * (n + 1));
|
|
||||||
|
|
||||||
// find provider based off prefix of name
|
|
||||||
const mjpResourceProvider* provider = mjp_getResourceProvider(resource->name);
|
|
||||||
if (provider != nullptr) {
|
|
||||||
resource->provider = provider;
|
|
||||||
resource->data = nullptr;
|
|
||||||
if (provider->open(resource)) {
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
snprintf(error, nerror, "could not open '%s'"
|
|
||||||
"using a resource provider matching prefix '%s'",
|
|
||||||
resource->name, provider->prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
mju_closeResource(resource);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lastly fallback to OS filesystem
|
|
||||||
if (FileOpen(resource, error, nerror)) {
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
mju_closeResource(resource);
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// close the given resource; no-op if resource is NULL
|
|
||||||
void mju_closeResource(mjResource* resource) {
|
void mju_closeResource(mjResource* resource) {
|
||||||
if (resource == nullptr) {
|
if (resource && resource->vfs) {
|
||||||
return;
|
mujoco::user::VFS::Upcast(resource->vfs)->Close(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the resource provider close callback
|
|
||||||
const mjpResourceProvider* provider = resource->provider;
|
|
||||||
if (provider) {
|
|
||||||
if (provider->close) provider->close(resource);
|
|
||||||
} else {
|
|
||||||
FileClose(resource); // clear OS filesystem if present
|
|
||||||
}
|
|
||||||
|
|
||||||
// free resource
|
|
||||||
if (resource->name) mju_free(resource->name);
|
|
||||||
mju_free(resource);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// set buffer to bytes read from the resource and return number of bytes in
|
|
||||||
// buffer; return negative value if error
|
|
||||||
int mju_readResource(mjResource* resource, const void** buffer) {
|
int mju_readResource(mjResource* resource, const void** buffer) {
|
||||||
if (resource->provider) {
|
if (resource && resource->vfs) {
|
||||||
return resource->provider->read(resource, buffer);
|
return mujoco::user::VFS::Upcast(resource->vfs)->Read(resource, buffer);
|
||||||
}
|
}
|
||||||
|
return -1; // default (error reading bytes)
|
||||||
// if provider is NULL, then OS filesystem is used
|
|
||||||
return FileRead(resource, buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// get directory path of resource
|
|
||||||
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) {
|
void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) {
|
||||||
*dir = nullptr;
|
*dir = nullptr;
|
||||||
*ndir = 0;
|
*ndir = 0;
|
||||||
@@ -247,22 +102,11 @@ void mju_getResourceDir(mjResource* resource, const char** dir, int* ndir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// return 0 if the resource's timestamp matches the provided timestamp
|
|
||||||
// return > 0 if the resource is younger than the given timestamp
|
|
||||||
// return < 0 if the resource is older than the given timestamp
|
|
||||||
int mju_isModifiedResource(const mjResource* resource, const char* timestamp) {
|
int mju_isModifiedResource(const mjResource* resource, const char* timestamp) {
|
||||||
// provider is not OS filesystem
|
if (resource && resource->provider && resource->provider->modified) {
|
||||||
if (resource->provider) {
|
return resource->provider->modified(resource, timestamp);
|
||||||
if (resource->provider->modified) {
|
|
||||||
return resource->provider->modified(resource, timestamp);
|
|
||||||
}
|
|
||||||
return 1; // default (modified)
|
|
||||||
}
|
}
|
||||||
|
return 1; // default (assume modified)
|
||||||
// fallback to OS filesystem
|
|
||||||
return FileModified(resource, timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mjSpec* mju_decodeResource(mjResource* resource, const char* content_type, const mjVFS* vfs) {
|
mjSpec* mju_decodeResource(mjResource* resource, const char* content_type, const mjVFS* vfs) {
|
||||||
|
|||||||
+442
-217
@@ -14,265 +14,490 @@
|
|||||||
|
|
||||||
#include "user/user_vfs.h"
|
#include "user/user_vfs.h"
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define stat _stat
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstring>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstring>
|
||||||
#include <cstdlib>
|
#include <ctime>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <mujoco/mujoco.h>
|
||||||
#include "engine/engine_util_misc.h"
|
#include "engine/engine_util_misc.h"
|
||||||
#include "user/user_util.h"
|
#include "user/user_util.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using mujoco::user::FilePath;
|
// struct for holding the contents of a file
|
||||||
using mujoco::user::FileToMemory;
|
struct ResourceFileData {
|
||||||
|
std::vector<uint8_t> contents;
|
||||||
// internal struct for VFS files
|
time_t modified_time = 0;
|
||||||
struct VFSFile {
|
bool is_read = false;
|
||||||
FilePath filename;
|
|
||||||
std::vector<uint8_t> filedata;
|
|
||||||
std::size_t filesize;
|
|
||||||
uint64_t filestamp;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// internal container class for VFS
|
time_t GetModifiedTime(const char* path) {
|
||||||
class VFS {
|
struct stat file_stat;
|
||||||
public:
|
if (stat(path, &file_stat) == 0) {
|
||||||
// returns true if the file exists in the VFS
|
return file_stat.st_mtime;
|
||||||
bool HasFile(const FilePath& filename) const;
|
|
||||||
|
|
||||||
// returns inserted mjuuVFSFile if the file was added successfully. This class
|
|
||||||
// assumes ownership of the buffer.
|
|
||||||
VFSFile* AddFile(const FilePath& filename, std::vector<uint8_t>&& buffer,
|
|
||||||
uint64_t filestamp);
|
|
||||||
|
|
||||||
// returns the internal file struct for the given filename
|
|
||||||
const VFSFile* GetFile(const FilePath& filename) const;
|
|
||||||
|
|
||||||
// deletes file from VFS, return 0: success, -1: not found
|
|
||||||
int DeleteFile(const FilePath& filename);
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unordered_map<std::string, VFSFile> files_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns the internal VFS class pointer from the VFS C struct
|
|
||||||
inline VFS* GetVFSImpl(const mjVFS* vfs) {
|
|
||||||
return vfs->impl_ ? static_cast<VFS*>(vfs->impl_) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip path prefix from filename and make lowercase
|
|
||||||
FilePath StripPath(const char* filename) {
|
|
||||||
return FilePath(filename).StripPath().Lower();
|
|
||||||
}
|
|
||||||
|
|
||||||
// copies data into a buffer and produces a hash of the data
|
|
||||||
uint64_t vfs_memcpy(std::vector<uint8_t>& dest, const void* src, size_t n) {
|
|
||||||
uint64_t hash = 0xcbf29ce484222325; // magic number
|
|
||||||
uint64_t prime = 0x100000001b3; // magic prime
|
|
||||||
const uint8_t* bytes = (uint8_t*) src;
|
|
||||||
for (size_t i = 0; i < n; i++) {
|
|
||||||
dest.push_back(bytes[i]);
|
|
||||||
|
|
||||||
// do FNV-1 hash
|
|
||||||
hash |= bytes[i];
|
|
||||||
hash *= prime;
|
|
||||||
}
|
}
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VFS hash function implemented using the FNV-1 hash
|
|
||||||
uint64_t vfs_hash(const std::vector<uint8_t>& buffer) {
|
|
||||||
uint64_t hash = 0xcbf29ce484222325; // magic number
|
|
||||||
uint64_t prime = 0x100000001b3; // magic prime
|
|
||||||
const uint8_t* bytes = (uint8_t*) buffer.data();
|
|
||||||
std::size_t n = buffer.size();
|
|
||||||
for (std::size_t i = 0; i < n; i++) {
|
|
||||||
hash |= bytes[i];
|
|
||||||
hash *= prime;
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool VFS::HasFile(const FilePath& filename) const {
|
|
||||||
return files_.find(filename.Str()) != files_.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
VFSFile* VFS::AddFile(const FilePath& filename, std::vector<uint8_t>&& buffer,
|
|
||||||
uint64_t filestamp) {
|
|
||||||
auto [it, inserted] = files_.insert({filename.Str(), VFSFile()});
|
|
||||||
if (!inserted) {
|
|
||||||
return nullptr; // repeated name
|
|
||||||
}
|
|
||||||
it->second.filename = filename;
|
|
||||||
it->second.filedata = buffer;
|
|
||||||
it->second.filestamp = filestamp;
|
|
||||||
return &(it->second);
|
|
||||||
}
|
|
||||||
|
|
||||||
const VFSFile* VFS::GetFile(const FilePath& filename) const {
|
|
||||||
auto it = files_.find(filename.Str());
|
|
||||||
if (it == files_.end()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return &it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
int VFS::DeleteFile(const FilePath& filename) {
|
|
||||||
auto it = files_.find(filename.Str());
|
|
||||||
if (it == files_.end()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
files_.erase(it);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// open callback for the VFS resource provider
|
int OpenFile(const char* filename, mjResource* resource) {
|
||||||
int Open(mjResource* resource) {
|
const time_t mtime = GetModifiedTime(filename);
|
||||||
if (!resource || !resource->name || !resource->data) {
|
if (mtime != 0) {
|
||||||
return 0;
|
ResourceFileData* data = new ResourceFileData();
|
||||||
}
|
resource->data = data;
|
||||||
|
|
||||||
const mjVFS* vfs = (const mjVFS*) resource->data;
|
data->modified_time = mtime;
|
||||||
const VFS* cvfs = GetVFSImpl(vfs);
|
mju_encodeBase64(resource->timestamp, (uint8_t*)&mtime, sizeof(time_t));
|
||||||
const VFSFile* file = cvfs->GetFile(StripPath(resource->name));
|
return 1;
|
||||||
if (file == nullptr) {
|
|
||||||
file = cvfs->GetFile(FilePath(resource->name));
|
|
||||||
if (file == nullptr) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
resource->data = (void*) file;
|
|
||||||
resource->timestamp[0] = '\0';
|
|
||||||
if (file->filestamp) {
|
|
||||||
mju_encodeBase64(resource->timestamp, (uint8_t*) &file->filestamp,
|
|
||||||
sizeof(uint64_t));
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// read callback for the VFS resource provider
|
int ReadFile(const char* filename, mjResource* resource, const void** buffer) {
|
||||||
int Read(mjResource* resource, const void** buffer) {
|
ResourceFileData* data = (ResourceFileData*)resource->data;
|
||||||
if (!resource || !resource->name || !resource->data) {
|
if (!data->is_read) {
|
||||||
*buffer = nullptr;
|
data->contents = mujoco::user::FileToMemory(filename);
|
||||||
return -1;
|
data->is_read = true;
|
||||||
}
|
}
|
||||||
|
*buffer = data->contents.data();
|
||||||
const VFSFile* file = static_cast<const VFSFile*>(resource->data);
|
return static_cast<int>(data->contents.size());
|
||||||
if (file == nullptr) {
|
|
||||||
*buffer = nullptr;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*buffer = file->filedata.data();
|
|
||||||
return file->filedata.size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// close callback for the VFS resource provider
|
void CloseFile(mjResource* resource) {
|
||||||
void Close(mjResource* resource) {
|
delete (ResourceFileData*)resource->data;
|
||||||
|
resource->data = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// modified callback for the VFS resource provider
|
int FileModified(const mjResource* resource, const char* timestamp) {
|
||||||
// return > 0 if modified and 0 if unmodified
|
if (mju_isValidBase64(timestamp) != sizeof(time_t)) {
|
||||||
int Modified(const mjResource* resource, const char* timestamp) {
|
return 1;
|
||||||
uint64_t filestamp;
|
|
||||||
if (mju_isValidBase64(timestamp) > sizeof(uint64_t)) {
|
|
||||||
return 2; // error (assume modified)
|
|
||||||
}
|
}
|
||||||
|
time_t time;
|
||||||
|
mju_decodeBase64((uint8_t*)&time, timestamp);
|
||||||
|
|
||||||
mju_decodeBase64((uint8_t*) &filestamp, timestamp);
|
ResourceFileData* data = (ResourceFileData*)resource->data;
|
||||||
if (!filestamp) return 3; // no hash (assume modified)
|
const double diff = difftime(data->modified_time, time);
|
||||||
|
|
||||||
if (resource) {
|
if (diff < 0) return -1;
|
||||||
const VFSFile* file = static_cast<const VFSFile*>(resource->data);
|
if (diff > 0) return 1;
|
||||||
if (file == nullptr) return 4; // missing file (assume modified)
|
return 0;
|
||||||
if (!file->filestamp) return 5; // missing filestamp (assume modified)
|
}
|
||||||
|
|
||||||
if (file->filestamp == filestamp) {
|
std::string StripPathAndLower(std::string path) {
|
||||||
return 0; // unmodified
|
std::size_t n = path.find_last_of("/\\");
|
||||||
}
|
if (n != std::string::npos) {
|
||||||
|
path = path.substr(n + 1);
|
||||||
}
|
}
|
||||||
return 1; // modified
|
std::transform(path.begin(), path.end(), path.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// initialize to empty (no deallocation)
|
namespace mujoco::user {
|
||||||
|
|
||||||
|
VFS::VFS(mjVFS* vfs) : self_(vfs) {
|
||||||
|
mjp_defaultResourceProvider(&default_provider_);
|
||||||
|
default_provider_.open = [](mjResource* res) {
|
||||||
|
return OpenFile(res->name, res);
|
||||||
|
};
|
||||||
|
default_provider_.read = [](mjResource* res, const void** buffer) {
|
||||||
|
return ReadFile(res->name, res, buffer);
|
||||||
|
};
|
||||||
|
default_provider_.close = [](mjResource* res) {
|
||||||
|
CloseFile(res);
|
||||||
|
};
|
||||||
|
default_provider_.modified = [](const mjResource* res, const char* time) {
|
||||||
|
return FileModified(res, time);
|
||||||
|
};
|
||||||
|
|
||||||
|
default_provider_.prefix = nullptr;
|
||||||
|
default_mount_.vfs = self_;
|
||||||
|
default_mount_.provider = &default_provider_;
|
||||||
|
default_mount_.data = nullptr;
|
||||||
|
default_mount_.name = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS::~VFS() {
|
||||||
|
if (!open_resources_.empty()) {
|
||||||
|
mju_warning(
|
||||||
|
"VFS destroyed with %zu open resources. Resources will be invalidated.",
|
||||||
|
open_resources_.size());
|
||||||
|
}
|
||||||
|
for (auto& [ptr, res] : open_resources_) {
|
||||||
|
if (res->provider->close) {
|
||||||
|
res->provider->close(res.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open_resources_.clear();
|
||||||
|
|
||||||
|
for (auto& [path, res] : mounts_) {
|
||||||
|
if (res->provider->unmount) {
|
||||||
|
res->provider->unmount(res.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mounts_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
mjResource* VFS::Open(const char* dir, const char* name) {
|
||||||
|
const std::string path = FilePath(dir, name).Str();
|
||||||
|
|
||||||
|
const mjResource* mount = FindMount(path);
|
||||||
|
if (!mount) {
|
||||||
|
MaybeSelfDestruct();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourcePtr res = CreateResource(path.c_str(), mount->provider);
|
||||||
|
|
||||||
|
// Smuggle the mounted resource provider's mount-specific data pointer in the
|
||||||
|
// requested resource's data pointer. This allows the provider to access its
|
||||||
|
// own per-mount data without any intrusive changes to the provider interface.
|
||||||
|
res->data = mount->data;
|
||||||
|
const int result = mount->provider->open(res.get());
|
||||||
|
// If the data pointer was not modified, then that means the resource did not
|
||||||
|
// set its own data pointer. So, we need to set it back to nullptr.
|
||||||
|
if (res->data == mount->data) {
|
||||||
|
res->data = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == 0) {
|
||||||
|
res.reset();
|
||||||
|
MaybeSelfDestruct();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
mjResource* res_ptr = res.get();
|
||||||
|
open_resources_.emplace(res_ptr, std::move(res));
|
||||||
|
return res_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS::Status VFS::Mount(const FilePath& path,
|
||||||
|
const mjpResourceProvider* provider) {
|
||||||
|
if (!provider) {
|
||||||
|
return kInvalidResourceProvider;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (mounts_.contains(path.Str())) {
|
||||||
|
return kRepeatedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourcePtr res = CreateResource(path.c_str(), provider);
|
||||||
|
provider->mount(res.get());
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
mounts_.emplace(path.Str(), std::move(res));
|
||||||
|
return kSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS::Status VFS::Close(mjResource* res) {
|
||||||
|
VFS::Status status = kInvalidResource;
|
||||||
|
bool last_resource = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (auto it = open_resources_.find(res); it != open_resources_.end()) {
|
||||||
|
if (res->provider->close) {
|
||||||
|
res->provider->close(res);
|
||||||
|
}
|
||||||
|
open_resources_.erase(it);
|
||||||
|
last_resource = open_resources_.empty();
|
||||||
|
status = kSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == kSuccess && last_resource) {
|
||||||
|
MaybeSelfDestruct();
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS::Status VFS::Unmount(const FilePath& path) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
if (auto it = mounts_.find(path.Str()); it != mounts_.end()) {
|
||||||
|
if (it->second->provider->unmount) {
|
||||||
|
it->second->provider->unmount(it->second.get());
|
||||||
|
}
|
||||||
|
mounts_.erase(it);
|
||||||
|
return kSuccess;
|
||||||
|
}
|
||||||
|
return kInvalidResourceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
int VFS::Read(mjResource* resource, const void** buffer) {
|
||||||
|
if (resource && resource->provider && resource->provider->read) {
|
||||||
|
return resource->provider->read(resource, buffer);
|
||||||
|
}
|
||||||
|
return kFailedToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS::ResourcePtr VFS::CreateResource(std::string_view name,
|
||||||
|
const mjpResourceProvider* provider) {
|
||||||
|
mjResource* res = new mjResource();
|
||||||
|
res->vfs = self_;
|
||||||
|
res->provider = provider;
|
||||||
|
res->data = nullptr;
|
||||||
|
res->name = new char[name.size() + 1];
|
||||||
|
std::strncpy(res->name, name.data(), name.size());
|
||||||
|
res->name[name.size()] = 0;
|
||||||
|
res->timestamp[0] = 0;
|
||||||
|
|
||||||
|
return ResourcePtr(res, [](mjResource* ptr) {
|
||||||
|
if (ptr->data) {
|
||||||
|
// TODO: Make this an error eventually. For now, we continue to allow
|
||||||
|
// users to free their data pointers without resetting them to nullptr.
|
||||||
|
mju_warning(
|
||||||
|
"mjResource::data is not null; did you forget to close/unmount it?");
|
||||||
|
}
|
||||||
|
delete[] ptr->name;
|
||||||
|
delete ptr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mjResource* VFS::FindMount(const std::string& fullpath) {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
std::string str = fullpath;
|
||||||
|
while (!str.empty()) {
|
||||||
|
auto it = mounts_.find(str);
|
||||||
|
if (it != mounts_.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t n = str.find_last_of("/\\");
|
||||||
|
if (n == std::string::npos) {
|
||||||
|
str = "";
|
||||||
|
} else {
|
||||||
|
str = str.substr(0, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mjpResourceProvider* provider =
|
||||||
|
mjp_getResourceProvider(fullpath.c_str());
|
||||||
|
if (provider) {
|
||||||
|
if (auto it = mounts_.find(provider->prefix); it != mounts_.end()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourcePtr res = CreateResource(provider->prefix, provider);
|
||||||
|
mjResource* res_ptr = res.get();
|
||||||
|
mounts_.emplace(provider->prefix, std::move(res));
|
||||||
|
if (provider->mount) {
|
||||||
|
provider->mount(res_ptr);
|
||||||
|
}
|
||||||
|
return res_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy use-case: match on just the case-insensitive filename.
|
||||||
|
const std::string filename = StripPathAndLower(fullpath);
|
||||||
|
for (auto& [path, res] : mounts_) {
|
||||||
|
if (StripPathAndLower(path) == filename) {
|
||||||
|
return res.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &default_mount_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VFS::MaybeSelfDestruct() {
|
||||||
|
if (destructor_) {
|
||||||
|
destructor_(self_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VFS::SetToSelfDestruct(std::function<void(mjVFS*)> destructor) {
|
||||||
|
destructor_ = std::move(destructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
VFS* VFS::Upcast(mjVFS* vfs) {
|
||||||
|
return vfs ? static_cast<VFS*>(vfs->impl_) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VFS* VFS::Upcast(const mjVFS* vfs) {
|
||||||
|
return vfs ? static_cast<const VFS*>(vfs->impl_) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mujoco::user
|
||||||
|
|
||||||
void mj_defaultVFS(mjVFS* vfs) {
|
void mj_defaultVFS(mjVFS* vfs) {
|
||||||
vfs->impl_ = new VFS();
|
if (vfs == nullptr) {
|
||||||
|
mju_error("mjVFS is null.");
|
||||||
|
} else {
|
||||||
|
vfs->impl_ = new mujoco::user::VFS(vfs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add file to VFS, return 0: success, 2: repeated name, -1: failed to load
|
|
||||||
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename) {
|
|
||||||
VFS* cvfs = GetVFSImpl(vfs);
|
|
||||||
|
|
||||||
// make full name
|
|
||||||
const char* dir = directory != nullptr ? directory : "";
|
|
||||||
FilePath fullname = FilePath(dir, filename);
|
|
||||||
|
|
||||||
// strip path
|
|
||||||
FilePath newname = StripPath(filename);
|
|
||||||
|
|
||||||
// check beforehand for repeated name, to avoid reading file into memory
|
|
||||||
if (cvfs->HasFile(newname)) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocate and read
|
|
||||||
std::vector<uint8_t> buffer = FileToMemory(fullname.c_str());
|
|
||||||
if (buffer.empty()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cvfs->AddFile(newname, std::move(buffer), vfs_hash(buffer))) {
|
|
||||||
return 2; // AddFile failed, SHOULD NOT OCCUR
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add file from buffer into VFS
|
|
||||||
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer,
|
|
||||||
int nbuffer) {
|
|
||||||
std::vector<uint8_t> inbuffer;
|
|
||||||
VFS* cvfs = GetVFSImpl(vfs);
|
|
||||||
VFSFile* file;
|
|
||||||
if (!(file = cvfs->AddFile(FilePath(name), std::move(inbuffer), 0))) {
|
|
||||||
return 2; // AddFile failed, repeated name
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
file->filedata.reserve(nbuffer);
|
|
||||||
} catch (...) {
|
|
||||||
cvfs->DeleteFile(file->filename); // delete file on error
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
file->filestamp = vfs_memcpy(file->filedata, buffer, nbuffer);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete file from VFS, return 0: success, -1: not found in VFS
|
|
||||||
int mj_deleteFileVFS(mjVFS* vfs, const char* filename) {
|
|
||||||
VFS* cvfs = GetVFSImpl(vfs);
|
|
||||||
if (cvfs->DeleteFile(StripPath(filename))) {
|
|
||||||
return cvfs->DeleteFile(FilePath(filename));
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete all files from VFS
|
|
||||||
void mj_deleteVFS(mjVFS* vfs) {
|
void mj_deleteVFS(mjVFS* vfs) {
|
||||||
if (vfs) {
|
if (vfs) {
|
||||||
delete GetVFSImpl(vfs);
|
delete mujoco::user::VFS::Upcast(vfs);
|
||||||
|
vfs->impl_ = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mjpResourceProvider* GetVfsResourceProvider() {
|
int mj_mountVFS(mjVFS* vfs, const char* filepath,
|
||||||
static mjpResourceProvider provider
|
const mjpResourceProvider* provider) {
|
||||||
= { nullptr, &Open, &Read, &Close, &Modified, nullptr };
|
mujoco::user::VFS* impl = mujoco::user::VFS::Upcast(vfs);
|
||||||
return &provider;
|
if (impl == nullptr) {
|
||||||
|
mju_error("mjVFS is null.");
|
||||||
|
return mujoco::user::VFS::kInvalidVfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filepath == nullptr) {
|
||||||
|
return mujoco::user::VFS::kNotFound;
|
||||||
|
}
|
||||||
|
const mujoco::user::FilePath path(filepath);
|
||||||
|
const mujoco::user::VFS::Status status = impl->Mount(path, provider);
|
||||||
|
return static_cast<int>(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mj_unmountVFS(mjVFS* vfs, const char* filename) {
|
||||||
|
mujoco::user::VFS* impl = mujoco::user::VFS::Upcast(vfs);
|
||||||
|
if (impl == nullptr) {
|
||||||
|
mju_error("mjVFS is null.");
|
||||||
|
return mujoco::user::VFS::kInvalidVfs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename == nullptr) {
|
||||||
|
return mujoco::user::VFS::kNotFound;
|
||||||
|
}
|
||||||
|
const mujoco::user::FilePath path(filename);
|
||||||
|
const mujoco::user::VFS::Status status = impl->Unmount(path);
|
||||||
|
return static_cast<int>(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Custom provider for mj_addFileVFS and mj_addBufferVFS.
|
||||||
|
class BufferProvider : public mjpResourceProvider {
|
||||||
|
public:
|
||||||
|
template <typename... Args>
|
||||||
|
static int Mount(mjVFS* vfs, Args&&... args) {
|
||||||
|
mujoco::user::VFS* impl = mujoco::user::VFS::Upcast(vfs);
|
||||||
|
if (impl == nullptr) {
|
||||||
|
mju_error("mjVFS is null.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferProvider* provider = new BufferProvider(std::forward<Args>(args)...);
|
||||||
|
provider->mount = [](mjResource* res) {
|
||||||
|
return static_cast<int>(mujoco::user::VFS::kSuccess);
|
||||||
|
};
|
||||||
|
provider->unmount = [](mjResource* res) {
|
||||||
|
delete (BufferProvider*)res->provider;
|
||||||
|
return static_cast<int>(mujoco::user::VFS::kSuccess);
|
||||||
|
};
|
||||||
|
provider->open = [](mjResource* res) {
|
||||||
|
BufferProvider* self = (BufferProvider*)res->provider;
|
||||||
|
mju_encodeBase64(res->timestamp, (std::uint8_t*)&self->hash_,
|
||||||
|
sizeof(self->hash_));
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
provider->read = [](mjResource* res, const void** out) {
|
||||||
|
BufferProvider* self = (BufferProvider*)res->provider;
|
||||||
|
*out = reinterpret_cast<void*>(self->contents_.data());
|
||||||
|
return static_cast<int>(self->contents_.size());
|
||||||
|
};
|
||||||
|
provider->close = [](mjResource* res) {
|
||||||
|
// no-op
|
||||||
|
};
|
||||||
|
provider->modified = [](const mjResource* res, const char* timestamp) {
|
||||||
|
const BufferProvider* self = (const BufferProvider*)res->provider;
|
||||||
|
if (mju_isValidBase64(timestamp) > sizeof(std::uint64_t)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::uint64_t test = 0;
|
||||||
|
mju_decodeBase64((std::uint8_t*)&test, timestamp);
|
||||||
|
if (self->hash_ != test) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mujoco::user::VFS::Status status =
|
||||||
|
impl->Mount(provider->path_, provider);
|
||||||
|
if (status != mujoco::user::VFS::kSuccess) {
|
||||||
|
delete provider;
|
||||||
|
}
|
||||||
|
return static_cast<int>(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
BufferProvider(const char* dir, const char* filename) {
|
||||||
|
mjp_defaultResourceProvider(this);
|
||||||
|
|
||||||
|
mujoco::user::FilePath file_path(dir ? dir : "", filename);
|
||||||
|
path_ = file_path.StripPath().Lower();
|
||||||
|
contents_ = mujoco::user::FileToMemory(file_path.c_str());
|
||||||
|
|
||||||
|
static constexpr std::uint64_t prime = 0x100000001b3;
|
||||||
|
hash_ = contents_.empty() ? 0 : 0xcbf29ce484222325;
|
||||||
|
for (const std::uint8_t& byte : contents_) {
|
||||||
|
hash_ |= byte;
|
||||||
|
hash_ *= prime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferProvider(const char* name, const void* src, size_t n) {
|
||||||
|
mjp_defaultResourceProvider(this);
|
||||||
|
path_ = mujoco::user::FilePath(name);
|
||||||
|
|
||||||
|
static constexpr std::uint64_t prime = 0x100000001b3;
|
||||||
|
hash_ = n ? 0xcbf29ce484222325 : 0;
|
||||||
|
|
||||||
|
const std::uint8_t* src_bytes = static_cast<const std::uint8_t*>(src);
|
||||||
|
contents_.reserve(n);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
contents_.push_back(src_bytes[i]);
|
||||||
|
hash_ |= src_bytes[i];
|
||||||
|
hash_ *= prime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mujoco::user::FilePath path_;
|
||||||
|
std::vector<std::uint8_t> contents_;
|
||||||
|
std::uint64_t hash_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename) {
|
||||||
|
// Opens the files and copies its contents into the BufferProvider, then
|
||||||
|
// mounts the provider at the given path.
|
||||||
|
return BufferProvider::Mount(vfs, directory, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mj_addBufferVFS(mjVFS* vfs, const char* name, const void* buffer,
|
||||||
|
int nbuffer) {
|
||||||
|
// Copies the buffer into the BufferProvider and mounts it at the given path.
|
||||||
|
return BufferProvider::Mount(vfs, name, buffer, nbuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mj_deleteFileVFS(mjVFS* vfs, const char* filename) {
|
||||||
|
if (filename == nullptr) {
|
||||||
|
return mujoco::user::VFS::kNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mj_unmountVFS(vfs, filename) != 0) {
|
||||||
|
mujoco::user::FilePath path(filename);
|
||||||
|
return mj_unmountVFS(vfs, path.StripPath().Lower().c_str());
|
||||||
|
}
|
||||||
|
return mujoco::user::VFS::kSuccess;
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-22
@@ -17,38 +17,115 @@
|
|||||||
#ifndef MUJOCO_SRC_USER_USER_VFS_H_
|
#ifndef MUJOCO_SRC_USER_USER_VFS_H_
|
||||||
#define MUJOCO_SRC_USER_USER_VFS_H_
|
#define MUJOCO_SRC_USER_USER_VFS_H_
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <mujoco/mjexport.h>
|
|
||||||
#include <mujoco/mjmodel.h>
|
#include <mujoco/mjmodel.h>
|
||||||
#include <mujoco/mjplugin.h>
|
#include <mujoco/mujoco.h>
|
||||||
|
#include "user/user_util.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
namespace mujoco::user {
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Initialize an empty VFS, mj_deleteVFS must be called to deallocate the VFS
|
// Underlying Virtual File System implementation for opaque mjVFS struct.
|
||||||
MJAPI void mj_defaultVFS(mjVFS* vfs);
|
//
|
||||||
|
// This class owns and manages all the mjResource instances that are created
|
||||||
|
// using the mju_openResource. Its main job is to find the correct
|
||||||
|
// mjpResourceProvider to handle the mju open/read/close operations. It does
|
||||||
|
// this by "mounting" mjpResourceProviders at specific paths such that any
|
||||||
|
// operation within that path will be handled by the mjpResourceProvider.
|
||||||
|
//
|
||||||
|
// Mounting can be done explicitly (using mj_mountVFS) or implicitly (using
|
||||||
|
// mjp_registerResourceProvider). If no provider is found for a given path,
|
||||||
|
// then a "default" provider is used that uses normal C file operations to
|
||||||
|
// open/read/close files.
|
||||||
|
//
|
||||||
|
// To support legacy use-cases (where the VFS is an optional argument), a
|
||||||
|
// "self-destruct" mode can be configured so that a temporary VFS instance can
|
||||||
|
// be created with a lifetime tied to the opened mjResource instance.
|
||||||
|
//
|
||||||
|
// This class itself is thread-safe, but it makes no guarantees about the
|
||||||
|
// thread-safety of the underlying mjResourceProviders.
|
||||||
|
class VFS {
|
||||||
|
public:
|
||||||
|
explicit VFS(mjVFS* vfs);
|
||||||
|
~VFS();
|
||||||
|
|
||||||
// add file to VFS, return 0: success, 2: repeated name, -1: not found on disk
|
VFS(const VFS&) = delete;
|
||||||
MJAPI int mj_addFileVFS(mjVFS* vfs, const char* directory, const char* filename);
|
VFS& operator=(const VFS&) = delete;
|
||||||
|
|
||||||
// add file from buffer into VFS, return 0: success, 2: repeated name, -1: failed to load
|
// Status codes for VFS operations. Values are based on the mj_VFS APIs.
|
||||||
MJAPI int mj_addBufferVFS(mjVFS* vfs, const char* filename, const void* buffer, int nbuffer);
|
enum Status {
|
||||||
|
kSuccess = 0,
|
||||||
|
kFailedToLoad = -1,
|
||||||
|
kFailedToRead = -1,
|
||||||
|
kNotFound = -1,
|
||||||
|
kRepeatedName = 2,
|
||||||
|
kInvalidVfs = -1,
|
||||||
|
kInvalidResource = -1,
|
||||||
|
kInvalidResourceProvider = -1,
|
||||||
|
};
|
||||||
|
|
||||||
// return file index in VFS, or -1 if not found in VFS
|
// Opens a mjResource for the given path, or nullptr on error. If successful,
|
||||||
MJAPI int mj_findFileVFS(const mjVFS* vfs, const char* filename);
|
// will invoke the 'open' callback for the mjpResourceProvider associated with
|
||||||
|
// the path/dir.
|
||||||
|
mjResource* Open(const char* dir, const char* name);
|
||||||
|
|
||||||
// delete file from VFS, return 0: success, -1: not found in VFS
|
// Sets `buffer` to the contents of the resource and returns the number of
|
||||||
MJAPI int mj_deleteFileVFS(mjVFS* vfs, const char* filename);
|
// bytes of the content. This is done by invoking the 'read' callback for the
|
||||||
|
// mjpResourceProvider associated with the resource. Returns -1 on error.
|
||||||
|
int Read(mjResource* resource, const void** buffer);
|
||||||
|
|
||||||
// delete all files from VFS
|
// Closes the resource by invoking the 'close' callback for the
|
||||||
MJAPI void mj_deleteVFS(mjVFS* vfs);
|
// mjpResourceProvider associated with the resource.
|
||||||
|
Status Close(mjResource* resource);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
// Mounts a ResourceProvider at the given path. All subsequent operations
|
||||||
}
|
// under `path` will be delegated to the `provider` until it is unmounted.
|
||||||
#endif
|
Status Mount(const FilePath& path, const mjpResourceProvider* provider);
|
||||||
|
|
||||||
const mjpResourceProvider* GetVfsResourceProvider();
|
// Unmounts the ResourceProvider from the given path.
|
||||||
|
Status Unmount(const FilePath& path);
|
||||||
|
|
||||||
|
// Sets a destructor to be called when the VFS has no more open resources.
|
||||||
|
// Assumes that `destructor` will delete `this`.
|
||||||
|
//
|
||||||
|
// This is useful for when you want to create a temporary VFS instance with
|
||||||
|
// a lifetime tied to a single mjResource to be opened. The `destructor`
|
||||||
|
// should be set to `delete this` and any other cleanup that needs to happen.
|
||||||
|
void SetToSelfDestruct(std::function<void(mjVFS*)> destructor);
|
||||||
|
|
||||||
|
// Converts the public C-API pointer to the internal C++ class.
|
||||||
|
static VFS* Upcast(mjVFS* vfs);
|
||||||
|
static const VFS* Upcast(const mjVFS* vfs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
using ResourcePtr = std::unique_ptr<mjResource, void (*)(mjResource*)>;
|
||||||
|
ResourcePtr CreateResource(std::string_view name,
|
||||||
|
const mjpResourceProvider* provider);
|
||||||
|
|
||||||
|
// Returns a mounted mjResource* that matches the given path. If no explicitly
|
||||||
|
// mounted mjResource* is found, returns a "default" mounting that uses the
|
||||||
|
// C file system.
|
||||||
|
mjResource* FindMount(const std::string& fullpath);
|
||||||
|
|
||||||
|
// Invokes the `destructor_, but only if it has been set previously. This
|
||||||
|
// should be only called when resources_ is empty and callers should assume
|
||||||
|
// that `this` will be invalidated after this call.
|
||||||
|
void MaybeSelfDestruct();
|
||||||
|
|
||||||
|
mjVFS* self_;
|
||||||
|
std::mutex mutex_; // Protects open_resources_ and mounts_.
|
||||||
|
std::unordered_map<mjResource*, ResourcePtr> open_resources_;
|
||||||
|
std::unordered_map<std::string, ResourcePtr> mounts_;
|
||||||
|
mjResource default_mount_;
|
||||||
|
mjpResourceProvider default_provider_;
|
||||||
|
std::function<void(mjVFS*)> destructor_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mujoco::user
|
||||||
|
|
||||||
#endif // MUJOCO_SRC_USER_USER_VFS_H_
|
#endif // MUJOCO_SRC_USER_USER_VFS_H_
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ TEST_F(ResourceTest, GeneralFailureTest) {
|
|||||||
error.data(), error.size());
|
error.data(), error.size());
|
||||||
ASSERT_THAT(resource, IsNull());
|
ASSERT_THAT(resource, IsNull());
|
||||||
|
|
||||||
EXPECT_THAT(error.data(), HasSubstr("could not open"));
|
EXPECT_THAT(error.data(), HasSubstr("Error opening file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ResourceTest, NameWithValidPrefix) {
|
TEST_F(ResourceTest, NameWithValidPrefix) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
namespace mujoco {
|
namespace mujoco {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using ::testing::IsNull;
|
||||||
using ::testing::NotNull;
|
using ::testing::NotNull;
|
||||||
using UserVfsTest = MujocoTest;
|
using UserVfsTest = MujocoTest;
|
||||||
|
|
||||||
@@ -36,6 +37,46 @@ static bool HasFile(const mjVFS* vfs, const std::string& filename) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestProvider : public mjpResourceProvider {
|
||||||
|
// The TestProvider will increment the value at `addr` by these amounts when
|
||||||
|
// each callback is invoked. This can be used to verity that the correct
|
||||||
|
// callbacks are being invoked when expected.
|
||||||
|
enum CallbackValues {
|
||||||
|
Mounted = 100,
|
||||||
|
Unmounted = 200,
|
||||||
|
Opened = 300,
|
||||||
|
Read = 400,
|
||||||
|
Closed = 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit TestProvider(int* addr) {
|
||||||
|
mjp_defaultResourceProvider(this);
|
||||||
|
prefix = "test";
|
||||||
|
data = addr;
|
||||||
|
|
||||||
|
mount = [](mjResource* res) {
|
||||||
|
*(int*)res->provider->data += Mounted;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
unmount = [](mjResource* res) {
|
||||||
|
*(int*)res->provider->data += Unmounted;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
open = [](mjResource* res) {
|
||||||
|
*(int*)res->provider->data += Opened;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
read = [](mjResource* res, const void** out) {
|
||||||
|
*(int*)res->provider->data += Read;
|
||||||
|
*out = res->provider->data;
|
||||||
|
return (int)sizeof(int);
|
||||||
|
};
|
||||||
|
close = [](mjResource* res) {
|
||||||
|
*(int*)res->provider->data += Closed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
TEST_F(UserVfsTest, AddFile) {
|
TEST_F(UserVfsTest, AddFile) {
|
||||||
constexpr char path[] = "engine/testdata/actuation/";
|
constexpr char path[] = "engine/testdata/actuation/";
|
||||||
const std::string dir = GetTestDataFilePath(path);
|
const std::string dir = GetTestDataFilePath(path);
|
||||||
@@ -245,5 +286,126 @@ TEST_F(UserVfsTest, Timestamps) {
|
|||||||
mj_deleteVFS(&vfs);
|
mj_deleteVFS(&vfs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(UserVfsTest, MountUnmount) {
|
||||||
|
int test = 0;
|
||||||
|
int expect = 0;
|
||||||
|
TestProvider provider(&test);
|
||||||
|
|
||||||
|
mjVFS vfs;
|
||||||
|
mj_defaultVFS(&vfs);
|
||||||
|
mjResource* res = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
|
||||||
|
EXPECT_THAT(res, IsNull());
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
mj_mountVFS(&vfs, "/some/path", &provider);
|
||||||
|
expect += TestProvider::Mounted;
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
res = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
|
||||||
|
expect += TestProvider::Opened;
|
||||||
|
EXPECT_THAT(res, NotNull());
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
const void* buffer = nullptr;
|
||||||
|
const int size = mju_readResource(res, &buffer);
|
||||||
|
expect += TestProvider::Read;
|
||||||
|
EXPECT_THAT(buffer, NotNull());
|
||||||
|
EXPECT_GT(size, 0);
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
mju_closeResource(res);
|
||||||
|
expect += TestProvider::Closed;
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
mj_unmountVFS(&vfs, "/some/path");
|
||||||
|
expect += TestProvider::Unmounted;
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
mj_deleteVFS(&vfs);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UserVfsTest, AutoMountProviders) {
|
||||||
|
int test = 0;
|
||||||
|
int expect = 0;
|
||||||
|
|
||||||
|
TestProvider provider(&test);
|
||||||
|
mjp_registerResourceProvider(&provider);
|
||||||
|
|
||||||
|
mjVFS vfs;
|
||||||
|
mj_defaultVFS(&vfs);
|
||||||
|
|
||||||
|
mjResource* res = mju_openResource("", "test:foo", &vfs, nullptr, 0);
|
||||||
|
expect += TestProvider::Mounted + TestProvider::Opened;
|
||||||
|
EXPECT_THAT(res, NotNull());
|
||||||
|
|
||||||
|
mju_closeResource(res);
|
||||||
|
expect += TestProvider::Closed;
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
|
||||||
|
mj_deleteVFS(&vfs);
|
||||||
|
expect += TestProvider::Unmounted;
|
||||||
|
EXPECT_EQ(test, expect);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(UserVfsTest, StackedMounts) {
|
||||||
|
int test1 = 0;
|
||||||
|
int test2 = 1000;
|
||||||
|
int test3 = 1000000;
|
||||||
|
int expect1 = test1;
|
||||||
|
int expect2 = test2;
|
||||||
|
int expect3 = test3;
|
||||||
|
|
||||||
|
TestProvider provider1(&test1);
|
||||||
|
TestProvider provider2(&test2);
|
||||||
|
TestProvider provider3(&test3);
|
||||||
|
|
||||||
|
mjVFS vfs;
|
||||||
|
mj_defaultVFS(&vfs);
|
||||||
|
|
||||||
|
mj_mountVFS(&vfs, "/some/path", &provider1);
|
||||||
|
mj_mountVFS(&vfs, "/some/path/further/down/very/deep", &provider2);
|
||||||
|
mj_mountVFS(&vfs, "/some/path/further/down", &provider3);
|
||||||
|
expect1 += TestProvider::Mounted;
|
||||||
|
expect2 += TestProvider::Mounted;
|
||||||
|
expect3 += TestProvider::Mounted;
|
||||||
|
EXPECT_EQ(test1, expect1);
|
||||||
|
EXPECT_EQ(test2, expect2);
|
||||||
|
EXPECT_EQ(test3, expect3);
|
||||||
|
|
||||||
|
mjResource* res1 = mju_openResource("", "/some/path/foo", &vfs, nullptr, 0);
|
||||||
|
expect1 += TestProvider::Opened;
|
||||||
|
EXPECT_EQ(test1, expect1);
|
||||||
|
EXPECT_EQ(test2, expect2);
|
||||||
|
EXPECT_EQ(test3, expect3);
|
||||||
|
|
||||||
|
mjResource* res2 =
|
||||||
|
mju_openResource("", "/some/path/further/down/foo", &vfs, nullptr, 0);
|
||||||
|
expect3 += TestProvider::Opened;
|
||||||
|
EXPECT_EQ(test1, expect1);
|
||||||
|
EXPECT_EQ(test2, expect2);
|
||||||
|
EXPECT_EQ(test3, expect3);
|
||||||
|
|
||||||
|
mjResource* res3 = mju_openResource(
|
||||||
|
"", "/some/path/further/down/very/deep/foo", &vfs, nullptr, 0);
|
||||||
|
expect2 += TestProvider::Opened;
|
||||||
|
EXPECT_EQ(test1, expect1);
|
||||||
|
EXPECT_EQ(test2, expect2);
|
||||||
|
EXPECT_EQ(test3, expect3);
|
||||||
|
|
||||||
|
mju_closeResource(res1);
|
||||||
|
mju_closeResource(res2);
|
||||||
|
mju_closeResource(res3);
|
||||||
|
expect1 += TestProvider::Closed;
|
||||||
|
expect2 += TestProvider::Closed;
|
||||||
|
expect3 += TestProvider::Closed;
|
||||||
|
|
||||||
|
mj_deleteVFS(&vfs);
|
||||||
|
expect1 += TestProvider::Unmounted;
|
||||||
|
expect2 += TestProvider::Unmounted;
|
||||||
|
expect3 += TestProvider::Unmounted;
|
||||||
|
EXPECT_EQ(test1, expect1);
|
||||||
|
EXPECT_EQ(test2, expect2);
|
||||||
|
EXPECT_EQ(test3, expect3);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mujoco
|
} // namespace mujoco
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ TEST_F(LoadXmlTest, InvalidFileFails) {
|
|||||||
std::array<char, 1000> error;
|
std::array<char, 1000> error;
|
||||||
mjSpec* spec = mj_parseXML("invalid", nullptr, error.data(), error.size());
|
mjSpec* spec = mj_parseXML("invalid", nullptr, error.data(), error.size());
|
||||||
EXPECT_THAT(spec, IsNull()) << "Expected model loading to fail.";
|
EXPECT_THAT(spec, IsNull()) << "Expected model loading to fail.";
|
||||||
EXPECT_THAT(error.data(), HasSubstr("No such file or directory"));
|
EXPECT_THAT(error.data(), HasSubstr("Error opening file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(MujocoTest, SaveXmlShortString) {
|
TEST_F(MujocoTest, SaveXmlShortString) {
|
||||||
|
|||||||
Reference in New Issue
Block a user