Enable meta-tagging for redaction purposes
See https://protobuf.dev/news/2024-12-04/

PiperOrigin-RevId: 721507009
diff --git a/src/google/protobuf/BUILD.bazel b/src/google/protobuf/BUILD.bazel
index f3c1442..a1c448c 100644
--- a/src/google/protobuf/BUILD.bazel
+++ b/src/google/protobuf/BUILD.bazel
@@ -878,6 +878,7 @@
         "unittest_proto3_extensions.proto",
         "unittest_proto3_lite.proto",
         "unittest_proto3_optional.proto",
+        "unittest_redaction.proto",
         "unittest_retention.proto",
         "unittest_string_type.proto",
         "unittest_well_known_types.proto",
@@ -961,6 +962,7 @@
         "unittest_proto3_bad_macros.proto",
         "unittest_proto3_extensions.proto",
         "unittest_proto3_lite.proto",
+        "unittest_redaction.proto",
         "unittest_retention.proto",
         "unittest_string_type.proto",
         "unittest_string_view.proto",
diff --git a/src/google/protobuf/text_format.cc b/src/google/protobuf/text_format.cc
index 45693ad..2524d49 100644
--- a/src/google/protobuf/text_format.cc
+++ b/src/google/protobuf/text_format.cc
@@ -3026,12 +3026,70 @@
   }
 }
 
+// Traverse the tree of field options and check if any of them are sensitive.
+// We check for sensitive enum values in the options and in the fields of the
+// message-type options, recursively.
+TextFormat::RedactionState TextFormat::IsOptionSensitive(
+    const Message& opts, const Reflection* reflection,
+    const FieldDescriptor* option) {
+  if (option->type() == FieldDescriptor::TYPE_ENUM) {
+    auto count =
+        option->is_repeated() ? reflection->FieldSize(opts, option) : 1;
+    for (auto i = 0; i < count; i++) {
+      int enum_val = option->is_repeated()
+                         ? reflection->GetRepeatedEnumValue(opts, option, i)
+                         : reflection->GetEnumValue(opts, option);
+      const EnumValueDescriptor* option_value =
+          option->enum_type()->FindValueByNumber(enum_val);
+      if (option_value->options().debug_redact()) {
+        return TextFormat::RedactionState{true, false};
+      }
+    }
+  } else if (option->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
+    auto count =
+        option->is_repeated() ? reflection->FieldSize(opts, option) : 1;
+    for (auto i = 0; i < count; i++) {
+      const Message& sub_message =
+          option->is_repeated()
+              ? reflection->GetRepeatedMessage(opts, option, i)
+              : reflection->GetMessage(opts, option);
+      const Reflection* sub_reflection = sub_message.GetReflection();
+      std::vector<const FieldDescriptor*> message_fields;
+      sub_reflection->ListFields(sub_message, &message_fields);
+      for (const FieldDescriptor* message_field : message_fields) {
+        auto result = TextFormat::IsOptionSensitive(sub_message, sub_reflection,
+                                                    message_field);
+        if (result.redact) {
+          return result;
+        }
+      }
+    }
+  }
+  return TextFormat::RedactionState{false, false};
+}
+
+TextFormat::RedactionState TextFormat::GetRedactionState(
+    const FieldDescriptor* field) {
+  auto options = field->options();
+  auto state = TextFormat::RedactionState{options.debug_redact(), false};
+  std::vector<const FieldDescriptor*> field_options;
+  const Reflection* reflection = options.GetReflection();
+  reflection->ListFields(options, &field_options);
+  for (const FieldDescriptor* option : field_options) {
+    auto result = TextFormat::IsOptionSensitive(options, reflection, option);
+    state = TextFormat::RedactionState{state.redact || result.redact,
+                                       state.report || result.report};
+  }
+  return state;
+}
 bool TextFormat::Printer::TryRedactFieldValue(
     const Message& message, const FieldDescriptor* field,
     BaseTextGenerator* generator, bool insert_value_separator) const {
-  RedactionState redaction_state = field->options().debug_redact()
-                                       ? RedactionState{true, false}
-                                       : RedactionState{false, false};
+  TextFormat::RedactionState redaction_state =
+      field->file()->pool()->MemoizeProjection(
+          field, [](const FieldDescriptor* field) {
+            return TextFormat::GetRedactionState(field);
+          });
   if (redaction_state.redact) {
     if (redact_debug_string_) {
       IncrementRedactedFieldCounter();
@@ -3051,7 +3109,6 @@
   }
   return false;
 }
-
 }  // namespace protobuf
 }  // namespace google
 
