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