• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #region Copyright notice and license
2 // Protocol Buffers - Google's data interchange format
3 // Copyright 2015 Google Inc.  All rights reserved.
4 //
5 // Use of this source code is governed by a BSD-style
6 // license that can be found in the LICENSE file or at
7 // https://developers.google.com/open-source/licenses/bsd
8 #endregion
9 
10 using System;
11 using System.Collections;
12 using System.Collections.Concurrent;
13 using System.Collections.Generic;
14 using System.Diagnostics.CodeAnalysis;
15 using System.Globalization;
16 using System.IO;
17 using System.Linq;
18 using System.Reflection;
19 using System.Text;
20 using Google.Protobuf.Reflection;
21 using Google.Protobuf.WellKnownTypes;
22 
23 namespace Google.Protobuf {
24   /// <summary>
25   /// Reflection-based converter from messages to JSON.
26   /// </summary>
27   /// <remarks>
28   /// <para>
29   /// Instances of this class are thread-safe, with no mutable state.
30   /// </para>
31   /// <para>
32   /// This is a simple start to get JSON formatting working. As it's reflection-based,
33   /// it's not as quick as baking calls into generated messages - but is a simpler implementation.
34   /// (This code is generally not heavily optimized.)
35   /// </para>
36   /// </remarks>
37   public sealed class JsonFormatter {
38     internal const string AnyTypeUrlField = "@type";
39     internal const string AnyDiagnosticValueField = "@value";
40     internal const string AnyWellKnownTypeValueField = "value";
41     private const string NameValueSeparator = ": ";
42     private const string ValueSeparator = ", ";
43     private const string MultilineValueSeparator = ",";
44     private const char ObjectOpenBracket = '{';
45     private const char ObjectCloseBracket = '}';
46     private const char ListBracketOpen = '[';
47     private const char ListBracketClose = ']';
48 
49     /// <summary>
50     /// Returns a formatter using the default settings.
51     /// </summary>
52     public static JsonFormatter Default { get; } = new JsonFormatter(Settings.Default);
53 
54     // A JSON formatter which *only* exists
55     private static readonly JsonFormatter diagnosticFormatter = new JsonFormatter(Settings.Default);
56 
57     /// <summary>
58     /// The JSON representation of the first 160 characters of Unicode.
59     /// Empty strings are replaced by the static constructor.
60     /// </summary>
61     private static readonly string[] CommonRepresentations = {
62       // C0 (ASCII and derivatives) control characters
63       "\\u0000", "\\u0001", "\\u0002", "\\u0003",  // 0x00
64       "\\u0004", "\\u0005", "\\u0006", "\\u0007", "\\b", "\\t", "\\n", "\\u000b", "\\f", "\\r",
65       "\\u000e", "\\u000f", "\\u0010", "\\u0011", "\\u0012", "\\u0013",  // 0x10
66       "\\u0014", "\\u0015", "\\u0016", "\\u0017", "\\u0018", "\\u0019", "\\u001a", "\\u001b",
67       "\\u001c", "\\u001d", "\\u001e", "\\u001f",
68       // Escaping of " and \ are required by www.json.org string definition.
69       // Escaping of < and > are required for HTML security.
70       "", "", "\\\"", "", "", "", "", "",                                            // 0x20
71       "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",                // 0x30
72       "", "", "", "", "\\u003c", "", "\\u003e", "", "", "", "", "", "", "", "", "",  // 0x40
73       "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",                // 0x50
74       "", "", "", "", "\\\\", "", "", "", "", "", "", "", "", "", "", "",            // 0x60
75       "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",                // 0x70
76       "", "", "", "", "", "", "", "\\u007f",
77       // C1 (ISO 8859 and Unicode) extended control characters
78       "\\u0080", "\\u0081", "\\u0082", "\\u0083",  // 0x80
79       "\\u0084", "\\u0085", "\\u0086", "\\u0087", "\\u0088", "\\u0089", "\\u008a", "\\u008b",
80       "\\u008c", "\\u008d", "\\u008e", "\\u008f", "\\u0090", "\\u0091", "\\u0092",
81       "\\u0093",  // 0x90
82       "\\u0094", "\\u0095", "\\u0096", "\\u0097", "\\u0098", "\\u0099", "\\u009a", "\\u009b",
83       "\\u009c", "\\u009d", "\\u009e", "\\u009f"
84     };
85 
JsonFormatter()86     static JsonFormatter() {
87       for (int i = 0; i < CommonRepresentations.Length; i++) {
88         if (CommonRepresentations[i] == "") {
89           CommonRepresentations[i] = ((char)i).ToString();
90         }
91       }
92     }
93 
94     private readonly Settings settings;
95 
96     private bool DiagnosticOnly => ReferenceEquals(this, diagnosticFormatter);
97 
98     /// <summary>
99     /// Creates a new formatted with the given settings.
100     /// </summary>
101     /// <param name="settings">The settings.</param>
JsonFormatter(Settings settings)102     public JsonFormatter(Settings settings) {
103       this.settings = ProtoPreconditions.CheckNotNull(settings, nameof(settings));
104     }
105 
106     /// <summary>
107     /// Formats the specified message as JSON.
108     /// </summary>
109     /// <param name="message">The message to format.</param>
110     /// <remarks>This method delegates to <c>Format(IMessage, int)</c> with <c>indentationLevel =
111     /// 0</c>.</remarks> <returns>The formatted message.</returns>
112     public string Format(IMessage message) => Format(message, indentationLevel: 0);
113 
114     /// <summary>
115     /// Formats the specified message as JSON.
116     /// </summary>
117     /// <param name="message">The message to format.</param>
118     /// <param name="indentationLevel">Indentation level to start at.</param>
119     /// <remarks>To keep consistent indentation when embedding a message inside another JSON string,
120     /// set <paramref name="indentationLevel"/>. E.g: <code> var response = $@"{{
121     ///   ""data"": { Format(message, indentationLevel: 1) }
122     /// }}"</code>
123     /// </remarks>
124     /// <returns>The formatted message.</returns>
Format(IMessage message, int indentationLevel)125     public string Format(IMessage message, int indentationLevel) {
126       var writer = new StringWriter();
127       Format(message, writer, indentationLevel);
128       return writer.ToString();
129     }
130 
131     /// <summary>
132     /// Formats the specified message as JSON.
133     /// </summary>
134     /// <param name="message">The message to format.</param>
135     /// <param name="writer">The TextWriter to write the formatted message to.</param>
136     /// <remarks>This method delegates to <c>Format(IMessage, TextWriter, int)</c> with
137     /// <c>indentationLevel = 0</c>.</remarks> <returns>The formatted message.</returns>
Format(IMessage message, TextWriter writer)138     public void Format(IMessage message, TextWriter writer) => Format(message, writer,
139                                                                       indentationLevel: 0);
140 
141     /// <summary>
142     /// Formats the specified message as JSON. When <see cref="Settings.Indentation"/> is not null,
143     /// start indenting at the specified <paramref name="indentationLevel"/>.
144     /// </summary>
145     /// <param name="message">The message to format.</param>
146     /// <param name="writer">The TextWriter to write the formatted message to.</param>
147     /// <param name="indentationLevel">Indentation level to start at.</param>
148     /// <remarks>To keep consistent indentation when embedding a message inside another JSON string,
149     /// set <paramref name="indentationLevel"/>.</remarks>
Format(IMessage message, TextWriter writer, int indentationLevel)150     public void Format(IMessage message, TextWriter writer, int indentationLevel) {
151       ProtoPreconditions.CheckNotNull(message, nameof(message));
152       ProtoPreconditions.CheckNotNull(writer, nameof(writer));
153 
154       if (message.Descriptor.IsWellKnownType) {
155         WriteWellKnownTypeValue(writer, message.Descriptor, message, indentationLevel);
156       } else {
157         WriteMessage(writer, message, indentationLevel);
158       }
159     }
160 
161     /// <summary>
162     /// Converts a message to JSON for diagnostic purposes with no extra context.
163     /// </summary>
164     /// <remarks>
165     /// <para>
166     /// This differs from calling <see cref="Format(IMessage)"/> on the default JSON
167     /// formatter in its handling of <see cref="Any"/>. As no type registry is available
168     /// in <see cref="object.ToString"/> calls, the normal way of resolving the type of
169     /// an <c>Any</c> message cannot be applied. Instead, a JSON property named <c>@value</c>
170     /// is included with the base64 data from the <see cref="Any.Value"/> property of the message.
171     /// </para>
172     /// <para>The value returned by this method is only designed to be used for diagnostic
173     /// purposes. It may not be parsable by <see cref="JsonParser"/>, and may not be parsable
174     /// by other Protocol Buffer implementations.</para>
175     /// </remarks>
176     /// <param name="message">The message to format for diagnostic purposes.</param>
177     /// <returns>The diagnostic-only JSON representation of the message</returns>
ToDiagnosticString(IMessage message)178     public static string ToDiagnosticString(IMessage message) {
179       ProtoPreconditions.CheckNotNull(message, nameof(message));
180       return diagnosticFormatter.Format(message);
181     }
182 
WriteMessage(TextWriter writer, IMessage message, int indentationLevel)183     private void WriteMessage(TextWriter writer, IMessage message, int indentationLevel) {
184       if (message == null) {
185         WriteNull(writer);
186         return;
187       }
188       if (DiagnosticOnly) {
189         if (message is ICustomDiagnosticMessage customDiagnosticMessage) {
190           writer.Write(customDiagnosticMessage.ToDiagnosticString());
191           return;
192         }
193       }
194 
195       WriteBracketOpen(writer, ObjectOpenBracket);
196       bool writtenFields = WriteMessageFields(writer, message, false, indentationLevel + 1);
197       WriteBracketClose(writer, ObjectCloseBracket, writtenFields, indentationLevel);
198     }
199 
WriteMessageFields(TextWriter writer, IMessage message, bool assumeFirstFieldWritten, int indentationLevel)200     private bool WriteMessageFields(TextWriter writer, IMessage message,
201                                     bool assumeFirstFieldWritten, int indentationLevel) {
202       var fields = message.Descriptor.Fields;
203       bool first = !assumeFirstFieldWritten;
204       // First non-oneof fields
205       foreach (var field in fields.InFieldNumberOrder()) {
206         var accessor = field.Accessor;
207         var value = accessor.GetValue(message);
208         if (!ShouldFormatFieldValue(message, field, value)) {
209           continue;
210         }
211 
212         MaybeWriteValueSeparator(writer, first);
213         MaybeWriteValueWhitespace(writer, indentationLevel);
214 
215         if (settings.PreserveProtoFieldNames) {
216           WriteString(writer, accessor.Descriptor.Name);
217         } else {
218           WriteString(writer, accessor.Descriptor.JsonName);
219         }
220         writer.Write(NameValueSeparator);
221         WriteValue(writer, value, indentationLevel);
222 
223         first = false;
224       }
225       return !first;
226     }
227 
MaybeWriteValueSeparator(TextWriter writer, bool first)228     private void MaybeWriteValueSeparator(TextWriter writer, bool first) {
229       if (first) {
230         return;
231       }
232 
233       writer.Write(settings.Indentation == null ? ValueSeparator : MultilineValueSeparator);
234     }
235 
236     /// <summary>
237     /// Determines whether or not a field value should be serialized according to the field,
238     /// its value in the message, and the settings of this formatter.
239     /// </summary>
ShouldFormatFieldValue(IMessage message, FieldDescriptor field, object value)240     private bool ShouldFormatFieldValue(IMessage message, FieldDescriptor field, object value) =>
241         field.HasPresence
242             // Fields that support presence *just* use that
243             ? field.Accessor.HasValue(message)
244             // Otherwise, format if either we've been asked to format default values, or if it's
245             // not a default value anyway.
246             : settings.FormatDefaultValues || !IsDefaultValue(field, value);
247 
248     // Converted from java/core/src/main/java/com/google/protobuf/Descriptors.java
ToJsonName(string name)249     internal static string ToJsonName(string name) {
250       StringBuilder result = new StringBuilder(name.Length);
251       bool isNextUpperCase = false;
252       foreach (char ch in name) {
253         if (ch == '_') {
254           isNextUpperCase = true;
255         } else if (isNextUpperCase) {
256           result.Append(char.ToUpperInvariant(ch));
257           isNextUpperCase = false;
258         } else {
259           result.Append(ch);
260         }
261       }
262       return result.ToString();
263     }
264 
FromJsonName(string name)265     internal static string FromJsonName(string name) {
266       StringBuilder result = new StringBuilder(name.Length);
267       foreach (char ch in name) {
268         if (char.IsUpper(ch)) {
269           result.Append('_');
270           result.Append(char.ToLowerInvariant(ch));
271         } else {
272           result.Append(ch);
273         }
274       }
275       return result.ToString();
276     }
277 
WriteNull(TextWriter writer)278     private static void WriteNull(TextWriter writer) {
279       writer.Write("null");
280     }
281 
IsDefaultValue(FieldDescriptor descriptor, object value)282     private static bool IsDefaultValue(FieldDescriptor descriptor, object value) {
283       if (descriptor.IsMap) {
284         IDictionary dictionary = (IDictionary)value;
285         return dictionary.Count == 0;
286       }
287       if (descriptor.IsRepeated) {
288         IList list = (IList)value;
289         return list.Count == 0;
290       }
291       return descriptor.FieldType switch {
292         FieldType.Bool => (bool)value == false,
293         FieldType.Bytes => (ByteString)value == ByteString.Empty,
294         FieldType.String => (string)value == "",
295         FieldType.Double => (double)value == 0.0,
296         FieldType.SInt32 or FieldType.Int32 or FieldType.SFixed32 or FieldType.Enum =>
297             (int)value == 0,
298         FieldType.Fixed32 or FieldType.UInt32 => (uint)value == 0,
299         FieldType.Fixed64 or FieldType.UInt64 => (ulong)value == 0,
300         FieldType.SFixed64 or FieldType.Int64 or FieldType.SInt64 => (long)value == 0,
301         FieldType.Float => (float)value == 0f,
302         FieldType.Message or FieldType.Group => value == null,
303         _ => throw new ArgumentException("Invalid field type"),
304       };
305     }
306 
307     /// <summary>
308     /// Writes a single value to the given writer as JSON. Only types understood by
309     /// Protocol Buffers can be written in this way. This method is only exposed for
310     /// advanced use cases; most users should be using <see cref="Format(IMessage)"/>
311     /// or <see cref="Format(IMessage, TextWriter)"/>.
312     /// </summary>
313     /// <param name="writer">The writer to write the value to. Must not be null.</param>
314     /// <param name="value">The value to write. May be null.</param>
315     /// <remarks>Delegates to <c>WriteValue(TextWriter, object, int)</c> with <c>indentationLevel =
316     /// 0</c>.</remarks>
WriteValue(TextWriter writer, object value)317     public void WriteValue(TextWriter writer, object value) => WriteValue(writer, value, 0);
318 
319     /// <summary>
320     /// Writes a single value to the given writer as JSON. Only types understood by
321     /// Protocol Buffers can be written in this way. This method is only exposed for
322     /// advanced use cases; most users should be using <see cref="Format(IMessage)"/>
323     /// or <see cref="Format(IMessage, TextWriter)"/>.
324     /// </summary>
325     /// <param name="writer">The writer to write the value to. Must not be null.</param>
326     /// <param name="value">The value to write. May be null.</param>
327     /// <param name="indentationLevel">The current indentationLevel. Not used when <see
328     /// cref="Settings.Indentation"/> is null.</param>
WriteValue(TextWriter writer, object value, int indentationLevel)329     public void WriteValue(TextWriter writer, object value, int indentationLevel) {
330       if (value == null || value is NullValue) {
331         WriteNull(writer);
332       } else if (value is bool b) {
333         writer.Write(b ? "true" : "false");
334       } else if (value is ByteString byteString) {
335         // Nothing in Base64 needs escaping
336         writer.Write('"');
337         writer.Write(byteString.ToBase64());
338         writer.Write('"');
339       } else if (value is string str) {
340         WriteString(writer, str);
341       } else if (value is IDictionary dictionary) {
342         WriteDictionary(writer, dictionary, indentationLevel);
343       } else if (value is IList list) {
344         WriteList(writer, list, indentationLevel);
345       } else if (value is int || value is uint) {
346         IFormattable formattable = (IFormattable)value;
347         writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
348       } else if (value is long || value is ulong) {
349         writer.Write('"');
350         IFormattable formattable = (IFormattable)value;
351         writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
352         writer.Write('"');
353       } else if (value is System.Enum) {
354         if (settings.FormatEnumsAsIntegers) {
355           WriteValue(writer, (int)value);
356         } else {
357           string name = OriginalEnumValueHelper.GetOriginalName(value);
358           if (name != null) {
359             WriteString(writer, name);
360           } else {
361             WriteValue(writer, (int)value);
362           }
363         }
364       } else if (value is float || value is double) {
365         string text = ((IFormattable)value).ToString("r", CultureInfo.InvariantCulture);
366         if (text == "NaN" || text == "Infinity" || text == "-Infinity") {
367           writer.Write('"');
368           writer.Write(text);
369           writer.Write('"');
370         } else {
371           writer.Write(text);
372         }
373       } else if (value is IMessage message) {
374         Format(message, writer, indentationLevel);
375       } else {
376         throw new ArgumentException("Unable to format value of type " + value.GetType());
377       }
378     }
379 
380     /// <summary>
381     /// Central interception point for well-known type formatting. Any well-known types which
382     /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
383     /// values are using the embedded well-known types, in order to allow for dynamic messages
384     /// in the future.
385     /// </summary>
WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value, int indentationLevel)386     private void WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor,
387                                          object value, int indentationLevel) {
388       // Currently, we can never actually get here, because null values are always handled by the
389       // caller. But if we *could*, this would do the right thing.
390       if (value == null) {
391         WriteNull(writer);
392         return;
393       }
394       // For wrapper types, the value will either be the (possibly boxed) "native" value,
395       // or the message itself if we're formatting it at the top level (e.g. just calling ToString
396       // on the object itself). If it's the message form, we can extract the value first, which
397       // *will* be the (possibly boxed) native value, and then proceed, writing it as if we were
398       // definitely in a field. (We never need to wrap it in an extra string... WriteValue will do
399       // the right thing.)
400       if (descriptor.IsWrapperType) {
401         if (value is IMessage message) {
402           value = message.Descriptor.Fields[WrappersReflection.WrapperValueFieldNumber]
403                       .Accessor.GetValue(message);
404         }
405         WriteValue(writer, value);
406         return;
407       }
408       if (descriptor.FullName == Timestamp.Descriptor.FullName) {
409         WriteTimestamp(writer, (IMessage)value);
410         return;
411       }
412       if (descriptor.FullName == Duration.Descriptor.FullName) {
413         WriteDuration(writer, (IMessage)value);
414         return;
415       }
416       if (descriptor.FullName == FieldMask.Descriptor.FullName) {
417         WriteFieldMask(writer, (IMessage)value);
418         return;
419       }
420       if (descriptor.FullName == Struct.Descriptor.FullName) {
421         WriteStruct(writer, (IMessage)value, indentationLevel);
422         return;
423       }
424       if (descriptor.FullName == ListValue.Descriptor.FullName) {
425         var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
426         WriteList(writer, (IList)fieldAccessor.GetValue((IMessage)value), indentationLevel);
427         return;
428       }
429       if (descriptor.FullName == Value.Descriptor.FullName) {
430         WriteStructFieldValue(writer, (IMessage)value, indentationLevel);
431         return;
432       }
433       if (descriptor.FullName == Any.Descriptor.FullName) {
434         WriteAny(writer, (IMessage)value, indentationLevel);
435         return;
436       }
437       WriteMessage(writer, (IMessage)value, indentationLevel);
438     }
439 
WriteTimestamp(TextWriter writer, IMessage value)440     private void WriteTimestamp(TextWriter writer, IMessage value) {
441       // TODO: In the common case where this *is* using the built-in Timestamp type, we could
442       // avoid all the reflection at this point, by casting to Timestamp. In the interests of
443       // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can
444       // prove it still works in that case.
445       int nanos = (int)value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
446       long seconds =
447           (long)value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
448       writer.Write(Timestamp.ToJson(seconds, nanos, DiagnosticOnly));
449     }
450 
WriteDuration(TextWriter writer, IMessage value)451     private void WriteDuration(TextWriter writer, IMessage value) {
452       // TODO: Same as for WriteTimestamp
453       int nanos = (int)value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
454       long seconds =
455           (long)value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
456       writer.Write(Duration.ToJson(seconds, nanos, DiagnosticOnly));
457     }
458 
WriteFieldMask(TextWriter writer, IMessage value)459     private void WriteFieldMask(TextWriter writer, IMessage value) {
460       var paths =
461           (IList<string>)value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(
462               value);
463       writer.Write(FieldMask.ToJson(paths, DiagnosticOnly));
464     }
465 
WriteAny(TextWriter writer, IMessage value, int indentationLevel)466     private void WriteAny(TextWriter writer, IMessage value, int indentationLevel) {
467       if (DiagnosticOnly) {
468         WriteDiagnosticOnlyAny(writer, value);
469         return;
470       }
471 
472       string typeUrl =
473           (string)value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
474       ByteString data =
475           (ByteString)value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
476       string typeName = Any.GetTypeName(typeUrl);
477       MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName);
478       if (descriptor == null) {
479         throw new InvalidOperationException(
480             $"Type registry has no descriptor for type name '{typeName}'");
481       }
482       IMessage message = descriptor.Parser.ParseFrom(data);
483       WriteBracketOpen(writer, ObjectOpenBracket);
484       MaybeWriteValueWhitespace(writer, indentationLevel + 1);
485       WriteString(writer, AnyTypeUrlField);
486       writer.Write(NameValueSeparator);
487       WriteString(writer, typeUrl);
488 
489       if (descriptor.IsWellKnownType) {
490         writer.Write(ValueSeparator);
491         WriteString(writer, AnyWellKnownTypeValueField);
492         writer.Write(NameValueSeparator);
493         WriteWellKnownTypeValue(writer, descriptor, message, indentationLevel + 1);
494       } else {
495         WriteMessageFields(writer, message, true, indentationLevel + 1);
496       }
497       WriteBracketClose(writer, ObjectCloseBracket, true, indentationLevel);
498     }
499 
WriteDiagnosticOnlyAny(TextWriter writer, IMessage value)500     private void WriteDiagnosticOnlyAny(TextWriter writer, IMessage value) {
501       string typeUrl =
502           (string)value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
503       ByteString data =
504           (ByteString)value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
505       writer.Write("{ ");
506       WriteString(writer, AnyTypeUrlField);
507       writer.Write(NameValueSeparator);
508       WriteString(writer, typeUrl);
509       writer.Write(ValueSeparator);
510       WriteString(writer, AnyDiagnosticValueField);
511       writer.Write(NameValueSeparator);
512       writer.Write('"');
513       writer.Write(data.ToBase64());
514       writer.Write('"');
515       writer.Write(" }");
516     }
517 
WriteStruct(TextWriter writer, IMessage message, int indentationLevel)518     private void WriteStruct(TextWriter writer, IMessage message, int indentationLevel) {
519       WriteBracketOpen(writer, ObjectOpenBracket);
520       IDictionary fields =
521           (IDictionary)message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(
522               message);
523       bool first = true;
524       foreach (DictionaryEntry entry in fields) {
525         string key = (string)entry.Key;
526         IMessage value = (IMessage)entry.Value;
527         if (string.IsNullOrEmpty(key) || value == null) {
528           throw new InvalidOperationException(
529               "Struct fields cannot have an empty key or a null value.");
530         }
531 
532         MaybeWriteValueSeparator(writer, first);
533         MaybeWriteValueWhitespace(writer, indentationLevel + 1);
534         WriteString(writer, key);
535         writer.Write(NameValueSeparator);
536         WriteStructFieldValue(writer, value, indentationLevel + 1);
537         first = false;
538       }
539       WriteBracketClose(writer, ObjectCloseBracket, !first, indentationLevel);
540     }
541 
WriteStructFieldValue(TextWriter writer, IMessage message, int indentationLevel)542     private void WriteStructFieldValue(TextWriter writer, IMessage message, int indentationLevel) {
543       var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
544       if (specifiedField == null) {
545         throw new InvalidOperationException("Value message must contain a value for the oneof.");
546       }
547 
548       object value = specifiedField.Accessor.GetValue(message);
549 
550       switch (specifiedField.FieldNumber) {
551         case Value.BoolValueFieldNumber:
552         case Value.StringValueFieldNumber:
553         case Value.NumberValueFieldNumber:
554           WriteValue(writer, value);
555           return;
556         case Value.StructValueFieldNumber:
557         case Value.ListValueFieldNumber:
558           // Structs and ListValues are nested messages, and already well-known types.
559           var nestedMessage = (IMessage)specifiedField.Accessor.GetValue(message);
560           WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage,
561                                   indentationLevel);
562           return;
563         case Value.NullValueFieldNumber:
564           WriteNull(writer);
565           return;
566         default:
567           throw new InvalidOperationException("Unexpected case in struct field: " +
568                                               specifiedField.FieldNumber);
569       }
570     }
571 
WriteList(TextWriter writer, IList list, int indentationLevel = 0)572     internal void WriteList(TextWriter writer, IList list, int indentationLevel = 0) {
573       WriteBracketOpen(writer, ListBracketOpen);
574 
575       bool first = true;
576       foreach (var value in list) {
577         MaybeWriteValueSeparator(writer, first);
578         MaybeWriteValueWhitespace(writer, indentationLevel + 1);
579         WriteValue(writer, value, indentationLevel + 1);
580         first = false;
581       }
582 
583       WriteBracketClose(writer, ListBracketClose, !first, indentationLevel);
584     }
585 
WriteDictionary(TextWriter writer, IDictionary dictionary, int indentationLevel = 0)586     internal void WriteDictionary(TextWriter writer, IDictionary dictionary,
587                                   int indentationLevel = 0) {
588       WriteBracketOpen(writer, ObjectOpenBracket);
589 
590       bool first = true;
591       // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of
592       // disposal.
593       foreach (DictionaryEntry pair in dictionary) {
594         string keyText;
595         if (pair.Key is string s) {
596           keyText = s;
597         } else if (pair.Key is bool b) {
598           keyText = b ? "true" : "false";
599         } else if (pair.Key is int || pair.Key is uint || pair.Key is long || pair.Key is ulong) {
600           keyText = ((IFormattable)pair.Key).ToString("d", CultureInfo.InvariantCulture);
601         } else {
602           if (pair.Key == null) {
603             throw new ArgumentException("Dictionary has entry with null key");
604           }
605           throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
606         }
607 
608         MaybeWriteValueSeparator(writer, first);
609         MaybeWriteValueWhitespace(writer, indentationLevel + 1);
610         WriteString(writer, keyText);
611         writer.Write(NameValueSeparator);
612         WriteValue(writer, pair.Value, indentationLevel + 1);
613         first = false;
614       }
615 
616       WriteBracketClose(writer, ObjectCloseBracket, !first, indentationLevel);
617     }
618 
619     /// <summary>
620     /// Writes a string (including leading and trailing double quotes) to a builder, escaping as
621     /// required.
622     /// </summary>
623     /// <remarks>
624     /// Other than surrogate pair handling, this code is mostly taken from
625     /// src/google/protobuf/util/internal/json_escaping.cc.
626     /// </remarks>
WriteString(TextWriter writer, string text)627     internal static void WriteString(TextWriter writer, string text) {
628       writer.Write('"');
629       for (int i = 0; i < text.Length; i++) {
630         char c = text[i];
631         if (c < 0xa0) {
632           writer.Write(CommonRepresentations[c]);
633           continue;
634         }
635         if (char.IsHighSurrogate(c)) {
636           // Encountered first part of a surrogate pair.
637           // Check that we have the whole pair, and encode both parts as hex.
638           i++;
639           if (i == text.Length || !char.IsLowSurrogate(text[i])) {
640             throw new ArgumentException(
641                 "String contains low surrogate not followed by high surrogate");
642           }
643           HexEncodeUtf16CodeUnit(writer, c);
644           HexEncodeUtf16CodeUnit(writer, text[i]);
645           continue;
646         } else if (char.IsLowSurrogate(c)) {
647           throw new ArgumentException(
648               "String contains high surrogate not preceded by low surrogate");
649         }
650         switch ((uint)c) {
651           // These are not required by json spec
652           // but used to prevent security bugs in javascript.
653           case 0xfeff:  // Zero width no-break space
654           case 0xfff9:  // Interlinear annotation anchor
655           case 0xfffa:  // Interlinear annotation separator
656           case 0xfffb:  // Interlinear annotation terminator
657 
658           case 0x00ad:  // Soft-hyphen
659           case 0x06dd:  // Arabic end of ayah
660           case 0x070f:  // Syriac abbreviation mark
661           case 0x17b4:  // Khmer vowel inherent Aq
662           case 0x17b5:  // Khmer vowel inherent Aa
663             HexEncodeUtf16CodeUnit(writer, c);
664             break;
665 
666           default:
667             if ((c >= 0x0600 && c <= 0x0603) ||  // Arabic signs
668                 (c >= 0x200b && c <= 0x200f) ||  // Zero width etc.
669                 (c >= 0x2028 && c <= 0x202e) ||  // Separators etc.
670                 (c >= 0x2060 && c <= 0x2064) ||  // Invisible etc.
671                 (c >= 0x206a && c <= 0x206f)) {
672               HexEncodeUtf16CodeUnit(writer, c);
673             } else {
674               // No handling of surrogates here - that's done earlier
675               writer.Write(c);
676             }
677             break;
678         }
679       }
680       writer.Write('"');
681     }
682 
683     private const string Hex = "0123456789abcdef";
HexEncodeUtf16CodeUnit(TextWriter writer, char c)684     private static void HexEncodeUtf16CodeUnit(TextWriter writer, char c) {
685       writer.Write("\\u");
686       writer.Write(Hex[(c >> 12) & 0xf]);
687       writer.Write(Hex[(c >> 8) & 0xf]);
688       writer.Write(Hex[(c >> 4) & 0xf]);
689       writer.Write(Hex[(c >> 0) & 0xf]);
690     }
691 
WriteBracketOpen(TextWriter writer, char openChar)692     private void WriteBracketOpen(TextWriter writer, char openChar) {
693       writer.Write(openChar);
694       if (settings.Indentation == null) {
695         writer.Write(' ');
696       }
697     }
698 
WriteBracketClose(TextWriter writer, char closeChar, bool hasFields, int indentationLevel)699     private void WriteBracketClose(TextWriter writer, char closeChar, bool hasFields,
700                                    int indentationLevel) {
701       if (hasFields) {
702         if (settings.Indentation != null) {
703           writer.WriteLine();
704           WriteIndentation(writer, indentationLevel);
705         } else {
706           writer.Write(" ");
707         }
708       }
709 
710       writer.Write(closeChar);
711     }
712 
MaybeWriteValueWhitespace(TextWriter writer, int indentationLevel)713     private void MaybeWriteValueWhitespace(TextWriter writer, int indentationLevel) {
714       if (settings.Indentation != null) {
715         writer.WriteLine();
716         WriteIndentation(writer, indentationLevel);
717       }
718     }
719 
WriteIndentation(TextWriter writer, int indentationLevel)720     private void WriteIndentation(TextWriter writer, int indentationLevel) {
721       for (int i = 0; i < indentationLevel; i++) {
722         writer.Write(settings.Indentation);
723       }
724     }
725 
726     /// <summary>
727     /// Settings controlling JSON formatting.
728     /// </summary>
729     public sealed class Settings {
730       /// <summary>
731       /// Default settings, as used by <see cref="JsonFormatter.Default"/>
732       /// </summary>
733       public static Settings Default { get; }
734 
735       // Workaround for the Mono compiler complaining about XML comments not being on
736       // valid language elements.
Settings()737       static Settings() {
738         Default = new Settings(false);
739       }
740 
741       /// <summary>
742       /// Whether fields which would otherwise not be included in the formatted data
743       /// should be formatted even when the value is not present, or has the default value.
744       /// This option only affects fields which don't support "presence" (e.g.
745       /// singular non-optional proto3 primitive fields).
746       /// </summary>
747       public bool FormatDefaultValues { get; }
748 
749       /// <summary>
750       /// The type registry used to format <see cref="Any"/> messages.
751       /// </summary>
752       public TypeRegistry TypeRegistry { get; }
753 
754       /// <summary>
755       /// Whether to format enums as ints. Defaults to false.
756       /// </summary>
757       public bool FormatEnumsAsIntegers { get; }
758 
759       /// <summary>
760       /// Whether to use the original proto field names as defined in the .proto file. Defaults to
761       /// false.
762       /// </summary>
763       public bool PreserveProtoFieldNames { get; }
764 
765       /// <summary>
766       /// Indentation string, used for formatting. Setting null disables indentation.
767       /// </summary>
768       public string Indentation { get; }
769 
770       /// <summary>
771       /// Creates a new <see cref="Settings"/> object with the specified formatting of default
772       /// values and an empty type registry.
773       /// </summary>
774       /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc)
775       /// should be formatted; <c>false</c> otherwise.</param>
Settings(bool formatDefaultValues)776       public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty) {}
777 
778       /// <summary>
779       /// Creates a new <see cref="Settings"/> object with the specified formatting of default
780       /// values and type registry.
781       /// </summary>
782       /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc)
783       /// should be formatted; <c>false</c> otherwise.</param> <param name="typeRegistry">The <see
784       /// cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
Settings(bool formatDefaultValues, TypeRegistry typeRegistry)785       public Settings(bool formatDefaultValues, TypeRegistry typeRegistry)
786           : this(formatDefaultValues, typeRegistry, false, false) {}
787 
788       /// <summary>
789       /// Creates a new <see cref="Settings"/> object with the specified parameters.
790       /// </summary>
791       /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc)
792       /// should be formatted; <c>false</c> otherwise.</param> <param name="typeRegistry">The <see
793       /// cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.
794       /// TypeRegistry.Empty will be used if it is null.</param> <param
795       /// name="formatEnumsAsIntegers"><c>true</c> to format the enums as integers; <c>false</c> to
796       /// format enums as enum names.</param> <param name="preserveProtoFieldNames"><c>true</c> to
797       /// preserve proto field names; <c>false</c> to convert them to lowerCamelCase.</param> <param
798       /// name="indentation">The indentation string to use for multi-line formatting. <c>null</c> to
799       /// disable multi-line format.</param>
Settings(bool formatDefaultValues, TypeRegistry typeRegistry, bool formatEnumsAsIntegers, bool preserveProtoFieldNames, string indentation = null)800       private Settings(bool formatDefaultValues, TypeRegistry typeRegistry,
801                        bool formatEnumsAsIntegers, bool preserveProtoFieldNames,
802                        string indentation = null) {
803         FormatDefaultValues = formatDefaultValues;
804         TypeRegistry = typeRegistry ?? TypeRegistry.Empty;
805         FormatEnumsAsIntegers = formatEnumsAsIntegers;
806         PreserveProtoFieldNames = preserveProtoFieldNames;
807         Indentation = indentation;
808       }
809 
810       /// <summary>
811       /// Creates a new <see cref="Settings"/> object with the specified formatting of default
812       /// values and the current settings.
813       /// </summary>
814       /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc)
815       /// should be formatted; <c>false</c> otherwise.</param>
816       public Settings WithFormatDefaultValues(bool formatDefaultValues) =>
817           new Settings(formatDefaultValues, TypeRegistry, FormatEnumsAsIntegers,
818                        PreserveProtoFieldNames, Indentation);
819 
820       /// <summary>
821       /// Creates a new <see cref="Settings"/> object with the specified type registry and the
822       /// current settings.
823       /// </summary>
824       /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see
825       /// cref="Any"/> messages.</param>
826       public Settings WithTypeRegistry(TypeRegistry typeRegistry) =>
827           new Settings(FormatDefaultValues, typeRegistry, FormatEnumsAsIntegers,
828                        PreserveProtoFieldNames, Indentation);
829 
830       /// <summary>
831       /// Creates a new <see cref="Settings"/> object with the specified enums formatting option and
832       /// the current settings.
833       /// </summary>
834       /// <param name="formatEnumsAsIntegers"><c>true</c> to format the enums as integers;
835       /// <c>false</c> to format enums as enum names.</param>
836       public Settings WithFormatEnumsAsIntegers(bool formatEnumsAsIntegers) =>
837           new Settings(FormatDefaultValues, TypeRegistry, formatEnumsAsIntegers,
838                        PreserveProtoFieldNames, Indentation);
839 
840       /// <summary>
841       /// Creates a new <see cref="Settings"/> object with the specified field name formatting
842       /// option and the current settings.
843       /// </summary>
844       /// <param name="preserveProtoFieldNames"><c>true</c> to preserve proto field names;
845       /// <c>false</c> to convert them to lowerCamelCase.</param>
846       public Settings WithPreserveProtoFieldNames(bool preserveProtoFieldNames) =>
847           new Settings(FormatDefaultValues, TypeRegistry, FormatEnumsAsIntegers,
848                        preserveProtoFieldNames, Indentation);
849 
850       /// <summary>
851       /// Creates a new <see cref="Settings"/> object with the specified indentation and the current
852       /// settings.
853       /// </summary>
854       /// <param name="indentation">The string to output for each level of indentation (nesting).
855       /// The default is two spaces per level. Use null to disable indentation entirely.</param>
856       /// <remarks>A non-null value for <see cref="Indentation"/> will insert additional line-breaks
857       /// to the JSON output. Each line will contain either a single value, or braces. The default
858       /// line-break is determined by <see cref="Environment.NewLine"/>, which is <c>"\n"</c> on
859       /// Unix platforms, and <c>"\r\n"</c> on Windows. If <see cref="JsonFormatter"/> seems to
860       /// produce empty lines, you need to pass a <see cref="TextWriter"/> that uses a <c>"\n"</c>
861       /// newline. See <see cref="JsonFormatter.Format(Google.Protobuf.IMessage, TextWriter)"/>.
862       /// </remarks>
WithIndentation(string indentation = "  ")863       public Settings WithIndentation(string indentation = "  ") =>
864           new Settings(FormatDefaultValues, TypeRegistry, FormatEnumsAsIntegers,
865                        PreserveProtoFieldNames, indentation);
866     }
867 
868     // Effectively a cache of mapping from enum values to the original name as specified in the
869     // proto file, fetched by reflection. The need for this is unfortunate, as is its unbounded
870     // size, but realistically it shouldn't cause issues.
871     private static class OriginalEnumValueHelper {
872       private static readonly ConcurrentDictionary<System.Type, Dictionary<object, string>>
873           dictionaries = new ConcurrentDictionary<System.Type, Dictionary<object, string>>();
874 
875       [UnconditionalSuppressMessage(
876           "Trimming", "IL2072",
877           Justification =
878               "The field for the value must still be present. It will be returned by reflection, will be in this collection, and its name can be resolved.")]
879       [UnconditionalSuppressMessage(
880           "Trimming", "IL2067",
881           Justification =
882               "The field for the value must still be present. It will be returned by reflection, will be in this collection, and its name can be resolved.")]
GetOriginalName(object value)883       internal static string GetOriginalName(object value) {
884         // Warnings are suppressed on this method. However, this code has been tested in an AOT app
885         // and verified that it works. Issue
886         // https://github.com/protocolbuffers/protobuf/issues/14788 discusses changes to guarantee
887         // that enum fields are never trimmed.
888         Dictionary<object, string> nameMapping =
889             dictionaries.GetOrAdd(value.GetType(), static t => GetNameMapping(t));
890 
891         // If this returns false, originalName will be null, which is what we want.
892         nameMapping.TryGetValue(value, out string originalName);
893         return originalName;
894       }
895 
GetNameMapping([ DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields) ] System.Type enumType)896       private static Dictionary<object, string> GetNameMapping([
897         DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields |
898                                    DynamicallyAccessedMemberTypes.NonPublicFields)
899       ] System.Type enumType) {
900         return enumType.GetTypeInfo()
901             .DeclaredFields.Where(f => f.IsStatic)
902             .Where(f => f.GetCustomAttributes<OriginalNameAttribute>()
903                             .FirstOrDefault()
904                             ?.PreferredAlias ??
905                         true)
906             .ToDictionary(
907                 f => f.GetValue(null),
908                 f =>
909                     f.GetCustomAttributes<OriginalNameAttribute>()
910                         .FirstOrDefault()
911                         // If the attribute hasn't been applied, fall back to the name of the field.
912                         ?.Name ??
913                     f.Name);
914       }
915     }
916   }
917 }
918