| // Copyright 2026 Google LLC |
| // |
| // 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 |
| // |
| // https://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 "formats/zip/extractor.h" |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/log/check.h" |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/types/span.h" |
| #include "formats/byte_range.h" |
| #include "formats/zip/constants.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "riegeli/bytes/string_reader.h" |
| #include "riegeli/bytes/string_writer.h" |
| #include "riegeli/endian/endian_writing.h" |
| #include "testing/jumbf_utils.h" |
| #include "testing/test_file_utils.h" |
| |
| // Compatibility macros for OSS |
| #ifndef ASSERT_OK |
| #define ASSERT_OK(expr) ASSERT_THAT(expr, ::absl_testing::IsOk()) |
| #endif |
| |
| #ifndef ASSERT_OK_AND_ASSIGN |
| #define ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) x##y |
| #define ASSERT_OK_AND_ASSIGN_CONCAT(x, y) ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) |
| |
| #define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \ |
| ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, __COUNTER__) |
| |
| #define ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, id) \ |
| auto ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id) = (rexpr); \ |
| ASSERT_THAT(ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id), \ |
| ::absl_testing::IsOk()); \ |
| lhs = std::move(*ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id)) |
| #endif |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOkAndHolds; |
| using ::absl_testing::StatusIs; |
| using ::testing::AllOf; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::HasSubstr; |
| |
| // A Zip file entry. |
| struct ZipEntry { |
| std::string name; |
| std::string content; |
| }; |
| |
| // Creates a minimal Zip file in memory. |
| // This is not a full-fledged Zip creator, but enough to satisfy ZipReader. |
| absl::StatusOr<std::string> CreateFakeZip(absl::Span<const ZipEntry> entries) { |
| std::string buffer; |
| riegeli::StringWriter writer(&buffer); |
| |
| // Write local file headers and data. |
| std::vector<uint32_t> offsets; |
| for (const auto& entry : entries) { |
| offsets.push_back(writer.pos()); |
| |
| writer.Write(kZipLocalFileHeaderSignature); |
| // Version needed to extract (2.0) |
| riegeli::WriteLittleEndian<uint16_t>(20, writer); |
| // General purpose bit flag |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Compression method (stored) |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Last mod file time |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Last mod file date |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // CRC-32 (0 for now, we don't check it) |
| riegeli::WriteLittleEndian<uint32_t>(0, writer); |
| // Compressed size |
| riegeli::WriteLittleEndian<uint32_t>(entry.content.size(), writer); |
| // Uncompressed size |
| riegeli::WriteLittleEndian<uint32_t>(entry.content.size(), writer); |
| // File name length |
| riegeli::WriteLittleEndian<uint16_t>(entry.name.size(), writer); |
| // Extra field length |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| |
| // File name |
| writer.Write(entry.name); |
| // File data |
| writer.Write(entry.content); |
| } |
| |
| uint32_t central_directory_start_offset = writer.pos(); |
| |
| // Write central directory. |
| for (size_t i = 0; i < entries.size(); ++i) { |
| const auto& entry = entries[i]; |
| writer.Write(kZipCentralDirectorySignature); |
| // Version made by |
| riegeli::WriteLittleEndian<uint16_t>(20, writer); |
| // Version needed to extract |
| riegeli::WriteLittleEndian<uint16_t>(20, writer); |
| // General purpose bit flag |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Compression method |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Last mod file time |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Last mod file date |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // CRC-32 |
| riegeli::WriteLittleEndian<uint32_t>(0, writer); |
| // Compressed size |
| riegeli::WriteLittleEndian<uint32_t>(entry.content.size(), writer); |
| // Uncompressed size |
| riegeli::WriteLittleEndian<uint32_t>(entry.content.size(), writer); |
| // File name length |
| riegeli::WriteLittleEndian<uint16_t>(entry.name.size(), writer); |
| // Extra field length |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // File comment length |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Disk number start |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Internal file attributes |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // External file attributes |
| riegeli::WriteLittleEndian<uint32_t>(0, writer); |
| // Relative offset of local header |
| riegeli::WriteLittleEndian<uint32_t>(offsets[i], writer); |
| |
| // File name |
| writer.Write(entry.name); |
| } |
| |
| uint32_t central_directory_size = |
| writer.pos() - central_directory_start_offset; |
| |
| // Write End of Central Directory (EOCD) record. |
| writer.Write(kZipEndOfCentralDirectorySignature); |
| // Number of this disk |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Number of the disk with the start of the central directory |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| // Total number of entries in the central directory on this disk |
| riegeli::WriteLittleEndian<uint16_t>(entries.size(), writer); |
| // Total number of entries in the central directory |
| riegeli::WriteLittleEndian<uint16_t>(entries.size(), writer); |
| // Size of the central directory |
| riegeli::WriteLittleEndian<uint32_t>(central_directory_size, writer); |
| // Offset of start of central directory with respect to the starting disk |
| // number |
| riegeli::WriteLittleEndian<uint32_t>(central_directory_start_offset, writer); |
| // .ZIP file comment length |
| riegeli::WriteLittleEndian<uint16_t>(0, writer); |
| |
| if (!writer.Close()) { |
| return writer.status(); |
| } |
| return buffer; |
| } |
| |
| TEST(ZipExtractorTest, ExtractManifestStore) { |
| ASSERT_OK_AND_ASSIGN(auto input, |
| credentio_testing::GetFileReader( |
| "c2pa/formats/zip/testing/asset_fake_c2pa.zip")); |
| |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(*input), |
| IsOkAndHolds(HasSubstr("c2pa manifest"))); |
| } |
| |
| TEST(ZipExtractorTest, |
| ExtractManifestStoreThrowsErrorIfMultipleC2paManifestStoresFound) { |
| // Create a fake Zip file with duplicate manifest stores. |
| ASSERT_OK_AND_ASSIGN(auto zip_file_contents, |
| CreateFakeZip({ |
| {.name = "META-INF/content_credential.c2pa", |
| .content = "c2pa manifest 1"}, |
| {.name = "META-INF/content_credential.c2pa", |
| .content = "c2pa manifest 2"}, |
| })); |
| riegeli::StringReader<> input(zip_file_contents); |
| |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(input), |
| StatusIs(absl::StatusCode::kNotFound, |
| HasSubstr("Multiple manifest stores found"))); |
| } |
| |
| TEST(ZipExtractorTest, ExtractManifestStoreThrowsNotFoundErrorIfNoC2pa) { |
| ASSERT_OK_AND_ASSIGN(auto input, |
| credentio_testing::GetFileReader( |
| "c2pa/formats/zip/testing/asset_no_c2pa.zip")); |
| |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(*input), |
| StatusIs(absl::StatusCode::kNotFound, |
| HasSubstr("No manifest store found"))); |
| } |
| |
| TEST(ZipExtractorTest, ExtractManifestStoreLocation) { |
| ASSERT_OK_AND_ASSIGN(auto input, |
| credentio_testing::GetFileReader( |
| "c2pa/formats/zip/testing/asset_fake_c2pa.zip")); |
| ASSERT_OK_AND_ASSIGN(auto manifest_store_location, |
| ZipExtractor().ExtractManifestStoreLocation( |
| *input, {.requires_c2pa = true})); |
| |
| EXPECT_THAT(manifest_store_location, |
| Optional(AllOf(Field(&ByteRange::offset, Eq(201)), |
| Field(&ByteRange::length, Eq(14))))); |
| } |
| |
| TEST(ZipExtractorTest, |
| ExtractManifestStoreLocationReturnsNulloptIfNoC2paAndNotRequired) { |
| ASSERT_OK_AND_ASSIGN(auto input, |
| credentio_testing::GetFileReader( |
| "c2pa/formats/zip/testing/asset_no_c2pa.zip")); |
| ASSERT_OK_AND_ASSIGN(auto manifest_store_location, |
| ZipExtractor().ExtractManifestStoreLocation( |
| *input, {.requires_c2pa = false})); |
| |
| EXPECT_EQ(manifest_store_location, std::nullopt); |
| } |
| |
| TEST(ZipExtractorTest, |
| ExtractManifestStoreLocationReturnsErrorIfNoC2paButRequired) { |
| ASSERT_OK_AND_ASSIGN(auto input, |
| credentio_testing::GetFileReader( |
| "c2pa/formats/zip/testing/asset_no_c2pa.zip")); |
| EXPECT_THAT(ZipExtractor().ExtractManifestStoreLocation( |
| *input, {.requires_c2pa = true}), |
| StatusIs(absl::StatusCode::kNotFound, |
| HasSubstr("No manifest store found"))); |
| } |
| |
| TEST(ZipExtractorTest, ExtractBoxesReturnsUnimplemented) { |
| ASSERT_OK_AND_ASSIGN(auto zip, CreateFakeZip({})); |
| riegeli::StringReader<> input(zip); |
| |
| EXPECT_THAT(ZipExtractor().ExtractBoxes(input, {}), |
| StatusIs(absl::StatusCode::kUnimplemented, |
| HasSubstr("ZIP does not have a concept of boxes"))); |
| } |
| |
| TEST(ZipExtractorTest, IsManifestStore) { |
| EXPECT_TRUE(ZipExtractor().MightBeC2paManifestStore( |
| CreateStartOfManifestStorePayload())); |
| EXPECT_FALSE(ZipExtractor().MightBeC2paManifestStore("not a manifest store")); |
| } |
| |
| TEST(ZipExtractorTest, |
| ExtractManifestStoreThrowsErrorIfManifestStoreExceedsLimit) { |
| std::string large_content(17 * 1024 * 1024, 'a'); |
| ASSERT_OK_AND_ASSIGN(auto zip, |
| CreateFakeZip({ |
| {.name = "META-INF/content_credential.c2pa", |
| .content = std::move(large_content)}, |
| })); |
| riegeli::StringReader<> input(zip); |
| |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Manifest store size exceeds maximum limit"))); |
| } |
| |
| TEST(ZipExtractorTest, ExtractManifestStoreThrowsErrorIfCompressed) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 20); |
| write_le16(zip, 0); |
| write_le16(zip, 8); // LFH compression method = 8 (deflated) |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 20); |
| std::string filename = "META-INF/content_credential.c2pa"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 20); |
| write_le16(zip, 20); |
| write_le16(zip, 0); |
| write_le16(zip, 8); // CD compression method = 8 (deflated) |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 20); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Manifest store must not be compressed"))); |
| } |
| |
| TEST(ZipExtractorTest, |
| ExtractManifestStoreThrowsErrorIfUncompressedSizeExceedsLimit) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 20); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 17 * 1024 * 1024); // Uncompressed size = 17MB |
| std::string filename = "META-INF/content_credential.c2pa"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 20); |
| write_le16(zip, 20); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 17 * 1024 * 1024); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT(ZipExtractor().ExtractManifestStore(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Manifest store size exceeds maximum limit"))); |
| } |
| |
| } // namespace |
| } // namespace credentio |