blob: 3634cd9b808f394b279be04eb7d38d0a764ea90b [file] [edit]
// 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/embedder.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "formats/byte_range.h"
#include "formats/zip/constants.h"
#include "formats/zip/extractor.h"
#include "formats/zip/reader.h"
#include "formats/zip/zlib_util.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
#include "riegeli/bytes/string_writer.h"
#include "testing/test_file_utils.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
absl::StatusOr<std::vector<ZipReader::FileEntry>> GetFileEntries(
riegeli::Reader& input) {
ABSL_ASSIGN_OR_RETURN(auto reader, ZipReader::Create(&input));
std::vector<ZipReader::FileEntry> file_entries;
while (reader->HasNext()) {
ABSL_ASSIGN_OR_RETURN(ZipReader::FileEntry entry, reader->Next());
file_entries.push_back(entry);
}
return file_entries;
}
absl::StatusOr<std::string> GetContentTypesStream(riegeli::Reader& input) {
ABSL_ASSIGN_OR_RETURN(auto reader, ZipReader::Create(&input));
while (reader->HasNext()) {
ABSL_ASSIGN_OR_RETURN(ZipReader::FileEntry entry, reader->Next());
if (entry.file_name == kOoxmlContentTypesFileName) {
if (!input.Seek(entry.file_range.offset)) {
return input.StatusOrAnnotate(absl::DataLossError("failed to seek"));
}
std::string content;
if (!input.Read(entry.file_range.length, content)) {
return input.StatusOrAnnotate(absl::DataLossError("failed to read"));
}
ABSL_ASSIGN_OR_RETURN(std::string uncompressed, Uncompress(content));
return uncompressed;
}
}
return absl::NotFoundError("Content types stream not found.");
}
class ZipEmbedderTest : public testing::Test {
protected:
void InitWithZipFile(absl::string_view file_name,
int64_t manifest_store_size) {
auto source = credentio_testing::GetFileReader(file_name);
ABSL_ASSERT_OK(source);
source_ = *std::move(source);
auto file_entries = GetFileEntries(*source_);
ABSL_ASSERT_OK(file_entries);
file_entries_ = *std::move(file_entries);
destination_ = std::make_unique<riegeli::StringWriter<>>(&output_s_);
manifest_store_size_ = manifest_store_size;
}
std::string zip_file_contents_;
std::unique_ptr<riegeli::Reader> source_;
std::vector<ZipReader::FileEntry> file_entries_;
std::string output_s_;
std::unique_ptr<riegeli::StringWriter<>> destination_;
int64_t manifest_store_size_;
};
TEST_F(ZipEmbedderTest, CopyAndAllocateFailsIfManifestStoreAlreadyExists) {
InitWithZipFile(
"credentio/formats/zip/testing/"
"asset_fake_c2pa.zip",
/*manifest_store_size=*/100);
EXPECT_THAT(
ZipEmbedder().CopyAndAllocate(source_.get(), file_entries_,
manifest_store_size_, destination_.get()),
StatusIs(absl::StatusCode::kAlreadyExists,
HasSubstr("Manifest store already exists.")));
}
TEST_F(ZipEmbedderTest, CopyAndAllocateFailsIfNoContentTypesStream) {
InitWithZipFile("credentio/formats/zip/testing/asset_no_c2pa.zip",
/*manifest_store_size=*/100);
EXPECT_THAT(
ZipEmbedder().CopyAndAllocate(source_.get(), file_entries_,
manifest_store_size_, destination_.get()),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("Content types stream not found.")));
}
TEST_F(ZipEmbedderTest, CopyAndAllocate) {
InitWithZipFile("credentio/formats/zip/testing/no_c2pa_032_asset.pptx",
/*manifest_store_size=*/100);
auto copy_and_allocate_result = ZipEmbedder().CopyAndAllocate(
source_.get(), file_entries_, manifest_store_size_, destination_.get());
ABSL_ASSERT_OK(copy_and_allocate_result);
ASSERT_TRUE(destination_->Close());
riegeli::StringReader<absl::string_view> output_reader(output_s_);
EXPECT_THAT(ZipExtractor().ExtractManifestStore(output_reader),
IsOkAndHolds(std::string(100, '\0')));
ASSERT_TRUE(output_reader.Seek(0));
EXPECT_THAT(ZipExtractor().ExtractManifestStoreLocation(output_reader, {}),
IsOkAndHolds(ByteRange{
.offset = copy_and_allocate_result->manifest_store_offset,
.length = 100,
}));
ASSERT_TRUE(output_reader.Seek(0));
auto content_types_stream = GetContentTypesStream(output_reader);
ABSL_ASSERT_OK(content_types_stream);
EXPECT_THAT(
*content_types_stream,
HasSubstr(
R"(<Override ContentType="application/c2pa" PartName="/META-INF/content_credential.c2pa"/>)"));
}
TEST_F(ZipEmbedderTest, Embed) {
std::string manifest_store = "c2pa manifest store";
InitWithZipFile("credentio/formats/zip/testing/no_c2pa_032_asset.pptx",
manifest_store.size());
auto copy_and_allocate_result = ZipEmbedder().CopyAndAllocate(
source_.get(), file_entries_, manifest_store_size_, destination_.get());
ABSL_ASSERT_OK(copy_and_allocate_result);
EXPECT_THAT(ZipEmbedder().Embed(manifest_store, *copy_and_allocate_result,
destination_.get()),
IsOk());
ASSERT_TRUE(destination_->Close());
riegeli::StringReader output_reader(&output_s_);
EXPECT_THAT(ZipExtractor().ExtractManifestStore(output_reader),
IsOkAndHolds(manifest_store));
}
TEST_F(ZipEmbedderTest, CopyAndAllocateFailsWithDuplicateEntries) {
InitWithZipFile("credentio/formats/zip/testing/no_c2pa_032_asset.pptx", 100);
ASSERT_GE(file_entries_.size(), 1);
std::vector<ZipReader::FileEntry> duplicate_entries = file_entries_;
duplicate_entries.push_back(file_entries_[0]);
EXPECT_THAT(
ZipEmbedder().CopyAndAllocate(source_.get(), duplicate_entries,
manifest_store_size_, destination_.get()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Overlapping or duplicate ZIP entries detected")));
}
TEST_F(ZipEmbedderTest, CopyAndAllocatePreservesExtraFieldAndCommentData) {
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;
std::string file1_name = "[Content_Types].xml";
std::string file1_data = "<Types></Types>";
std::string file1_extra = "EX1";
std::string file1_cmt = "CM1";
std::string file2_name = "doc.xml";
std::string file2_data = "<Document/>";
std::string file2_extra = "EXTRA2";
std::string file2_cmt = "CMT2";
// LFH 1
uint32_t lfh1_offset = zip.size();
zip.append("PK\x03\x04", 4);
write_le16(zip, 10);
write_le16(zip, 0x0008); // Has data descriptor
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0);
write_le32(zip, 0);
write_le32(zip, 0);
write_le16(zip, file1_name.size());
write_le16(zip, file1_extra.size());
zip.append(file1_name);
zip.append(file1_extra);
zip.append(file1_data);
// Data descriptor with signature
zip.append("PK\x07\x08", 4);
write_le32(zip, 0x12345678);
write_le32(zip, file1_data.size());
write_le32(zip, file1_data.size());
// LFH 2
uint32_t lfh2_offset = zip.size();
zip.append("PK\x03\x04", 4);
write_le16(zip, 10);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0x87654321);
write_le32(zip, file2_data.size());
write_le32(zip, file2_data.size());
write_le16(zip, file2_name.size());
write_le16(zip, file2_extra.size());
zip.append(file2_name);
zip.append(file2_extra);
zip.append(file2_data);
// CD Start
uint32_t cd_offset = zip.size();
// CD 1
zip.append("PK\x01\x02", 4);
write_le16(zip, 20);
write_le16(zip, 10);
write_le16(zip, 0x0008);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0x12345678);
write_le32(zip, file1_data.size());
write_le32(zip, file1_data.size());
write_le16(zip, file1_name.size());
write_le16(zip, file1_extra.size());
write_le16(zip, file1_cmt.size());
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0);
write_le32(zip, lfh1_offset);
zip.append(file1_name);
zip.append(file1_extra);
zip.append(file1_cmt);
// CD 2
zip.append("PK\x01\x02", 4);
write_le16(zip, 20);
write_le16(zip, 10);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0x87654321);
write_le32(zip, file2_data.size());
write_le32(zip, file2_data.size());
write_le16(zip, file2_name.size());
write_le16(zip, file2_extra.size());
write_le16(zip, file2_cmt.size());
write_le16(zip, 0);
write_le16(zip, 0);
write_le32(zip, 0);
write_le32(zip, lfh2_offset);
zip.append(file2_name);
zip.append(file2_extra);
zip.append(file2_cmt);
uint32_t cd_size = zip.size() - cd_offset;
// EOCD
zip.append("PK\x05\x06", 4);
write_le16(zip, 0);
write_le16(zip, 0);
write_le16(zip, 2);
write_le16(zip, 2);
write_le32(zip, cd_size);
write_le32(zip, cd_offset);
write_le16(zip, 0);
riegeli::StringReader source_reader(zip);
auto source_zip_reader = ZipReader::Create(&source_reader);
ABSL_ASSERT_OK(source_zip_reader);
std::vector<ZipReader::FileEntry> file_entries;
while ((*source_zip_reader)->HasNext()) {
auto entry = (*source_zip_reader)->Next();
ABSL_ASSERT_OK(entry);
file_entries.push_back(*entry);
}
ASSERT_EQ(file_entries.size(), 2);
std::string output_s;
riegeli::StringWriter<> destination(&output_s);
auto copy_and_allocate_result = ZipEmbedder().CopyAndAllocate(
&source_reader, file_entries, /*manifest_store_size=*/100, &destination);
ABSL_ASSERT_OK(copy_and_allocate_result);
(void)*copy_and_allocate_result;
ASSERT_TRUE(destination.Close());
riegeli::StringReader output_reader(output_s);
auto out_zip_reader = ZipReader::Create(&output_reader);
ABSL_ASSERT_OK(out_zip_reader);
std::vector<ZipReader::FileEntry> out_file_entries;
while ((*out_zip_reader)->HasNext()) {
auto entry = (*out_zip_reader)->Next();
ABSL_ASSERT_OK(entry);
out_file_entries.push_back(*entry);
}
// Expect 3 entries in output: doc.xml, [Content_Types].xml, and manifest
// store
EXPECT_EQ(out_file_entries.size(), 3);
// Check doc.xml retains extra_field_length and file_comment_length
const ZipReader::FileEntry* doc_entry = nullptr;
for (const auto& entry : out_file_entries) {
if (entry.file_name == file2_name) {
doc_entry = &entry;
}
}
ASSERT_NE(doc_entry, nullptr);
EXPECT_EQ(doc_entry->extra_field_length, file2_extra.size());
EXPECT_EQ(doc_entry->file_comment_length, file2_cmt.size());
// Check extra field data and comment data are present in output string
EXPECT_THAT(output_s, HasSubstr(file1_extra));
EXPECT_THAT(output_s, HasSubstr(file1_cmt));
EXPECT_THAT(output_s, HasSubstr(file2_extra));
EXPECT_THAT(output_s, HasSubstr(file2_cmt));
}
} // namespace
} // namespace credentio