blob: 957eff1e63617b3545eb6ea4d4952da0c048e79d [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/object_writer.h"
#include <algorithm>
#include <cstddef>
#include <ostream>
#include <string>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "formats/pdf/objects.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_writer.h"
#include "riegeli/bytes/writer.h"
namespace credentio {
namespace {
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
class FailingWriter : public riegeli::Writer {
public:
explicit FailingWriter(absl::Status status) { Fail(status); }
bool PushSlow(size_t min_length, size_t recommended_length) override {
return false;
}
};
class StepFailingWriter : public riegeli::Writer {
public:
explicit StepFailingWriter(int fail_after_bytes, absl::Status status)
: fail_after_bytes_(fail_after_bytes), status_(status) {
set_buffer(buffer_, 1);
}
bool PushSlow(size_t min_length, size_t recommended_length) override {
if (fail_after_bytes_ <= 0) {
Fail(status_);
return false;
}
size_t write_len =
std::min(min_length, static_cast<size_t>(fail_after_bytes_));
fail_after_bytes_ -= write_len;
if (fail_after_bytes_ <= 0) {
Fail(status_);
return false;
}
set_buffer(buffer_, 1);
return true;
}
private:
int fail_after_bytes_;
absl::Status status_;
char buffer_[1];
};
TEST(WriteObjectTest, WriteBoolean) {
Object obj = {.value = Object::Boolean{.value = true}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "true");
}
TEST(WriteObjectTest, WriteInteger) {
Object obj = {.value = Object::Integer{.value = 123}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "123");
}
TEST(WriteObjectTest, WriteIntegerPreservesNegativeSign) {
Object obj = {.value = Object::Integer{.value = -123}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "-123");
}
TEST(WriteObjectTest, WriteRealNumber) {
Object obj = {.value = Object::RealNumber{.raw_value = "1.23"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "1.23");
}
TEST(WriteObjectTest, WriteLiteralString) {
Object obj = {.value = Object::LiteralString{.raw_value = "abc"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "(abc)");
}
TEST(WriteObjectTest, WriteHexadecimalString) {
Object obj = {.value = Object::HexadecimalString{.raw_value = "4E6F76"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "<4E6F76>");
}
TEST(WriteObjectTest, WriteName) {
Object obj = {.value = Object::Name{.value = "Catalog"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "/Catalog");
}
TEST(WriteObjectTest, WriteNameWithEscapedCharacters) {
Object obj = {.value = Object::Name{.value = "/\x20\x80"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "/#2f#20#80");
}
TEST(WriteObjectTest, WriteArray) {
Object obj = {
.value = Object::Array{
.objects = {
Object{.value = Object::LiteralString{.raw_value = "abc"}},
Object{.value = Object::LiteralString{.raw_value = "def"}}}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "[(abc) (def)]");
}
TEST(WriteObjectTest, WriteDictionary) {
Object obj = {
.value = Object::Dictionary{
.entries = {
{"key_1", Object{.value = Object::Name{.value = "name"}}},
{"key_2", Object{.value = Object::Integer{.value = 123}}}}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s.substr(0, 2), "<<");
// The order of the entries is non-deterministic, so we check the substrings
// instead of the entire string.
EXPECT_THAT(output_s, HasSubstr("/key_1 /name"));
EXPECT_THAT(output_s, HasSubstr("/key_2 123"));
EXPECT_EQ(output_s.substr(output_s.size() - 2, 2), ">>");
}
TEST(WriteObjectTest, WriteStreamFailure) {
Object obj = {.value = Object::Stream{.raw_value = "1234567890"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(
WriteObject(obj, output),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Stream must be written as an indirect "
"object. Call WriteIndirectObject() instead.")));
}
TEST(WriteObjectTest, WriteNull) {
Object obj = {.value = Object::Null{}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "null");
}
TEST(WriteIndirectObjectTest, WriteStream) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length",
Object{.value = Object::Integer{.value = 10}}}}},
.object = {.value = Object::Stream{.raw_value = "1234567890"}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteIndirectObject(indirect_obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, R"(1 2 obj
<</Length 10>>
stream
1234567890
endstream
endobj
)");
}
TEST(WriteIndirectObjectTest, WriteName) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.object = {.value = Object::Name{.value = "Catalog"}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteIndirectObject(indirect_obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, R"(1 2 obj
/Catalog
endobj
)");
}
TEST(ComputeIndirectStreamHeaderLengthTest, ComputeHeaderLengthCorrectly) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length",
Object{.value = Object::Integer{.value = 10}}}}},
.object = {.value = Object::Stream{.raw_value = "1234567890"}}};
auto header_len = ComputeIndirectStreamHeaderByteSize(indirect_obj);
ABSL_ASSERT_OK(header_len);
EXPECT_EQ(*header_len, 30);
}
TEST(ComputeIndirectStreamHeaderLengthTest, FailsForNonStreamObject) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.object = {.value = Object::LiteralString{.raw_value = "abc"}}};
EXPECT_THAT(
ComputeIndirectStreamHeaderByteSize(indirect_obj),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("The object is not an indirect stream object")));
}
TEST(ComputeIndirectStreamHeaderLengthTest,
HandlesStreamSubstringInDictionaryField) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Title",
Object{.value =
Object::LiteralString{
.raw_value = "my stream\n title"}}}}},
.object = {.value = Object::Stream{.raw_value = "1234567890"}}};
auto header_len = ComputeIndirectStreamHeaderByteSize(indirect_obj);
ABSL_ASSERT_OK(header_len);
EXPECT_EQ(*header_len, 45);
}
TEST(WriteObjectTest, InjectionBlocked) {
// 1. LiteralString Injection is escaped safely
{
Object obj = {.value = Object::LiteralString{
.raw_value = "payload) /Injected /Dictionary ("}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "(payload\\) /Injected /Dictionary \\()");
}
// 2. HexadecimalString Injection is rejected as invalid hex
{
Object obj = {.value = Object::HexadecimalString{
.raw_value = "4E6F76> /Injected /Dictionary <"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(WriteObject(obj, output),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Invalid hexadecimal string")));
}
// 3. RealNumber Injection is rejected as invalid real number
{
Object obj = {.value = Object::RealNumber{.raw_value = "1.23 /Injected"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
EXPECT_THAT(WriteObject(obj, output),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Invalid real number")));
}
// 4. Name Injection is escaped safely
{
Object obj = {.value = Object::Name{.value = "MyName)(InjectedString)"}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteObject(obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, "/MyName#29#28InjectedString#29");
}
}
TEST(WriteIndirectObjectTest, EmptyStreamLengthSynchronization) {
// 1. Genuinely empty stream (not C2PA template) with non-zero /Length should
// have its /Length forced to 0.
{
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length",
Object{.value = Object::Integer{.value = 10}}}}},
.object = {.value = Object::Stream{.raw_value = ""}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(WriteIndirectObject(indirect_obj, output));
ASSERT_TRUE(output.Close());
EXPECT_EQ(output_s, R"(1 2 obj
<</Length 0>>
stream
endstream
endobj
)");
}
// 2. C2PA manifest store template stream during measurement (empty raw_value)
// with is_measurement = true should preserve its original /Length.
{
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries =
{{"Length", Object{.value = Object::Integer{.value = 100}}},
{"Subtype",
Object{.value =
Object::Name{.value = "application/c2pa"}}}}},
.object = {.value = Object::Stream{.raw_value = ""}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(
WriteIndirectObject(indirect_obj, output, /*is_measurement=*/true));
ASSERT_TRUE(output.Close());
// Substrings checked as dict serialization order is non-deterministic.
EXPECT_THAT(output_s, HasSubstr("/Length 100"));
EXPECT_THAT(output_s, HasSubstr("/Subtype /application#2fc2pa"));
EXPECT_THAT(output_s, HasSubstr("stream\n\nendstream"));
}
// 3. Attack vector: Malicious stream dictionary containing
// '_IsMeasurementCopy' key
// with is_measurement = false should still have its /Length forced to 0.
{
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length",
Object{.value = Object::Integer{.value = 100}}},
{"_IsMeasurementCopy",
Object{.value = Object::Boolean{.value = true}}}}},
.object = {.value = Object::Stream{.raw_value = ""}}};
std::string output_s;
riegeli::StringWriter<> output(&output_s);
ABSL_ASSERT_OK(
WriteIndirectObject(indirect_obj, output, /*is_measurement=*/false));
ASSERT_TRUE(output.Close());
EXPECT_THAT(output_s, HasSubstr("/Length 0"));
EXPECT_THAT(output_s, HasSubstr("/_IsMeasurementCopy true"));
}
}
struct FailureTestCase {
Object obj;
std::string name;
friend std::ostream& operator<<(std::ostream& os, const FailureTestCase& tc) {
return os << tc.name;
}
};
class WriteObjectFailureTest
: public ::testing::TestWithParam<FailureTestCase> {};
TEST_P(WriteObjectFailureTest, WriteFailsAndReturnsWriterStatus) {
const FailureTestCase& param = GetParam();
absl::Status expected_status = absl::InternalError("Simulated write failure");
FailingWriter writer(expected_status);
absl::Status status = WriteObject(param.obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated write failure")));
}
INSTANTIATE_TEST_SUITE_P(
WriteObjectFailureTests, WriteObjectFailureTest,
::testing::Values(
FailureTestCase{.obj = Object{.value = Object::Boolean{.value = true}},
.name = "Boolean"},
FailureTestCase{.obj = Object{.value = Object::Integer{.value = 42}},
.name = "Integer"},
FailureTestCase{
.obj = Object{.value = Object::RealNumber{.raw_value = "3.14"}},
.name = "RealNumber"},
FailureTestCase{
.obj = Object{.value = Object::LiteralString{.raw_value = "test"}},
.name = "LiteralString"},
FailureTestCase{
.obj =
Object{.value = Object::HexadecimalString{.raw_value = "AB"}},
.name = "HexadecimalString"},
FailureTestCase{.obj = Object{.value = Object::Name{.value = "test"}},
.name = "Name"},
FailureTestCase{.obj = Object{.value = Object::Array{.objects = {}}},
.name = "Array"},
FailureTestCase{
.obj = Object{.value = Object::Dictionary{.entries = {}}},
.name = "Dictionary"},
FailureTestCase{.obj = Object{.value = Object::Null{}}, .name = "Null"},
FailureTestCase{
.obj = Object{.value =
Object::IndirectReference{
.object_number = 1, .generation_number = 0}},
.name = "IndirectReference"}),
[](const ::testing::TestParamInfo<WriteObjectFailureTest::ParamType>&
info) { return info.param.name; });
TEST(WriteIndirectObjectTest, WriteFailsAndReturnsWriterStatus) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.object = {.value = Object::Name{.value = "Catalog"}}};
absl::Status expected_status =
absl::InternalError("Simulated indirect write failure");
FailingWriter writer(expected_status);
absl::Status status = WriteIndirectObject(indirect_obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated indirect write failure")));
}
TEST(WriteObjectTest, ArraySpaceWriteFailure) {
Object obj = {.value = Object::Array{
.objects = {Object{.value = Object::Integer{.value = 1}},
Object{.value = Object::Integer{.value = 2}}}}};
StepFailingWriter writer(2, absl::InternalError("Simulated space failure"));
absl::Status status = WriteObject(obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated space failure")));
}
TEST(WriteObjectTest, ArrayCloseBracketWriteFailure) {
Object obj = {.value = Object::Array{
.objects = {Object{.value = Object::Integer{.value = 1}}}}};
StepFailingWriter writer(
2, absl::InternalError("Simulated close bracket failure"));
absl::Status status = WriteObject(obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated close bracket failure")));
}
TEST(WriteObjectTest, DictionarySpaceWriteFailure) {
Object obj = {
.value = Object::Dictionary{
.entries = {{"K", Object{.value = Object::Integer{.value = 1}}}}}};
StepFailingWriter writer(4, absl::InternalError("Simulated space failure"));
absl::Status status = WriteObject(obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated space failure")));
}
TEST(WriteObjectTest, DictionaryCloseBracketsWriteFailure) {
Object obj = {
.value = Object::Dictionary{
.entries = {{"K", Object{.value = Object::Integer{.value = 1}}}}}};
StepFailingWriter writer(
6, absl::InternalError("Simulated close brackets failure"));
absl::Status status = WriteObject(obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated close brackets failure")));
}
TEST(WriteIndirectObjectTest, WriteStreamObjectFailure) {
IndirectObject indirect_obj = {
.object_number = 1,
.generation_number = 2,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length",
Object{.value = Object::Integer{.value = 10}}}}},
.object = {.value = Object::Stream{.raw_value = "1234567890"}}};
StepFailingWriter writer(23, absl::InternalError("Simulated stream failure"));
absl::Status status = WriteIndirectObject(indirect_obj, writer);
EXPECT_THAT(status, StatusIs(absl::StatusCode::kInternal,
HasSubstr("Simulated stream failure")));
}
} // namespace
} // namespace credentio