diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c7835ef..1e23e55a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- **Fix**: a zip entry name with a leading slash is read relative to the + archive root rather than throwing, and one named `/` alone is dropped. An + `.odt` carrying such an entry now opens; LibreOffice still refuses it. + - The rendered sheet exposes `odr.editing`: `enable()` / `disable()` turn the mode on, `lockAt()` answers for a cell, and a refusal reaches the host as `odr.onEditRefused` / `odr.onEditModeChange`, whose codes share the space diff --git a/src/odr/internal/zip/zip_archive.cpp b/src/odr/internal/zip/zip_archive.cpp index 2e424d94c..afd5fde00 100644 --- a/src/odr/internal/zip/zip_archive.cpp +++ b/src/odr/internal/zip/zip_archive.cpp @@ -44,6 +44,10 @@ ZipArchive::ZipArchive() = default; ZipArchive::ZipArchive(const std::shared_ptr &archive) { for (auto &&entry : *archive) { RelPath path(entry.path()); + // an entry named "/" addresses nothing + if (path.empty()) { + continue; + } if (entry.is_file()) { std::uint8_t compression_level = 6; if (entry.method() == util::Method::STORED) { diff --git a/src/odr/internal/zip/zip_util.cpp b/src/odr/internal/zip/zip_util.cpp index 793df44aa..e159c6fd4 100644 --- a/src/odr/internal/zip/zip_util.cpp +++ b/src/odr/internal/zip/zip_util.cpp @@ -132,7 +132,8 @@ RelPath Archive::Entry::path() const { std::array filename{}; mz_zip_reader_get_filename(m_archive->zip(), m_index, filename.data(), static_cast(filename.size())); - return RelPath(filename.data()); + // a leading slash is malformed (APPNOTE.TXT 4.4.17.1) and read away + return Path(filename.data()).make_relative(); } Method Archive::Entry::method() const { diff --git a/test/data.cmake b/test/data.cmake index a47ceac58..6560ab860 100644 --- a/test/data.cmake +++ b/test/data.cmake @@ -12,7 +12,7 @@ odr_test_data( odr_test_data( PATH "input/odr-private" URL "https://github.com/opendocument-app/OpenDocument.test-private.git" - REVISION "d0bedc89b67e73a2e4113f96f27d1d1cdf30c69f") + REVISION "c4efe97d67c9a12aa08965d696a0cb7a3dc4d025") odr_test_data( PATH "reference-output/odr-public" @@ -22,4 +22,4 @@ odr_test_data( odr_test_data( PATH "reference-output/odr-private" URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git" - REVISION "c43ef03e9ca239489141a8d91c0187ed9602436a") + REVISION "666ce5a24a1a2d4f74b1abf0f004c0778e9ebddd") diff --git a/test/src/internal/zip/zip_archive_test.cpp b/test/src/internal/zip/zip_archive_test.cpp index 0cc565fe2..30c48531e 100644 --- a/test/src/internal/zip/zip_archive_test.cpp +++ b/test/src/internal/zip/zip_archive_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -9,9 +10,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -130,6 +133,41 @@ TEST(ZipArchive, create_order) { } } +/// A leading slash, forbidden by APPNOTE.TXT 4.4.17.1, is read away; a name +/// that is nothing but the root addresses no entry. +TEST(ZipArchive, absolute_entry_name) { + const std::string path = + (std::filesystem::current_path() / "absolute-name.zip").string(); + + { + // miniz refuses the name, so a placeholder goes in and is patched out + mz_zip_archive archive{}; + ASSERT_TRUE(mz_zip_writer_init_file(&archive, path.c_str(), 0)); + ASSERT_TRUE(mz_zip_writer_add_mem(&archive, "@", nullptr, 0, 0)); + ASSERT_TRUE(mz_zip_writer_add_mem(&archive, "@one.txt", "abc", 3, 0)); + ASSERT_TRUE(mz_zip_writer_finalize_archive(&archive)); + ASSERT_TRUE(mz_zip_writer_end(&archive)); + + std::string data; + { + std::ifstream in(path, std::ios::binary); + data.assign(std::istreambuf_iterator(in), + std::istreambuf_iterator()); + } + std::ranges::replace(data, '@', '/'); + std::ofstream out(path, std::ios::binary); + out.write(data.data(), static_cast(data.size())); + } + + const auto zip = + std::make_shared(std::make_shared(path)); + EXPECT_EQ(3, zip->find(RelPath("one.txt"))->file()->size()); + + const ZipArchive read(zip); + EXPECT_EQ(1, std::distance(read.begin(), read.end())); + EXPECT_TRUE(read.as_filesystem()->is_file(AbsPath("/one.txt"))); +} + /// The read callback has to be re-entrant, for a memory and a stream source. TEST(ZipArchive, concurrent_entry_reads) { const std::string path =