blob: 0315081337d22be077dce5b5118de0a30021bd40 [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/pdf/embedder.h"
#include <cstdint>
#include <string>
#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/cord.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "crypto/algorithms.h"
#include "formats/pdf/constants.h"
#include "formats/pdf/extractor.h"
#include "formats/pdf/objects.h"
#include "formats/pdf/reader.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/generator.pb.h"
#include "riegeli/bytes/string_reader.h"
#include "riegeli/bytes/string_writer.h"
#include "testing/nonprod_generator.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
constexpr absl::string_view kTestPdfContent = R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
2 0 obj
123.456
endobj
3 0 obj
<</Type /Catalog>>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000041 00000 n
0000000064 00000 n
trailer
<</Root 3 0 R /Size 4>>
startxref
98
%%EOF
)";
constexpr absl::string_view kTestPdfContentWithDirectNamesDict = R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
2 0 obj
123.456
endobj
3 0 obj
<</Names <<>> /Type /Catalog>>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000041 00000 n
0000000064 00000 n
trailer
<</Root 3 0 R /Size 4>>
startxref
110
%%EOF
)";
constexpr absl::string_view kTestPdfContentWithFreeObjectEntries = R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
3 0 obj
123.456
endobj
5 0 obj
<</Type /Catalog>>
endobj
xref
0 6
0000000002 65535 f
0000000015 00000 n
0000000004 00000 f
0000000041 00000 n
0000000000 00000 f
0000000064 00000 n
trailer
<</Root 5 0 R /Size 6>>
startxref
98
%%EOF
)";
absl::StatusOr<std::string> CreateManifestStore() {
ABSL_ASSIGN_OR_RETURN(auto generator,
CreateNonProdManifestStoreGenerator({}));
GenerateManifestStoreRequest request;
DataHashAssertion* data_hash =
request.mutable_hard_binding()->mutable_data_hash();
data_hash->set_alg(absl::StrCat(HashAlgorithm::kSha256));
data_hash->set_hash("data_hash");
request.mutable_actions()->mutable_created();
ABSL_ASSIGN_OR_RETURN(auto response,
generator->GenerateManifestStore(request));
return std::string(response.manifest_store());
}
TEST(PdfRewriterTest, RewriteForEmbedding) {
riegeli::StringReader<> input(kTestPdfContent);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(PdfEmbedder().RewriteForEmbedding(
input, /*estimated_manifest_store_size=*/100, output),
IsOk());
ASSERT_TRUE(output.Close());
riegeli::StringReader<> output_reader(output_s);
EXPECT_THAT(PdfExtractor().ExtractManifestStore(output_reader),
IsOkAndHolds(std::string(100, '\0')));
}
TEST(PdfRewriterTest, RewriteForEmbeddingMovesDirectNamesDict) {
riegeli::StringReader<> input(kTestPdfContentWithDirectNamesDict);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(PdfEmbedder().RewriteForEmbedding(
input, /*estimated_manifest_store_size=*/100, output),
IsOk());
ASSERT_TRUE(output.Close());
riegeli::StringReader<> output_reader(output_s);
auto reader = PdfReader::Create(&output_reader);
ABSL_ASSERT_OK(reader);
auto trailer_dict = (*reader)->file_trailer_dictionary();
auto doc_catalog_dict_obj = (*reader)->GetObject(3, 0);
ABSL_ASSERT_OK(doc_catalog_dict_obj);
auto doc_catalog_dict =
Object::As<Object::Dictionary>(doc_catalog_dict_obj->object);
ABSL_ASSERT_OK(doc_catalog_dict);
// Verify that the catalog names dictionary is moved into an indirect object
// and referenced from the document catalog dictionary.
EXPECT_THAT(
(*reader)->GetObject(4, 0),
IsOkAndHolds(IndirectObject{
.object_number = 4,
.generation_number = 0,
.object = Object{
.value = Object::Dictionary{
.entries = {{"EmbeddedFiles",
Object{.value = Object::IndirectReference{
.object_number = 7,
.generation_number = 0}}}}}}}));
EXPECT_THAT(doc_catalog_dict->entries.at("Names"),
(Object{.value = Object::IndirectReference{
.object_number = 4, .generation_number = 0}}));
}
TEST(PdfRewriterTest, RewriteForEmbeddingRespectsFreeObjectEntries) {
riegeli::StringReader<> input(kTestPdfContentWithFreeObjectEntries);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(PdfEmbedder().RewriteForEmbedding(
input, /*estimated_manifest_store_size=*/100, output),
IsOk());
ASSERT_TRUE(output.Close());
riegeli::StringReader<> output_reader(output_s);
auto reader = PdfReader::Create(&output_reader);
ABSL_ASSERT_OK(reader);
EXPECT_THAT((*reader)->free_entries(),
ElementsAre(Object::IndirectReference{.object_number = 0,
.generation_number = 65535},
Object::IndirectReference{.object_number = 2,
.generation_number = 0},
Object::IndirectReference{.object_number = 4,
.generation_number = 0}));
}
TEST(PdfRewriterTest, RewriteForEmbeddingFailsIfC2paManifestAlreadyExists) {
// Prepare a C2PA-embeddable PDF.
riegeli::StringReader<> input(kTestPdfContent);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(PdfEmbedder().RewriteForEmbedding(
input, /*estimated_manifest_store_size=*/100, output),
IsOk());
ASSERT_TRUE(output.Close());
// Rewrite the PDF again, it should fail.
riegeli::StringReader<> output_reader(output_s);
std::string output_s2;
riegeli::StringWriter<> output2(&output_s2);
EXPECT_THAT(
PdfEmbedder().RewriteForEmbedding(
output_reader, /*estimated_manifest_store_size=*/100, output2),
StatusIs(absl::StatusCode::kAlreadyExists,
HasSubstr("C2PA manifest store already exists")));
}
TEST(PdfRewriterTest, Embed) {
auto manifest_store = CreateManifestStore();
ABSL_ASSERT_OK(manifest_store);
// Prepare a C2PA-embeddable PDF.
riegeli::StringReader<> input(kTestPdfContent);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
absl::StatusOr<int64_t> manifest_store_offset =
PdfEmbedder().RewriteForEmbedding(input, manifest_store->size(), output);
ABSL_ASSERT_OK(manifest_store_offset);
ASSERT_TRUE(output.Close());
// Embed the manifest store.
absl::Cord embeddable_asset(output_s);
EXPECT_THAT(PdfEmbedder().Embed(*manifest_store, *manifest_store_offset,
embeddable_asset),
IsOk());
riegeli::StringReader<> output_reader(embeddable_asset.Flatten());
EXPECT_THAT(PdfExtractor().ExtractManifestStore(output_reader),
IsOkAndHolds(*manifest_store));
}
TEST(PdfRewriterTest, RewriteForEmbeddingCorrectLayout) {
riegeli::StringReader<> input(kTestPdfContent);
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(PdfEmbedder().RewriteForEmbedding(
input, /*estimated_manifest_store_size=*/100, output),
IsOk());
ASSERT_TRUE(output.Close());
riegeli::StringReader<> output_reader(output_s);
auto reader = PdfReader::Create(&output_reader);
ABSL_ASSERT_OK(reader);
// The catalog should have /AF pointing to the file spec.
auto doc_catalog_dict_obj = (*reader)->GetObject(3, 0);
ABSL_ASSERT_OK(doc_catalog_dict_obj);
auto doc_catalog_dict =
Object::As<Object::Dictionary>(doc_catalog_dict_obj->object);
ABSL_ASSERT_OK(doc_catalog_dict);
auto af_array =
FindObjectInDictionary<Object::Array>(*doc_catalog_dict, kPdfAfKey);
ABSL_ASSERT_OK(af_array);
ASSERT_EQ(af_array->objects.size(), 1);
auto file_spec_ref =
Object::As<Object::IndirectReference>(af_array->objects[0]);
ABSL_ASSERT_OK(file_spec_ref);
// Inspect the file spec object.
auto file_spec_obj = (*reader)->GetObject(file_spec_ref->object_number,
file_spec_ref->generation_number);
ABSL_ASSERT_OK(file_spec_obj);
auto file_spec_dict = Object::As<Object::Dictionary>(file_spec_obj->object);
ABSL_ASSERT_OK(file_spec_dict);
// Subtype should NOT be in file spec dictionary.
EXPECT_THAT(
FindObjectInDictionary<Object::Name>(*file_spec_dict, kPdfSubtypeKey),
StatusIs(absl::StatusCode::kNotFound));
// AFRelationship should be C2PA_Manifest.
EXPECT_THAT(FindObjectInDictionary<Object::Name>(*file_spec_dict,
kPdfAfRelationshipKey),
IsOkAndHolds(Object::Name{
.value = std::string(kPdfC2paManifestAfRelationship)}));
// Get the embedded file stream ref.
auto ef_dict =
FindObjectInDictionary<Object::Dictionary>(*file_spec_dict, kPdfEfKey);
ABSL_ASSERT_OK(ef_dict);
auto stream_ref =
FindObjectInDictionary<Object::IndirectReference>(*ef_dict, kPdfFKey);
ABSL_ASSERT_OK(stream_ref);
// Inspect the stream object.
auto stream_obj = (*reader)->GetObject(stream_ref->object_number,
stream_ref->generation_number);
ABSL_ASSERT_OK(stream_obj);
ASSERT_TRUE(stream_obj->stream_dictionary.has_value());
const auto& stream_dict = *stream_obj->stream_dictionary;
// Subtype should be in stream dictionary.
EXPECT_THAT(FindObjectInDictionary<Object::Name>(stream_dict, kPdfSubtypeKey),
IsOkAndHolds(Object::Name{
.value = std::string(kPdfC2paManifestMimeType)}));
// Params should be in stream dictionary.
auto params_dict =
FindObjectInDictionary<Object::Dictionary>(stream_dict, kPdfParamsKey);
ABSL_ASSERT_OK(params_dict);
// Params should contain Size.
EXPECT_THAT(
FindObjectInDictionary<Object::Integer>(*params_dict, kPdfSizeKey),
IsOkAndHolds(Object::Integer{.value = 100}));
// Params should contain ModDate.
auto mod_date_obj = FindObjectInDictionary<Object::LiteralString>(
*params_dict, kPdfModDateKey);
ABSL_ASSERT_OK(mod_date_obj);
EXPECT_THAT(mod_date_obj->raw_value,
testing::MatchesRegex("D:[0-9]{14}[+]00'00'"));
}
} // namespace
} // namespace credentio