diff --git a/src/google/protobuf/text_format.h b/src/google/protobuf/text_format.h
index 9812be1..58dded1 100644
--- a/src/google/protobuf/text_format.h
+++ b/src/google/protobuf/text_format.h
@@ -622,6 +622,12 @@
     bool report;
   };
 
+  static TextFormat::RedactionState GetRedactionState(
+      const FieldDescriptor* field);
+
+  static TextFormat::RedactionState IsOptionSensitive(
+      const Message& opts, const Reflection* reflection,
+      const FieldDescriptor* option);
   // Data structure which is populated with the locations of each field
   // value parsed from the text.
   class PROTOBUF_EXPORT ParseInfoTree {
diff --git a/src/google/protobuf/text_format_unittest.cc b/src/google/protobuf/text_format_unittest.cc
index 58b5e0a..c57333c 100644
--- a/src/google/protobuf/text_format_unittest.cc
+++ b/src/google/protobuf/text_format_unittest.cc
@@ -49,6 +49,7 @@
 #include "google/protobuf/unittest_mset.pb.h"
 #include "google/protobuf/unittest_mset_wire_format.pb.h"
 #include "google/protobuf/unittest_proto3.pb.h"
+#include "google/protobuf/unittest_redaction.pb.h"
 #include "utf8_validity.h"
 
 
@@ -2679,6 +2680,85 @@
 }
 
 
