diff --git a/doc/changelog.rst b/doc/changelog.rst index a2243f40..b07ba2b0 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,6 +2,14 @@ Changelog ========= +Upcoming version (not yet released) +----------------------------------- + +Bug fixes +^^^^^^^^^ +- Fixed a bug in the ``mjz`` :ref:`decoder ` where unnormalized paths would fail to be read. + + Version 3.9.0 (May 27, 2026) ---------------------------- diff --git a/src/xml/mjz/mjz_decoder.cc b/src/xml/mjz/mjz_decoder.cc index f2864518..f2bd64bb 100644 --- a/src/xml/mjz/mjz_decoder.cc +++ b/src/xml/mjz/mjz_decoder.cc @@ -90,7 +90,7 @@ class ZipArchiveProvider : public mjpResourceProvider { // with the same name as the archive itself. Failing that, look for an XML // file within a subdirectory with the same name as the archive. const std::filesystem::path path(name_); - root_model_ = (path / path.stem()).string() + ".xml"; + root_model_ = (path / path.stem()).generic_string() + ".xml"; if (!Contains(root_model_)) { root_model_ = (path / path.stem() / path.stem()).string() + ".xml"; if (!Contains(root_model_)) { @@ -110,12 +110,14 @@ class ZipArchiveProvider : public mjpResourceProvider { }; open = [](mjResource* resource) { ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider; - const bool found = self->Contains(resource->name); + auto path = std::filesystem::path(resource->name).lexically_normal(); + const bool found = self->Contains(path.string()); return found ? 1 : 0; }; read = [](mjResource* resource, const void** buffer) { ZipArchiveProvider* self = (ZipArchiveProvider*)resource->provider; - std::span bytes = self->Read(resource->name); + auto path = std::filesystem::path(resource->name).lexically_normal(); + std::span bytes = self->Read(path.string()); *buffer = bytes.data(); return static_cast(bytes.size()); }; @@ -138,6 +140,11 @@ class ZipArchiveProvider : public mjpResourceProvider { // Returns true if the archive contains a file with the given name/path. bool Contains(std::string_view name) const { + // Lexically normalized path might strip the archive path. + // a/b.mjz/../../../c.xml -> ../c.xml + if (!name.starts_with(name_)) { + return false; + } const std::string_view filename = name.substr(name_.size() + 1); return files_.find(filename.data()) != files_.end(); }