diff --git a/java/core/BUILD.bazel b/java/core/BUILD.bazel index 683b0654ac..9027b7be27 100644 --- a/java/core/BUILD.bazel +++ b/java/core/BUILD.bazel @@ -520,6 +520,7 @@ LITE_TEST_EXCLUSIONS = [ "src/test/java/com/google/protobuf/AnyTest.java", "src/test/java/com/google/protobuf/CodedInputStreamTest.java", "src/test/java/com/google/protobuf/DeprecatedFieldTest.java", + "src/test/java/com/google/protobuf/DebugFormatTest.java", "src/test/java/com/google/protobuf/DescriptorsTest.java", "src/test/java/com/google/protobuf/DiscardUnknownFieldsTest.java", "src/test/java/com/google/protobuf/DynamicMessageTest.java", diff --git a/java/core/src/main/java/com/google/protobuf/DebugFormat.java b/java/core/src/main/java/com/google/protobuf/DebugFormat.java new file mode 100644 index 0000000000..75dc4f3705 --- /dev/null +++ b/java/core/src/main/java/com/google/protobuf/DebugFormat.java @@ -0,0 +1,83 @@ +package com.google.protobuf; + +import com.google.protobuf.Descriptors.FieldDescriptor; + +/** + * Provides an explicit API for unstable, redacting debug output suitable for debug logging. This + * implementation is based on TextFormat, but should not be parsed. + */ +final class DebugFormat { + + private final boolean isSingleLine; + + private DebugFormat(boolean singleLine) { + isSingleLine = singleLine; + } + + public static DebugFormat singleLine() { + return new DebugFormat(true); + } + + public static DebugFormat multiline() { + return new DebugFormat(false); + } + + public String toString(MessageOrBuilder message) { + return TextFormat.printer() + .emittingSingleLine(this.isSingleLine) + .enablingSafeDebugFormat(true) + .printToString(message); + } + + public String toString(FieldDescriptor field, Object value) { + return TextFormat.printer() + .emittingSingleLine(this.isSingleLine) + .enablingSafeDebugFormat(true) + .printFieldToString(field, value); + } + + public String toString(UnknownFieldSet fields) { + return TextFormat.printer() + .emittingSingleLine(this.isSingleLine) + .enablingSafeDebugFormat(true) + .printToString(fields); + } + + public Object lazyToString(MessageOrBuilder message) { + return new LazyDebugOutput(message, this.isSingleLine); + } + + public Object lazyToString(UnknownFieldSet fields) { + return new LazyDebugOutput(fields, this.isSingleLine); + } + + private static class LazyDebugOutput { + private final MessageOrBuilder message; + private final UnknownFieldSet fields; + private final boolean isSingleLine; + + LazyDebugOutput(MessageOrBuilder message, boolean isSingleLine) { + this.message = message; + this.fields = null; + this.isSingleLine = isSingleLine; + } + + LazyDebugOutput(UnknownFieldSet fields, boolean isSingleLine) { + this.fields = fields; + this.message = null; + this.isSingleLine = isSingleLine; + } + + @Override + public String toString() { + if (message != null) { + return this.isSingleLine + ? DebugFormat.singleLine().toString(message) + : DebugFormat.multiline().toString(message); + } + return this.isSingleLine + ? DebugFormat.singleLine().toString(fields) + : DebugFormat.multiline().toString(fields); + } + } +} diff --git a/java/core/src/main/java/com/google/protobuf/TextFormat.java b/java/core/src/main/java/com/google/protobuf/TextFormat.java index eb05fb2f41..431c1539b9 100644 --- a/java/core/src/main/java/com/google/protobuf/TextFormat.java +++ b/java/core/src/main/java/com/google/protobuf/TextFormat.java @@ -38,13 +38,21 @@ public final class TextFormat { private static final String DEBUG_STRING_SILENT_MARKER = "\t "; + private static final String REDACTED_MARKER = "[REDACTED]"; + /** * Generates a human readable form of this message, useful for debugging and other purposes, with * no newline characters. This is just a trivial wrapper around {@link * TextFormat.Printer#shortDebugString(MessageOrBuilder)}. + * + * @deprecated Use {@code printer().emittingSingleLine(true).printToString(MessageOrBuilder)} */ + @Deprecated + @InlineMe( + replacement = "TextFormat.printer().emittingSingleLine(true).printToString(message)", + imports = "com.google.protobuf.TextFormat") public static String shortDebugString(final MessageOrBuilder message) { - return printer().shortDebugString(message); + return printer().emittingSingleLine(true).printToString(message); } /** @@ -58,11 +66,12 @@ public final class TextFormat { */ public static void printUnknownFieldValue( final int tag, final Object value, final Appendable output) throws IOException { - printUnknownFieldValue(tag, value, multiLineOutput(output)); + printUnknownFieldValue(tag, value, setLineOutput(output, false), false); } private static void printUnknownFieldValue( - final int tag, final Object value, final TextGenerator generator) throws IOException { + final int tag, final Object value, final TextGenerator generator, boolean redact) + throws IOException { switch (WireFormat.getTagWireType(tag)) { case WireFormat.WIRETYPE_VARINT: generator.print(unsignedToString((Long) value)); @@ -80,7 +89,7 @@ public final class TextFormat { generator.print("{"); generator.eol(); generator.indent(); - Printer.printUnknownFields(message, generator); + Printer.printUnknownFields(message, generator, redact); generator.outdent(); generator.print("}"); } catch (InvalidProtocolBufferException e) { @@ -91,7 +100,7 @@ public final class TextFormat { } break; case WireFormat.WIRETYPE_START_GROUP: - Printer.printUnknownFields((UnknownFieldSet) value, generator); + Printer.printUnknownFields((UnknownFieldSet) value, generator, redact); break; default: throw new IllegalArgumentException("Bad tag: " + tag); @@ -109,7 +118,11 @@ public final class TextFormat { // Printer instance which escapes non-ASCII characters. private static final Printer DEFAULT = new Printer( - true, TypeRegistry.getEmptyTypeRegistry(), ExtensionRegistryLite.getEmptyRegistry()); + true, + TypeRegistry.getEmptyTypeRegistry(), + ExtensionRegistryLite.getEmptyRegistry(), + false, + false); /** Whether to escape non ASCII characters with backslash and octal. */ private final boolean escapeNonAscii; @@ -117,13 +130,25 @@ public final class TextFormat { private final TypeRegistry typeRegistry; private final ExtensionRegistryLite extensionRegistry; + /** + * Whether to enable redaction of sensitive fields and introduce randomization. Note that when + * this is enabled, the output will no longer be deserializable. + */ + private final boolean enablingSafeDebugFormat; + + private final boolean singleLine; + private Printer( boolean escapeNonAscii, TypeRegistry typeRegistry, - ExtensionRegistryLite extensionRegistry) { + ExtensionRegistryLite extensionRegistry, + boolean enablingSafeDebugFormat, + boolean singleLine) { this.escapeNonAscii = escapeNonAscii; this.typeRegistry = typeRegistry; this.extensionRegistry = extensionRegistry; + this.enablingSafeDebugFormat = enablingSafeDebugFormat; + this.singleLine = singleLine; } /** @@ -136,7 +161,8 @@ public final class TextFormat { * with the escape mode set to the given parameter. */ public Printer escapingNonAscii(boolean escapeNonAscii) { - return new Printer(escapeNonAscii, typeRegistry, extensionRegistry); + return new Printer( + escapeNonAscii, typeRegistry, extensionRegistry, enablingSafeDebugFormat, singleLine); } /** @@ -149,7 +175,8 @@ public final class TextFormat { if (this.typeRegistry != TypeRegistry.getEmptyTypeRegistry()) { throw new IllegalArgumentException("Only one typeRegistry is allowed."); } - return new Printer(escapeNonAscii, typeRegistry, extensionRegistry); + return new Printer( + escapeNonAscii, typeRegistry, extensionRegistry, enablingSafeDebugFormat, singleLine); } /** @@ -162,7 +189,34 @@ public final class TextFormat { if (this.extensionRegistry != ExtensionRegistryLite.getEmptyRegistry()) { throw new IllegalArgumentException("Only one extensionRegistry is allowed."); } - return new Printer(escapeNonAscii, typeRegistry, extensionRegistry); + return new Printer( + escapeNonAscii, typeRegistry, extensionRegistry, enablingSafeDebugFormat, singleLine); + } + + /** + * Return a new Printer instance that outputs a redacted and unstable format suitable for + * debugging. + * + * @param enablingSafeDebugFormat If true, the new Printer will redact all proto fields that are + * marked by a debug_redact=true option, and apply an unstable prefix to the output. + * @return a new Printer that clones all other configurations from the current {@link Printer}, + * with the enablingSafeDebugFormat mode set to the given parameter. + */ + Printer enablingSafeDebugFormat(boolean enablingSafeDebugFormat) { + return new Printer( + escapeNonAscii, typeRegistry, extensionRegistry, enablingSafeDebugFormat, singleLine); + } + + /** + * Return a new Printer instance with the specified line formatting status. + * + * @param singleLine If true, the new Printer will output no newline characters. + * @return a new Printer that clones all other configurations from the current {@link Printer}, + * with the singleLine mode set to the given parameter. + */ + public Printer emittingSingleLine(boolean singleLine) { + return new Printer( + escapeNonAscii, typeRegistry, extensionRegistry, enablingSafeDebugFormat, singleLine); } /** @@ -171,12 +225,13 @@ public final class TextFormat { * original Protocol Buffer system) */ public void print(final MessageOrBuilder message, final Appendable output) throws IOException { - print(message, multiLineOutput(output)); + print(message, setLineOutput(output, this.singleLine)); } /** Outputs a textual representation of {@code fields} to {@code output}. */ public void print(final UnknownFieldSet fields, final Appendable output) throws IOException { - printUnknownFields(fields, multiLineOutput(output)); + printUnknownFields( + fields, setLineOutput(output, this.singleLine), this.enablingSafeDebugFormat); } private void print(final MessageOrBuilder message, final TextGenerator generator) @@ -188,6 +243,14 @@ public final class TextFormat { printMessage(message, generator); } + private void applyUnstablePrefix(final Appendable output) { + try { + output.append(""); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + /** * Attempt to print the 'google.protobuf.Any' message in a human-friendly format. Returns false * if the message isn't a valid 'google.protobuf.Any' message (in which case the message should @@ -244,6 +307,9 @@ public final class TextFormat { public String printFieldToString(final FieldDescriptor field, final Object value) { try { final StringBuilder text = new StringBuilder(); + if (enablingSafeDebugFormat) { + applyUnstablePrefix(text); + } printField(field, value, text); return text.toString(); } catch (IOException e) { @@ -253,7 +319,7 @@ public final class TextFormat { public void printField(final FieldDescriptor field, final Object value, final Appendable output) throws IOException { - printField(field, value, multiLineOutput(output)); + printField(field, value, setLineOutput(output, this.singleLine)); } private void printField( @@ -358,12 +424,19 @@ public final class TextFormat { public void printFieldValue( final FieldDescriptor field, final Object value, final Appendable output) throws IOException { - printFieldValue(field, value, multiLineOutput(output)); + printFieldValue(field, value, setLineOutput(output, this.singleLine)); } private void printFieldValue( final FieldDescriptor field, final Object value, final TextGenerator generator) throws IOException { + if (shouldRedact(field)) { + generator.print(REDACTED_MARKER); + if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { + generator.eol(); + } + return; + } switch (field.getType()) { case INT32: case SINT32: @@ -429,10 +502,54 @@ public final class TextFormat { } } + private boolean shouldRedactOptionValue(EnumValueDescriptor optionValue) { + if (optionValue.getOptions().hasDebugRedact()) { + return optionValue.getOptions().getDebugRedact(); + } + return false; + } + + // The criteria for redacting a field is as follows: 1) The enablingSafeDebugFormat printer + // option + // must be on. 2) The field must be marked by a debug_redact=true option, or is marked by an + // option with an enum value that is marked by a debug_redact=true option. + private boolean shouldRedact(final FieldDescriptor field) { + if (!this.enablingSafeDebugFormat) { + return false; + } + if (field.getOptions().hasDebugRedact()) { + return field.getOptions().getDebugRedact(); + } + // Iterate through every option; if it's an enum, we check each enum value for debug_redact. + for (Map.Entry entry : + field.getOptions().getAllFields().entrySet()) { + Descriptors.FieldDescriptor option = entry.getKey(); + if (option.getType() != Descriptors.FieldDescriptor.Type.ENUM) { + continue; + } + if (option.isRepeated()) { + for (EnumValueDescriptor value : (List) entry.getValue()) { + if (shouldRedactOptionValue(value)) { + return true; + } + } + } else { + EnumValueDescriptor optionValue = (EnumValueDescriptor) entry.getValue(); + if (shouldRedactOptionValue(optionValue)) { + return true; + } + } + } + return false; + } + /** Like {@code print()}, but writes directly to a {@code String} and returns it. */ public String printToString(final MessageOrBuilder message) { try { final StringBuilder text = new StringBuilder(); + if (enablingSafeDebugFormat) { + applyUnstablePrefix(text); + } print(message, text); return text.toString(); } catch (IOException e) { @@ -443,6 +560,9 @@ public final class TextFormat { public String printToString(final UnknownFieldSet fields) { try { final StringBuilder text = new StringBuilder(); + if (enablingSafeDebugFormat) { + applyUnstablePrefix(text); + } print(fields, text); return text.toString(); } catch (IOException e) { @@ -453,56 +573,62 @@ public final class TextFormat { /** * Generates a human readable form of this message, useful for debugging and other purposes, * with no newline characters. + * + * @deprecated Use {@code + * this.printer().emittingSingleLine(true).printToString(MessageOrBuilder)} */ + @Deprecated + @InlineMe(replacement = "this.emittingSingleLine(true).printToString(message)") public String shortDebugString(final MessageOrBuilder message) { - try { - final StringBuilder text = new StringBuilder(); - print(message, singleLineOutput(text)); - return text.toString(); - } catch (IOException e) { - throw new IllegalStateException(e); - } + return this.emittingSingleLine(true).printToString(message); } /** * Generates a human readable form of the field, useful for debugging and other purposes, with * no newline characters. + * + * @deprecated Use {@code this.emittingSingleLine(true).printFieldToString(FieldDescriptor, + * Object)} */ + @Deprecated + @InlineMe(replacement = "this.emittingSingleLine(true).printFieldToString(field, value)") public String shortDebugString(final FieldDescriptor field, final Object value) { - try { - final StringBuilder text = new StringBuilder(); - printField(field, value, singleLineOutput(text)); - return text.toString(); - } catch (IOException e) { - throw new IllegalStateException(e); - } + return this.emittingSingleLine(true).printFieldToString(field, value); } /** * Generates a human readable form of the unknown fields, useful for debugging and other * purposes, with no newline characters. + * + * @deprecated Use {@code this.emittingSingleLine(true).printToString(UnknownFieldSet)} */ + @Deprecated + @InlineMe(replacement = "this.emittingSingleLine(true).printToString(fields)") public String shortDebugString(final UnknownFieldSet fields) { - try { - final StringBuilder text = new StringBuilder(); - printUnknownFields(fields, singleLineOutput(text)); - return text.toString(); - } catch (IOException e) { - throw new IllegalStateException(e); - } + return this.emittingSingleLine(true).printToString(fields); } private static void printUnknownFieldValue( - final int tag, final Object value, final TextGenerator generator) throws IOException { + final int tag, final Object value, final TextGenerator generator, boolean redact) + throws IOException { switch (WireFormat.getTagWireType(tag)) { case WireFormat.WIRETYPE_VARINT: - generator.print(unsignedToString((Long) value)); + generator.print( + redact + ? String.format("UNKNOWN_VARINT %s", REDACTED_MARKER) + : unsignedToString((Long) value)); break; case WireFormat.WIRETYPE_FIXED32: - generator.print(String.format((Locale) null, "0x%08x", (Integer) value)); + generator.print( + redact + ? String.format("UNKNOWN_FIXED32 %s", REDACTED_MARKER) + : String.format((Locale) null, "0x%08x", (Integer) value)); break; case WireFormat.WIRETYPE_FIXED64: - generator.print(String.format((Locale) null, "0x%016x", (Long) value)); + generator.print( + redact + ? String.format("UNKNOWN_FIXED64 %s", REDACTED_MARKER) + : String.format((Locale) null, "0x%016x", (Long) value)); break; case WireFormat.WIRETYPE_LENGTH_DELIMITED: try { @@ -511,18 +637,22 @@ public final class TextFormat { generator.print("{"); generator.eol(); generator.indent(); - printUnknownFields(message, generator); + printUnknownFields(message, generator, redact); generator.outdent(); generator.print("}"); } catch (InvalidProtocolBufferException e) { // If not parseable as a message, print as a String + if (redact) { + generator.print(String.format("UNKNOWN_STRING %s", REDACTED_MARKER)); + break; + } generator.print("\""); generator.print(escapeBytes((ByteString) value)); generator.print("\""); } break; case WireFormat.WIRETYPE_START_GROUP: - printUnknownFields((UnknownFieldSet) value, generator); + printUnknownFields((UnknownFieldSet) value, generator, redact); break; default: throw new IllegalArgumentException("Bad tag: " + tag); @@ -534,7 +664,7 @@ public final class TextFormat { for (Map.Entry field : message.getAllFields().entrySet()) { printField(field.getKey(), field.getValue(), generator); } - printUnknownFields(message.getUnknownFields(), generator); + printUnknownFields(message.getUnknownFields(), generator, this.enablingSafeDebugFormat); } private void printSingleField( @@ -580,24 +710,29 @@ public final class TextFormat { } private static void printUnknownFields( - final UnknownFieldSet unknownFields, final TextGenerator generator) throws IOException { + final UnknownFieldSet unknownFields, final TextGenerator generator, boolean redact) + throws IOException { for (Map.Entry entry : unknownFields.asMap().entrySet()) { final int number = entry.getKey(); final UnknownFieldSet.Field field = entry.getValue(); - printUnknownField(number, WireFormat.WIRETYPE_VARINT, field.getVarintList(), generator); - printUnknownField(number, WireFormat.WIRETYPE_FIXED32, field.getFixed32List(), generator); - printUnknownField(number, WireFormat.WIRETYPE_FIXED64, field.getFixed64List(), generator); + printUnknownField( + number, WireFormat.WIRETYPE_VARINT, field.getVarintList(), generator, redact); + printUnknownField( + number, WireFormat.WIRETYPE_FIXED32, field.getFixed32List(), generator, redact); + printUnknownField( + number, WireFormat.WIRETYPE_FIXED64, field.getFixed64List(), generator, redact); printUnknownField( number, WireFormat.WIRETYPE_LENGTH_DELIMITED, field.getLengthDelimitedList(), - generator); + generator, + redact); for (final UnknownFieldSet value : field.getGroupList()) { generator.print(entry.getKey().toString()); generator.print(" {"); generator.eol(); generator.indent(); - printUnknownFields(value, generator); + printUnknownFields(value, generator, redact); generator.outdent(); generator.print("}"); generator.eol(); @@ -606,12 +741,16 @@ public final class TextFormat { } private static void printUnknownField( - final int number, final int wireType, final List values, final TextGenerator generator) + final int number, + final int wireType, + final List values, + final TextGenerator generator, + boolean redact) throws IOException { for (final Object value : values) { generator.print(String.valueOf(number)); generator.print(": "); - printUnknownFieldValue(wireType, value, generator); + printUnknownFieldValue(wireType, value, generator, redact); generator.eol(); } } @@ -637,12 +776,8 @@ public final class TextFormat { } } - private static TextGenerator multiLineOutput(Appendable output) { - return new TextGenerator(output, false); - } - - private static TextGenerator singleLineOutput(Appendable output) { - return new TextGenerator(output, true); + private static TextGenerator setLineOutput(Appendable output, boolean singleLine) { + return new TextGenerator(output, singleLine); } /** An inner class for writing text to the output stream. */ diff --git a/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java b/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java new file mode 100644 index 0000000000..784b4d34de --- /dev/null +++ b/java/core/src/test/java/com/google/protobuf/DebugFormatTest.java @@ -0,0 +1,216 @@ +package com.google.protobuf; + +import static com.google.common.truth.Truth.assertThat; +import static protobuf_unittest.UnittestProto.redactedExtension; + +import com.google.protobuf.Descriptors.FieldDescriptor; +import protobuf_unittest.UnittestProto.RedactedFields; +import protobuf_unittest.UnittestProto.TestEmptyMessage; +import protobuf_unittest.UnittestProto.TestNestedMessageRedaction; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class DebugFormatTest { + + private static final String REDACTED_REGEX = getRedactedRegex(); + private static final String UNSTABLE_PREFIX_SINGLE_LINE = getUnstablePrefix(true); + private static final String UNSTABLE_PREFIX_MULTILINE = getUnstablePrefix(false); + + private static String getRedactedRegex() { + String redactedRegex = "\\[REDACTED\\]"; + return redactedRegex; + } + + private static String getUnstablePrefix(boolean singleLine) { + String unstablePrefix = ""; + return unstablePrefix; + } + + @Test + public void multilineMessageFormat_returnsMultiline() { + RedactedFields message = RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + + String result = DebugFormat.multiline().toString(message); + assertThat(result) + .matches( + String.format("%soptional_unredacted_string: \"foo\"\n", UNSTABLE_PREFIX_MULTILINE)); + } + + @Test + public void singleLineMessageFormat_returnsSingleLine() { + RedactedFields message = RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + + String result = DebugFormat.singleLine().toString(message); + assertThat(result) + .matches( + String.format("%soptional_unredacted_string: \"foo\"", UNSTABLE_PREFIX_SINGLE_LINE)); + } + + @Test + public void messageFormat_debugRedactFieldIsRedacted() { + RedactedFields message = RedactedFields.newBuilder().setOptionalRedactedString("foo").build(); + + String result = DebugFormat.multiline().toString(message); + + assertThat(result) + .matches( + String.format( + "%soptional_redacted_string: %s\n", UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX)); + } + + @Test + public void messageFormat_debugRedactMessageIsRedacted() { + RedactedFields message = + RedactedFields.newBuilder() + .setOptionalRedactedMessage( + TestNestedMessageRedaction.newBuilder().setOptionalUnredactedNestedString("foo")) + .build(); + + String result = DebugFormat.multiline().toString(message); + + assertThat(result) + .matches( + String.format( + "%soptional_redacted_message \\{\n %s\n\\}\n", + UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX)); + } + + @Test + public void messageFormat_debugRedactMapIsRedacted() { + RedactedFields message = RedactedFields.newBuilder().putMapRedactedString("foo", "bar").build(); + + String result = DebugFormat.multiline().toString(message); + + assertThat(result) + .matches( + String.format( + "%smap_redacted_string \\{\\n %s\n\\}\n", + UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX)); + } + + @Test + public void messageFormat_debugRedactExtensionIsRedacted() { + RedactedFields message = + RedactedFields.newBuilder().setExtension(redactedExtension, "foo").build(); + + String result = DebugFormat.multiline().toString(message); + + assertThat(result) + .matches( + String.format( + "%s\\[protobuf_unittest\\.redacted_extension\\]: %s\n", + UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX)); + } + + @Test + public void messageFormat_redactFalseIsNotRedacted() { + RedactedFields message = + RedactedFields.newBuilder().setOptionalRedactedFalseString("foo").build(); + + String result = DebugFormat.multiline().toString(message); + assertThat(result) + .matches( + String.format( + "%soptional_redacted_false_string: \"foo\"\n", UNSTABLE_PREFIX_MULTILINE)); + } + + @Test + public void messageFormat_nonSensitiveFieldIsNotRedacted() { + RedactedFields message = RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + + String result = DebugFormat.multiline().toString(message); + + assertThat(result) + .matches( + String.format("%soptional_unredacted_string: \"foo\"\n", UNSTABLE_PREFIX_MULTILINE)); + } + + @Test + public void descriptorDebugFormat_returnsExpectedFormat() { + FieldDescriptor field = + RedactedFields.getDescriptor().findFieldByName("optional_redacted_string"); + String result = DebugFormat.multiline().toString(field, "foo"); + assertThat(result) + .matches( + String.format( + "%soptional_redacted_string: %s\n", UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX)); + } + + @Test + public void unstableFormat_isStablePerProcess() { + RedactedFields message1 = + RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + RedactedFields message2 = + RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + for (int i = 0; i < 5; i++) { + String result1 = DebugFormat.multiline().toString(message1); + String result2 = DebugFormat.multiline().toString(message2); + assertThat(result1).isEqualTo(result2); + } + } + + @Test + public void lazyDebugFormatMessage_supportsImplicitFormatting() { + RedactedFields message = RedactedFields.newBuilder().setOptionalUnredactedString("foo").build(); + + Object lazyDebug = DebugFormat.singleLine().lazyToString(message); + + assertThat(String.format("%s", lazyDebug)) + .matches( + String.format("%soptional_unredacted_string: \"foo\"", UNSTABLE_PREFIX_SINGLE_LINE)); + } + + private UnknownFieldSet makeUnknownFieldSet() { + return UnknownFieldSet.newBuilder() + .addField( + 5, + UnknownFieldSet.Field.newBuilder() + .addVarint(1) + .addFixed32(2) + .addFixed64(3) + .addLengthDelimited(ByteString.copyFromUtf8("4")) + .addLengthDelimited( + UnknownFieldSet.newBuilder() + .addField(12, UnknownFieldSet.Field.newBuilder().addVarint(6).build()) + .build() + .toByteString()) + .addGroup( + UnknownFieldSet.newBuilder() + .addField(10, UnknownFieldSet.Field.newBuilder().addVarint(5).build()) + .build()) + .build()) + .addField( + 8, UnknownFieldSet.Field.newBuilder().addVarint(1).addVarint(2).addVarint(3).build()) + .addField( + 15, + UnknownFieldSet.Field.newBuilder() + .addVarint(0xABCDEF1234567890L) + .addFixed32(0xABCD1234) + .addFixed64(0xABCDEF1234567890L) + .build()) + .build(); + } + + @Test + public void unknownFieldsDebugFormat_returnsExpectedFormat() { + TestEmptyMessage unknownFields = + TestEmptyMessage.newBuilder().setUnknownFields(makeUnknownFieldSet()).build(); + + assertThat(DebugFormat.multiline().toString(unknownFields)) + .matches( + String.format("%s5: UNKNOWN_VARINT %s\n", UNSTABLE_PREFIX_MULTILINE, REDACTED_REGEX) + + String.format("5: UNKNOWN_FIXED32 %s\n", REDACTED_REGEX) + + String.format("5: UNKNOWN_FIXED64 %s\n", REDACTED_REGEX) + + String.format("5: UNKNOWN_STRING %s\n", REDACTED_REGEX) + + String.format("5: \\{\n 12: UNKNOWN_VARINT %s\n\\}\n", REDACTED_REGEX) + + String.format("5 \\{\n 10: UNKNOWN_VARINT %s\n\\}\n", REDACTED_REGEX) + + String.format("8: UNKNOWN_VARINT %s\n", REDACTED_REGEX) + + String.format("8: UNKNOWN_VARINT %s\n", REDACTED_REGEX) + + String.format("8: UNKNOWN_VARINT %s\n", REDACTED_REGEX) + + String.format("15: UNKNOWN_VARINT %s\n", REDACTED_REGEX) + + String.format("15: UNKNOWN_FIXED32 %s\n", REDACTED_REGEX) + + String.format("15: UNKNOWN_FIXED64 %s\n", REDACTED_REGEX)); + } +} diff --git a/src/google/protobuf/unittest.proto b/src/google/protobuf/unittest.proto index 3d9dc3a775..bd31033587 100644 --- a/src/google/protobuf/unittest.proto +++ b/src/google/protobuf/unittest.proto @@ -1738,6 +1738,12 @@ message RedactedFields { repeated TestNestedMessageRedaction repeated_unredacted_message = 8; map map_redacted_string = 9 [debug_redact = true]; map map_unredacted_string = 10; + optional string optional_redacted_false_string = 11 [debug_redact = false]; + extensions 20 to 30; +} + +extend RedactedFields { + optional string redacted_extension = 20 [debug_redact = true]; } message TestCord{