2ab76ec4f5
- Guard #define stat _stat with !defined(__MINGW32__) to avoid conflicts with MinGW's own POSIX-compliant stat implementation - Replace _MSC_VER check with defined(_WIN32) for localtime_s to allow all Windows compilers including MinGW to use the secure function Fixes #3038, #3037
497 lines
14 KiB
C++
497 lines
14 KiB
C++
// Copyright 2021 DeepMind Technologies Limited
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "user/user_vfs.h"
|
|
|
|
#include <sys/stat.h>
|
|
#if defined(_WIN32) && !defined(__MINGW32__)
|
|
#define stat _stat
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <mujoco/mujoco.h>
|
|
#include "engine/engine_util_misc.h"
|
|
#include "user/user_util.h"
|
|
|
|
namespace {
|
|
|
|
// struct for holding the contents of a file
|
|
struct ResourceFileData {
|
|
std::vector<uint8_t> contents;
|
|
time_t modified_time = 0;
|
|
bool is_read = false;
|
|
};
|
|
|
|
int OpenFile(const char* filename, mjResource* resource) {
|
|
struct stat file_stat;
|
|
if (stat(filename, &file_stat) == 0) {
|
|
ResourceFileData* data = new ResourceFileData();
|
|
resource->data = data;
|
|
|
|
data->modified_time = file_stat.st_mtime;
|
|
mju_encodeBase64(resource->timestamp, (uint8_t*)&file_stat.st_mtime,
|
|
sizeof(time_t));
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int ReadFile(const char* filename, mjResource* resource, const void** buffer) {
|
|
ResourceFileData* data = (ResourceFileData*)resource->data;
|
|
if (!data->is_read) {
|
|
data->contents = mujoco::user::FileToMemory(filename);
|
|
data->is_read = true;
|
|
}
|
|
*buffer = data->contents.data();
|
|
return static_cast<int>(data->contents.size());
|
|
}
|
|
|
|
void CloseFile(mjResource* resource) {
|
|
delete (ResourceFileData*)resource->data;
|
|
resource->data = nullptr;
|
|
}
|
|
|
|
int FileModified(const mjResource* resource, const char* timestamp) {
|
|
if (mju_isValidBase64(timestamp) != sizeof(time_t)) {
|
|
return 1;
|
|
}
|
|
time_t time;
|
|
mju_decodeBase64((uint8_t*)&time, timestamp);
|
|
|
|
ResourceFileData* data = (ResourceFileData*)resource->data;
|
|
const double diff = difftime(data->modified_time, time);
|
|
|
|
if (diff < 0) return -1;
|
|
if (diff > 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
std::string StripPathAndLower(std::string path) {
|
|
std::size_t n = path.find_last_of("/\\");
|
|
if (n != std::string::npos) {
|
|
path = path.substr(n + 1);
|
|
}
|
|
std::transform(path.begin(), path.end(), path.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
return path;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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) {
|
|
if (vfs == nullptr) {
|
|
mju_error("mjVFS is null.");
|
|
} else {
|
|
vfs->impl_ = new mujoco::user::VFS(vfs);
|
|
}
|
|
}
|
|
|
|
void mj_deleteVFS(mjVFS* vfs) {
|
|
if (vfs) {
|
|
delete mujoco::user::VFS::Upcast(vfs);
|
|
vfs->impl_ = nullptr;
|
|
}
|
|
}
|
|
|
|
int mj_mountVFS(mjVFS* vfs, const char* filepath,
|
|
const mjpResourceProvider* provider) {
|
|
mujoco::user::VFS* impl = mujoco::user::VFS::Upcast(vfs);
|
|
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;
|
|
}
|