+TEST(AbslStringifyTest, TextFormatIsUnchanged) {
+  unittest::TestAllTypes proto;
+  proto.set_optional_int32(1);
+  proto.set_optional_string("foo");
+
+  std::string text;
+  ASSERT_TRUE(TextFormat::PrintToString(proto, &text));
+  EXPECT_EQ(
+      "optional_int32: 1\n"
+      "optional_string: \"foo\"\n",
+      text);
+}
+
+TEST(AbslStringifyTest, StringifyHasRedactionMarker) {
+  unittest::TestAllTypes proto;
+  proto.set_optional_int32(1);
+  proto.set_optional_string("foo");
+
+  EXPECT_THAT(absl::StrCat(proto), testing::MatchesRegex(
+                                       "optional_int32: 1\n"
+                                       "optional_string: \"foo\"\n"));
+}
+
+
+TEST(AbslStringifyTest, StringifyMetaAnnotatedIsRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_meta_annotated("foo");
+  EXPECT_THAT(absl::StrCat(proto), testing::MatchesRegex(absl::Substitute(
+                                       "meta_annotated: $0\n",
+                                       value_replacement)));
+}
+
+TEST(AbslStringifyTest, StringifyRepeatedMetaAnnotatedIsRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_repeated_meta_annotated("foo");
+  EXPECT_THAT(absl::StrCat(proto), testing::MatchesRegex(absl::Substitute(
+                                       "repeated_meta_annotated: $0\n",
+                                       value_replacement)));
+}
+
+TEST(AbslStringifyTest, StringifyRepeatedMetaAnnotatedIsNotRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_unredacted_repeated_annotations("foo");
+  EXPECT_THAT(absl::StrCat(proto),
+              testing::MatchesRegex(
+                  "unredacted_repeated_annotations: \"foo\"\n"));
+}
+
+TEST(AbslStringifyTest, TextFormatMetaAnnotatedIsNotRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_meta_annotated("foo");
+  std::string text;
+  ASSERT_TRUE(TextFormat::PrintToString(proto, &text));
+  EXPECT_EQ("meta_annotated: \"foo\"\n", text);
+}
+TEST(AbslStringifyTest, StringifyDirectMessageEnumIsRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_test_direct_message_enum("foo");
+  EXPECT_THAT(absl::StrCat(proto), testing::MatchesRegex(absl::Substitute(
+                                       "test_direct_message_enum: $0\n",
+                                       value_replacement)));
+}
+TEST(AbslStringifyTest, StringifyNestedMessageEnumIsRedacted) {
+  unittest::TestRedactedMessage proto;
+  proto.set_test_nested_message_enum("foo");
+  EXPECT_THAT(absl::StrCat(proto), testing::MatchesRegex(absl::Substitute(
+                                       "test_nested_message_enum: $0\n",
+                                       value_replacement)));
+}
+
+TEST(AbslStringifyTest, StringifyRedactedOptionDoesNotRedact) {
+  unittest::TestRedactedMessage proto;
+  proto.set_test_redacted_message_enum("foo");
+  EXPECT_THAT(absl::StrCat(proto),
+              testing::MatchesRegex(
+                  "test_redacted_message_enum: \"foo\"\n"));
+}
+
+
 TEST(TextFormatFloatingPointTest, PreservesNegative0) {
   proto3_unittest::TestAllTypes in_message;
   in_message.set_optional_float(-0.0f);
diff --git a/src/google/protobuf/unittest_redaction.proto b/src/google/protobuf/unittest_redaction.proto
new file mode 100644
index 0000000..f74a6fd
--- /dev/null
+++ b/src/google/protobuf/unittest_redaction.proto
@@ -0,0 +1,72 @@
+// Test proto for redaction
+edition = "2023";
+
+package protobuf_unittest;
+
+import "google/protobuf/any.proto";
+import "google/protobuf/descriptor.proto";
+
+option java_package = "com.google.protos";
+option java_outer_classname = "RedactionProto";
+option features.repeated_field_encoding = EXPANDED;
+option features.utf8_validation = NONE;
+
+extend .google.protobuf.FieldOptions {
+  MetaAnnotatedEnum meta_annotated_enum = 535801413;
+  repeated MetaAnnotatedEnum repeated_meta_annotated_enum = 535801414;
+  TestNestedMessageEnum test_nested_message_enum = 535801415;
+}
+
+message TestRedactedNestMessage {
+  string foo = 1;
+}
+
+message TestRepeatedRedactedNestMessage {
+  string bar = 1;
+}
+
+message TestMessageEnum {
+  repeated MetaAnnotatedEnum redactable_enum = 1;
+}
+
+message TestNestedMessageEnum {
+  repeated MetaAnnotatedEnum direct_enum = 1;
+  TestMessageEnum nested_enum = 2;
+  string redacted_string = 3 [debug_redact = true];
+}
+
+message TestRedactedMessage {
+  string text_field = 1 [deprecated = true];
+  string meta_annotated = 8 [(meta_annotated_enum) = TEST_REDACTABLE];
+  string repeated_meta_annotated = 9 [
+    (protobuf_unittest.repeated_meta_annotated_enum) = TEST_NO_REDACT,
+    (protobuf_unittest.repeated_meta_annotated_enum) = TEST_REDACTABLE
+  ];
+  string unredacted_repeated_annotations = 10 [
+    (protobuf_unittest.repeated_meta_annotated_enum) = TEST_NO_REDACT,
+    (protobuf_unittest.repeated_meta_annotated_enum) = TEST_NO_REDACT_AGAIN
+  ];
+  string unreported_non_meta_debug_redact_field = 17 [debug_redact = true];
+  google.protobuf.Any any_field = 18 [debug_redact = true];
+  string redactable_false = 19 [(meta_annotated_enum) = TEST_REDACTABLE_FALSE];
+  string test_direct_message_enum = 22
+      [(protobuf_unittest.test_nested_message_enum) = {
+        direct_enum: [ TEST_NO_REDACT, TEST_REDACTABLE ]
+      }];
+  string test_nested_message_enum = 23
+      [(protobuf_unittest.test_nested_message_enum) = {
+        nested_enum { redactable_enum: [ TEST_NO_REDACT, TEST_REDACTABLE ] }
+      }];
+  string test_redacted_message_enum = 24
+      [(protobuf_unittest.test_nested_message_enum) = {
+        redacted_string: "redacted_but_doesnt_redact"
+      }];
+}
+
+enum MetaAnnotatedEnum {
+  TEST_NULL = 0;
+  TEST_REDACTABLE = 1 [debug_redact = true];
+  TEST_NO_REDACT = 2;
+  TEST_NO_REDACT_AGAIN = 3;
+  TEST_REDACTABLE_FALSE = 4 [debug_redact = false];
+}