| // 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 <cstddef> | |
| #include <cstdio> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/absl_check.h" | |
| #include "absl/status/status.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 "constants/labels.h" | |
| #include "formats/byte_range.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/box.h" | |
| #include "jumbf/box_builder.h" | |
| #include "jumbf/constants.h" | |
| #include "jumbf/encode_params.h" | |
| #include "jumbf/parse.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::credentio::kManifestStoreLabel; | |
| using ::credentio::kManifestStoreUuid; | |
| using ::credentio::kStandardManifestUuid; | |
| using ::jumbf::DescriptionBoxOptions; | |
| using ::jumbf::kCborBoxType; | |
| using ::jumbf::SuperBoxBuilder; | |
| using ::testing::HasSubstr; | |
| using ::testing::Optional; | |
| 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; | |
| EXPECT_THAT(extractor.ExtractManifestStore(input), | |
| IsOkAndHolds("test_c2pa_manifest_raw_bytestream")); | |
| } | |
| TEST(PdfExtractorTest, ExtractManifestStoreV22) { | |
| riegeli::StringReader<> input(kTestPdfContent); | |
| PdfExtractor extractor; | |
| EXPECT_THAT(extractor.ExtractManifestStore(input), | |
| IsOkAndHolds("test_c2pa_manifest_raw_bytestream")); | |
| } | |
| TEST(PdfExtractorTest, ExtractManifestStoreNoSubtype) { | |
| riegeli::StringReader<> input(kTestPdfContentNoC2paSubtype); | |
| PdfExtractor extractor; | |
| EXPECT_THAT(extractor.ExtractManifestStore(input), | |
| IsOkAndHolds("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; | |
| EXPECT_THAT( | |
| extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true}), | |
| IsOkAndHolds(Optional(ByteRange{.offset = 74, .length = 109}))); | |
| } | |
| TEST(PdfExtractorTest, ExtractManifestStoreLocationNoSubtype) { | |
| riegeli::StringReader<> input(kTestPdfContentNoC2paSubtype); | |
| PdfExtractor extractor; | |
| EXPECT_THAT( | |
| extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true}), | |
| IsOkAndHolds(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::")))); | |
| } | |
| std::string MakeValidPdfWithMultipleManifests(absl::string_view manifest1, | |
| absl::string_view manifest2) { | |
| std::vector<std::string> objs; | |
| objs.push_back( | |
| "1 0 obj\n<</Length 11>>\nstream\ntest_stream\nendstream\nendobj\n"); | |
| objs.push_back(absl::StrCat("2 0 obj\n<</Length ", manifest1.size(), | |
| " /Subtype /application#2fc2pa>>\nstream\n", | |
| manifest1, "\nendstream\nendobj\n")); | |
| objs.push_back( | |
| "3 0 obj\n<</EF <</F 2 0 R>> /AFRelationship /C2PA_Manifest " | |
| "/Type /FileSpec>>\nendobj\n"); | |
| objs.push_back(absl::StrCat("4 0 obj\n<</Length ", manifest2.size(), | |
| " /Subtype /application#2fc2pa>>\nstream\n", | |
| manifest2, "\nendstream\nendobj\n")); | |
| objs.push_back( | |
| "5 0 obj\n<</EF <</F 4 0 R>> /AFRelationship /C2PA_Manifest " | |
| "/Type /FileSpec>>\nendobj\n"); | |
| objs.push_back( | |
| "6 0 obj\n<</Names [(manifest1.c2pa) 3 0 R (manifest2.c2pa) 5 0 " | |
| "R]>>\nendobj\n"); | |
| objs.push_back("7 0 obj\n<</EmbeddedFiles 6 0 R>>\nendobj\n"); | |
| objs.push_back( | |
| "8 0 obj\n<</AF [3 0 R 5 0 R] /Type /Catalog /Names 7 0 R>>\nendobj\n"); | |
| std::string pdf = "%PDF-1.7\n"; | |
| std::vector<size_t> offsets; | |
| for (const auto& obj : objs) { | |
| offsets.push_back(pdf.size()); | |
| pdf += obj; | |
| } | |
| size_t xref_pos = pdf.size(); | |
| pdf += "xref\n0 9\n0000000000 65535 f \n"; | |
| for (size_t offset : offsets) { | |
| char buf[32]; | |
| snprintf(buf, sizeof(buf), "%010zu 00000 n \n", offset); | |
| pdf += buf; | |
| } | |
| absl::StrAppend(&pdf, "trailer\n<</Root 8 0 R>>\nstartxref\n", xref_pos, | |
| "\n%%EOF"); | |
| return pdf; | |
| } | |
| TEST(PdfExtractorTest, ExtractManifestStoreSucceedsWithMultipleManifestsInAf) { | |
| SuperBoxBuilder manifest_builder1( | |
| kStandardManifestUuid, | |
| DescriptionBoxOptions{.requestable_iff_label = true, | |
| .label = absl::Cord("manifest_1")}); | |
| ABSL_CHECK_OK( | |
| manifest_builder1.AddContent(kCborBoxType, absl::Cord("data_1"))); | |
| absl::StatusOr<jumbf::BuiltSuperBox> manifest_box1 = | |
| std::move(manifest_builder1).Finalize(); | |
| ABSL_ASSERT_OK(manifest_box1); | |
| SuperBoxBuilder store_builder1( | |
| kManifestStoreUuid, | |
| DescriptionBoxOptions{.requestable_iff_label = true, | |
| .label = absl::Cord(kManifestStoreLabel)}); | |
| ABSL_CHECK_OK(store_builder1.AddChild(*std::move(manifest_box1))); | |
| absl::StatusOr<jumbf::BuiltSuperBox> store_box1 = | |
| std::move(store_builder1).Finalize(); | |
| ABSL_ASSERT_OK(store_box1); | |
| std::string store1 = std::string(std::move(*store_box1).AsCord()); | |
| SuperBoxBuilder manifest_builder2( | |
| kStandardManifestUuid, | |
| DescriptionBoxOptions{.requestable_iff_label = true, | |
| .label = absl::Cord("manifest_2")}); | |
| ABSL_CHECK_OK( | |
| manifest_builder2.AddContent(kCborBoxType, absl::Cord("data_2"))); | |
| absl::StatusOr<jumbf::BuiltSuperBox> manifest_box2 = | |
| std::move(manifest_builder2).Finalize(); | |
| ABSL_ASSERT_OK(manifest_box2); | |
| SuperBoxBuilder store_builder2( | |
| kManifestStoreUuid, | |
| DescriptionBoxOptions{.requestable_iff_label = true, | |
| .label = absl::Cord(kManifestStoreLabel)}); | |
| ABSL_CHECK_OK(store_builder2.AddChild(*std::move(manifest_box2))); | |
| absl::StatusOr<jumbf::BuiltSuperBox> store_box2 = | |
| std::move(store_builder2).Finalize(); | |
| ABSL_ASSERT_OK(store_box2); | |
| std::string store2 = std::string(std::move(*store_box2).AsCord()); | |
| std::string pdf = MakeValidPdfWithMultipleManifests(store1, store2); | |
| riegeli::StringReader<> input(pdf); | |
| PdfExtractor extractor; | |
| absl::StatusOr<std::string> merged = extractor.ExtractManifestStore(input); | |
| ABSL_ASSERT_OK(merged); | |
| absl::string_view view = *merged; | |
| absl::StatusOr<jumbf::SuperBox> superbox = | |
| jumbf::ConsumeSuperBox(&view, /*recursion_limit=*/9); | |
| ABSL_ASSERT_OK(superbox); | |
| ASSERT_EQ(superbox->contents.size(), 2); | |
| EXPECT_EQ(superbox->contents.back().Get<jumbf::SuperBox>().description.label, | |
| "manifest_2"); | |
| // Verify location points to the active (second) manifest store stream. | |
| absl::StatusOr<std::optional<ByteRange>> manifest_store_location = | |
| extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true}); | |
| ABSL_ASSERT_OK(manifest_store_location); | |
| ASSERT_TRUE(manifest_store_location->has_value()); | |
| absl::string_view active_stream = absl::string_view(pdf).substr( | |
| (*manifest_store_location)->offset, (*manifest_store_location)->length); | |
| EXPECT_THAT(active_stream, HasSubstr("4 0 obj")); | |
| EXPECT_THAT(active_stream, HasSubstr(store2)); | |
| } | |
| 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 |