First-pass implementation of a spec loader for zipped assets.

PiperOrigin-RevId: 861625990
Change-Id: I65cf601cd9aa3aab4915a5a247dda00a6a9aa3db
This commit is contained in:
Haroon Qureshi
2026-01-27 03:17:38 -08:00
committed by Copybara-Service
parent e43ed60d29
commit 32a4412e2b
3 changed files with 262 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2026 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 <cstdarg>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <string>
#include <string_view>
#include <span>
#include <unordered_map>
#include <utility>
#include <vector>
#include <miniz_zip.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include "user/user_resource.h"
static void mjPRINTFLIKE(3, 4)
SetError(char* error, int error_sz, const char* format, ...) {
if (error) {
va_list args;
va_start(args, format);
std::vsnprintf(error, error_sz, format, args);
va_end(args);
}
}
// A mjpResourceProvider that reads files from a zip archive.
//
// The zip archive itself is provided as a byte buffer in the constructor. This
// class can then be used to lazily read the contents of the individual files
// from within the archive as needed.
class ZipArchiveProvider : public mjpResourceProvider {
public:
ZipArchiveProvider(std::string name, const void* buffer, int nbuffer,
char* error, int error_sz)
: name_(std::move(name)), buffer_((char*)buffer, (char*)buffer + nbuffer) {
mjp_defaultResourceProvider(this);
std::memset(&archive_, 0, sizeof(archive_));
if (!mz_zip_reader_init_mem(&archive_, buffer_.data(), nbuffer, 0)) {
SetError(error, error_sz, "Zip error: invalid zip archive");
return;
}
// Create an index of the files in the archive.
const int num_files = mz_zip_reader_get_num_files(&archive_);
for (int i = 0; i < num_files; ++i) {
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&archive_, i, &stat)) {
SetError(error, error_sz, "Zip error: failed to stat item %d.", i);
files_.clear();
return;
}
const int size = static_cast<int>(stat.m_uncomp_size);
if (size == 0) {
continue;
}
files_[stat.m_filename] = FileInfo{i, size, {}};
}
// Setup mjpResourceProvider callbacks.
mount = [](mjResource* resource) {
return 0;
};
unmount = [](mjResource* resource) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
delete self;
return 0;
};
open = [](mjResource* resource) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
const bool found = self->Contains(resource->name);
return found ? 1 : 0;
};
read = [](mjResource* resource, const void** buffer) {
ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider;
std::span<char> bytes = self->Read(resource->name);
*buffer = bytes.data();
return static_cast<int>(bytes.size());
};
close = [](mjResource* resource) {
// no-op
};
}
~ZipArchiveProvider() {
mz_zip_reader_end(&archive_);
}
ZipArchiveProvider(const ZipArchiveProvider&) = delete;
ZipArchiveProvider& operator=(const ZipArchiveProvider&) = delete;
// Returns the path to the root XML model in the archive.
std::string GetRootModelPath() const {
// Assumes the root has the same name as the archive itself, but with a .xml
// extension.
const std::filesystem::path path(name_);
return (path / path.stem()).string() + ".xml";
}
// Returns true if the archive contains a file with the given name/path.
bool Contains(std::string_view name) const {
const std::string_view filename = name.substr(name_.size() + 1);
return files_.find(filename.data()) != files_.end();
}
// Reads the contents of the file with the given name/path. The contents are
// cached internally so that subsequent reads for the same file do not need to
// re-read the file from the archive.
std::span<char> Read(const std::string& name) {
const std::string filename = name.substr(name_.size() + 1);
auto it = files_.find(filename);
if (it == files_.end()) {
return {};
}
FileInfo& info = it->second;
// Lazily read and store the file contents from the archive.
if (info.contents.empty()) {
info.contents.resize(info.size);
if (!mz_zip_reader_extract_to_mem(&archive_, info.index,
info.contents.data(), info.size, 0)) {
return {};
}
}
return info.contents;
}
private:
struct FileInfo {
// Index of the file in the archive.
int index = 0;
// Size (in bytes) of the uncompressed file
int size = 0;
// Contents of the uncompressed file.
std::vector<char> contents;
};
std::string name_;
mz_zip_archive archive_;
std::vector<char> buffer_;
std::unordered_map<std::string, FileInfo> files_;
};
static mjSpec* ParseZipBuffer(const void* buffer, int nbuffer, const char* name,
mjVFS* vfs, char* error, int error_sz) {
if (error) {
error[0] = 0;
}
ZipArchiveProvider* provider =
new ZipArchiveProvider(name, buffer, nbuffer, error, error_sz);
if (error && error[0]) {
return nullptr;
}
const int status = mj_mountVFS(vfs, name, provider);
if (status != 0) {
SetError(error, error_sz, "Failed to mount zip archive: %s", name);
return nullptr;
}
const std::string root = provider->GetRootModelPath();
return mj_parseXML(root.c_str(), vfs, error, error_sz);
}
mjPLUGIN_LIB_INIT {
mjpDecoder decoder;
decoder.content_type = "application/zip";
decoder.extension = ".mjz|.zip";
decoder.can_decode = +[](const mjResource* resource) {
const char* ext = strrchr(resource->name, '.');
return ext ? (!strcmp(ext, ".mjz") || !strcmp(ext, ".zip")) : 0;
};
decoder.decode = +[](mjResource* resource, const mjVFS* vfs) -> mjSpec* {
const char* buffer = nullptr;
const int size = mju_readResource(resource, (const void**)&buffer);
if (size <= 0) {
return nullptr;
}
return ParseZipBuffer(buffer, size, resource->name, const_cast<mjVFS*>(vfs),
nullptr, 0);
};
mjp_registerDecoder(&decoder);
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 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 <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include "test/fixture.h"
namespace mujoco {
namespace {
using MjzTest = MujocoTest;
using ::testing::IsNull;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::StrEq;
TEST_F(MjzTest, Parse) {
mjVFS vfs;
mj_defaultVFS(&vfs);
std::string filepath = GetTestDataFilePath("testdata/model.mjz");
char err[1000] = "";
mjSpec* spec = mj_parse(filepath.c_str(), "", &vfs, err, sizeof(err));
EXPECT_THAT(spec, NotNull());
EXPECT_THAT(err, StrEq(""));
mjModel* model = mj_compile(spec, &vfs);
EXPECT_THAT(model, NotNull()) << mjs_getError(spec);
mj_deleteModel(model);
mj_deleteSpec(spec);
mj_deleteVFS(&vfs);
}
TEST_F(MjzTest, InvalidPath) {
mjVFS vfs;
mj_defaultVFS(&vfs);
std::string filepath = GetTestDataFilePath("testdata/not_a_file.mjz");
char err[1000] = "";
mjSpec* spec = mj_parse(filepath.c_str(), "", nullptr, err, sizeof(err));
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(err, Not(StrEq("")));
mj_deleteVFS(&vfs);
}
} // namespace
} // namespace mujoco
BIN
View File
Binary file not shown.