blob: 60ded2f6e75efc025714b372c851fec9d0607298 [file]
// 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/extractor.h"
#include <string>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/string_view.h"
#include "formats/byte_range.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
using ::testing::Optional;
#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
constexpr absl::string_view kTestPdfContent =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNoC2paSubtype =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000155 00000 n \n"
"0000000238 00000 n \n"
"0000000298 00000 n \n"
"0000000338 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"397\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNoEmbeddedFilesRef =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(not_a_c2pa_manifest.txt) 1 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNamesNotDict =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names [1 0 R]>>\n"
"endobj\n"
"xref\n"
"0 5\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"trailer\n"
"<</Root 4 0 R>>\n"
"startxref\n"
"328\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentEmbeddedFilesDirect =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names <</EmbeddedFiles <</Names "
"[(content_credential.c2pa) 3 0 R]>> >> >>\n"
"endobj\n"
"xref\n"
"0 5\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"trailer\n"
"<</Root 4 0 R>>\n"
"startxref\n"
"386\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNoAfRelationship =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentDifferentAfRelationship =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /Other /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNonIndirectAf =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [123 ] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentInvalidEmbeddedFilesType =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 12345>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNamesDictNotDict =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"12345 \n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNoEmbeddedFiles =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</OtherFiles___ 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 5 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
constexpr absl::string_view kTestPdfContentNamesObjectNotFound =
"%PDF-1.7\n"
"%%%%%\n"
"1 0 obj\n"
"<</Length 11>>\n"
"stream\n"
"test_stream\n"
"endstream\n"
"endobj\n"
"2 0 obj\n"
"<</Length 33 /Subtype /application#2fc2pa>>\n"
"stream\n"
"test_c2pa_manifest_raw_bytestream\n"
"endstream\n"
"endobj\n"
"3 0 obj\n"
"<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest /Type /FileSpec>>\n"
"endobj\n"
"4 0 obj\n"
"<</Names [(content_credential.c2pa) 3 0 R]>>\n"
"endobj\n"
"5 0 obj\n"
"<</EmbeddedFiles 4 0 R>>\n"
"endobj\n"
"6 0 obj\n"
"<</AF [3 0 R] /Type /Catalog /Names 9 0 R>>\n"
"endobj\n"
"xref\n"
"0 7\n"
"0000000000 65535 f \n"
"0000000015 00000 n \n"
"0000000074 00000 n \n"
"0000000184 00000 n \n"
"0000000267 00000 n \n"
"0000000327 00000 n \n"
"0000000367 00000 n \n"
"trailer\n"
"<</Root 6 0 R>>\n"
"startxref\n"
"426\n"
"%%EOF";
TEST(PdfExtractorTest, ExtractManifestStoreV22EmbeddedFilesDirect) {
riegeli::StringReader<> input(kTestPdfContentEmbeddedFilesDirect);
PdfExtractor extractor;
ASSERT_OK_AND_ASSIGN(auto manifest_store,
extractor.ExtractManifestStore(input));
EXPECT_THAT(manifest_store, "test_c2pa_manifest_raw_bytestream");
}
TEST(PdfExtractorTest, ExtractManifestStoreV22) {
riegeli::StringReader<> input(kTestPdfContent);
PdfExtractor extractor;
ASSERT_OK_AND_ASSIGN(auto manifest_store,
extractor.ExtractManifestStore(input));
EXPECT_THAT(manifest_store, "test_c2pa_manifest_raw_bytestream");
}
TEST(PdfExtractorTest, ExtractManifestStoreNoSubtype) {
riegeli::StringReader<> input(kTestPdfContentNoC2paSubtype);
PdfExtractor extractor;
ASSERT_OK_AND_ASSIGN(auto manifest_store,
extractor.ExtractManifestStore(input));
EXPECT_THAT(manifest_store, "test_c2pa_manifest_raw_bytestream");
}
TEST(PdfExtractorTest,
ExtractManifestStoreThrowNotFoundErrorIfNoEmbeddedFilesRef) {
riegeli::StringReader<> input(kTestPdfContentNoEmbeddedFilesRef);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound));
}
TEST(PdfExtractorTest, ExtractManifestStoreLocation) {
riegeli::StringReader<> input(kTestPdfContent);
PdfExtractor extractor;
ASSERT_OK_AND_ASSIGN(
auto manifest_store_location,
extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true}));
EXPECT_THAT(manifest_store_location,
Optional(ByteRange{.offset = 74, .length = 109}));
}
TEST(PdfExtractorTest, ExtractManifestStoreLocationNoSubtype) {
riegeli::StringReader<> input(kTestPdfContentNoC2paSubtype);
PdfExtractor extractor;
ASSERT_OK_AND_ASSIGN(
auto manifest_store_location,
extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true}));
EXPECT_THAT(manifest_store_location,
Optional(ByteRange{.offset = 74, .length = 80}));
}
TEST(PdfExtractorTest, ExtractManifestStoreFailsWithInvalidNamesType) {
riegeli::StringReader<> input(kTestPdfContentNamesNotDict);
PdfExtractor extractor;
EXPECT_THAT(
extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kInvalidArgument,
::testing::AllOf(HasSubstr("Unexpected object type found."),
HasSubstr("credentio::Object::"))));
}
TEST(PdfExtractorTest, ExtractBoxesUnimplemented) {
riegeli::StringReader<> input("test_content");
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractBoxes(input, {.requires_c2pa = true}),
StatusIs(absl::StatusCode::kUnimplemented));
}
TEST(PdfExtractorTest,
ExtractManifestStoreThrowsNotFoundErrorIfNoAfRelationship) {
riegeli::StringReader<> input(kTestPdfContentNoAfRelationship);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No manifest store found.")));
}
TEST(PdfExtractorTest,
ExtractManifestStoreThrowsNotFoundErrorIfDifferentAfRelationship) {
riegeli::StringReader<> input(kTestPdfContentDifferentAfRelationship);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No manifest store found.")));
}
TEST(PdfExtractorTest, ExtractManifestStoreThrowsNotFoundErrorIfNonIndirectAf) {
riegeli::StringReader<> input(kTestPdfContentNonIndirectAf);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No manifest store found.")));
}
TEST(PdfExtractorTest, ExtractManifestStoreFailsWithInvalidEmbeddedFilesType) {
riegeli::StringReader<> input(kTestPdfContentInvalidEmbeddedFilesType);
PdfExtractor extractor;
EXPECT_THAT(
extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kInvalidArgument,
::testing::AllOf(HasSubstr("Unexpected object type found."),
HasSubstr("credentio::Object::"))));
}
TEST(PdfExtractorTest, ExtractManifestStoreFailsIfNamesObjectNotDict) {
riegeli::StringReader<> input(kTestPdfContentNamesDictNotDict);
PdfExtractor extractor;
EXPECT_THAT(
extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Object is not a credentio::Object::Dictionary")));
}
TEST(PdfExtractorTest,
ExtractManifestStoreThrowsNotFoundErrorIfNoEmbeddedFiles) {
riegeli::StringReader<> input(kTestPdfContentNoEmbeddedFiles);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No '/EmbeddedFiles' dictionary found.")));
}
TEST(PdfExtractorTest, ExtractManifestStoreFailsIfNamesObjectNotFound) {
riegeli::StringReader<> input(kTestPdfContentNamesObjectNotFound);
PdfExtractor extractor;
EXPECT_THAT(extractor.ExtractManifestStore(input),
StatusIs(absl::StatusCode::kNotFound,
HasSubstr("No offset found for object number: 9")));
}
} // namespace
} // namespace credentio