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