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
33using System;
34using System.Collections;
35using System.Globalization;
36using System.Text;
37using Google.Protobuf.Reflection;
38using Google.Protobuf.WellKnownTypes;
39using System.IO;
40using System.Linq;
41using System.Collections.Generic;
42using System.Reflection;
43
44namespace 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
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>
134        public JsonFormatter(Settings settings)
135        {
136            this.settings = ProtoPreconditions.CheckNotNull(settings, nameof(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>
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>
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>
189        public static string ToDiagnosticString(IMessage message)
190        {
191            ProtoPreconditions.CheckNotNull(message, nameof(message));
192            return diagnosticFormatter.Format(message);
193        }
194
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
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                var value = accessor.GetValue(message);
225                if (!ShouldFormatFieldValue(message, field, value))
226                {
227                    continue;
228                }
229
230                if (!first)
231                {
232                    writer.Write(PropertySeparator);
233                }
234
235                WriteString(writer, accessor.Descriptor.JsonName);
236                writer.Write(NameValueSeparator);
237                WriteValue(writer, value);
238
239                first = false;
240            }
241            return !first;
242        }
243
244        /// <summary>
245        /// Determines whether or not a field value should be serialized according to the field,
246        /// its value in the message, and the settings of this formatter.
247        /// </summary>
248        private bool ShouldFormatFieldValue(IMessage message, FieldDescriptor field, object value) =>
249            field.HasPresence
250            // Fields that support presence *just* use that
251            ? field.Accessor.HasValue(message)
252            // Otherwise, format if either we've been asked to format default values, or if it's
253            // not a default value anyway.
254            : settings.FormatDefaultValues || !IsDefaultValue(field, value);
255
256        // Converted from java/core/src/main/java/com/google/protobuf/Descriptors.java
257        internal static string ToJsonName(string name)
258        {
259            StringBuilder result = new StringBuilder(name.Length);
260            bool isNextUpperCase = false;
261            foreach (char ch in name)
262            {
263                if (ch == '_')
264                {
265                    isNextUpperCase = true;
266                }
267                else if (isNextUpperCase)
268                {
269                    result.Append(char.ToUpperInvariant(ch));
270                    isNextUpperCase = false;
271                }
272                else
273                {
274                    result.Append(ch);
275                }
276            }
277            return result.ToString();
278        }
279
280        internal static string FromJsonName(string name)
281        {
282            StringBuilder result = new StringBuilder(name.Length);
283            foreach (char ch in name)
284            {
285                if (char.IsUpper(ch))
286                {
287                    result.Append('_');
288                    result.Append(char.ToLowerInvariant(ch));
289                }
290                else
291                {
292                    result.Append(ch);
293                }
294            }
295            return result.ToString();
296        }
297
298        private static void WriteNull(TextWriter writer)
299        {
300            writer.Write("null");
301        }
302
303        private static bool IsDefaultValue(FieldDescriptor descriptor, object value)
304        {
305            if (descriptor.IsMap)
306            {
307                IDictionary dictionary = (IDictionary) value;
308                return dictionary.Count == 0;
309            }
310            if (descriptor.IsRepeated)
311            {
312                IList list = (IList) value;
313                return list.Count == 0;
314            }
315            switch (descriptor.FieldType)
316            {
317                case FieldType.Bool:
318                    return (bool) value == false;
319                case FieldType.Bytes:
320                    return (ByteString) value == ByteString.Empty;
321                case FieldType.String:
322                    return (string) value == "";
323                case FieldType.Double:
324                    return (double) value == 0.0;
325                case FieldType.SInt32:
326                case FieldType.Int32:
327                case FieldType.SFixed32:
328                case FieldType.Enum:
329                    return (int) value == 0;
330                case FieldType.Fixed32:
331                case FieldType.UInt32:
332                    return (uint) value == 0;
333                case FieldType.Fixed64:
334                case FieldType.UInt64:
335                    return (ulong) value == 0;
336                case FieldType.SFixed64:
337                case FieldType.Int64:
338                case FieldType.SInt64:
339                    return (long) value == 0;
340                case FieldType.Float:
341                    return (float) value == 0f;
342                case FieldType.Message:
343                case FieldType.Group: // Never expect to get this, but...
344                    return value == null;
345                default:
346                    throw new ArgumentException("Invalid field type");
347            }
348        }
349
350        /// <summary>
351        /// Writes a single value to the given writer as JSON. Only types understood by
352        /// Protocol Buffers can be written in this way. This method is only exposed for
353        /// advanced use cases; most users should be using <see cref="Format(IMessage)"/>
354        /// or <see cref="Format(IMessage, TextWriter)"/>.
355        /// </summary>
356        /// <param name="writer">The writer to write the value to. Must not be null.</param>
357        /// <param name="value">The value to write. May be null.</param>
358        public void WriteValue(TextWriter writer, object value)
359        {
360            if (value == null || value is NullValue)
361            {
362                WriteNull(writer);
363            }
364            else if (value is bool)
365            {
366                writer.Write((bool)value ? "true" : "false");
367            }
368            else if (value is ByteString)
369            {
370                // Nothing in Base64 needs escaping
371                writer.Write('"');
372                writer.Write(((ByteString)value).ToBase64());
373                writer.Write('"');
374            }
375            else if (value is string)
376            {
377                WriteString(writer, (string)value);
378            }
379            else if (value is IDictionary)
380            {
381                WriteDictionary(writer, (IDictionary)value);
382            }
383            else if (value is IList)
384            {
385                WriteList(writer, (IList)value);
386            }
387            else if (value is int || value is uint)
388            {
389                IFormattable formattable = (IFormattable) value;
390                writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
391            }
392            else if (value is long || value is ulong)
393            {
394                writer.Write('"');
395                IFormattable formattable = (IFormattable) value;
396                writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture));
397                writer.Write('"');
398            }
399            else if (value is System.Enum)
400            {
401                if (settings.FormatEnumsAsIntegers)
402                {
403                    WriteValue(writer, (int)value);
404                }
405                else
406                {
407                    string name = OriginalEnumValueHelper.GetOriginalName(value);
408                    if (name != null)
409                    {
410                        WriteString(writer, name);
411                    }
412                    else
413                    {
414                        WriteValue(writer, (int)value);
415                    }
416                }
417            }
418            else if (value is float || value is double)
419            {
420                string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture);
421                if (text == "NaN" || text == "Infinity" || text == "-Infinity")
422                {
423                    writer.Write('"');
424                    writer.Write(text);
425                    writer.Write('"');
426                }
427                else
428                {
429                    writer.Write(text);
430                }
431            }
432            else if (value is IMessage)
433            {
434                Format((IMessage)value, writer);
435            }
436            else
437            {
438                throw new ArgumentException("Unable to format value of type " + value.GetType());
439            }
440        }
441
442        /// <summary>
443        /// Central interception point for well-known type formatting. Any well-known types which
444        /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
445        /// values are using the embedded well-known types, in order to allow for dynamic messages
446        /// in the future.
447        /// </summary>
448        private void WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value)
449        {
450            // Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*,
451            // this would do the right thing.
452            if (value == null)
453            {
454                WriteNull(writer);
455                return;
456            }
457            // For wrapper types, the value will either be the (possibly boxed) "native" value,
458            // or the message itself if we're formatting it at the top level (e.g. just calling ToString on the object itself).
459            // If it's the message form, we can extract the value first, which *will* be the (possibly boxed) native value,
460            // and then proceed, writing it as if we were definitely in a field. (We never need to wrap it in an extra string...
461            // WriteValue will do the right thing.)
462            if (descriptor.IsWrapperType)
463            {
464                if (value is IMessage)
465                {
466                    var message = (IMessage) value;
467                    value = message.Descriptor.Fields[WrappersReflection.WrapperValueFieldNumber].Accessor.GetValue(message);
468                }
469                WriteValue(writer, value);
470                return;
471            }
472            if (descriptor.FullName == Timestamp.Descriptor.FullName)
473            {
474                WriteTimestamp(writer, (IMessage)value);
475                return;
476            }
477            if (descriptor.FullName == Duration.Descriptor.FullName)
478            {
479                WriteDuration(writer, (IMessage)value);
480                return;
481            }
482            if (descriptor.FullName == FieldMask.Descriptor.FullName)
483            {
484                WriteFieldMask(writer, (IMessage)value);
485                return;
486            }
487            if (descriptor.FullName == Struct.Descriptor.FullName)
488            {
489                WriteStruct(writer, (IMessage)value);
490                return;
491            }
492            if (descriptor.FullName == ListValue.Descriptor.FullName)
493            {
494                var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
495                WriteList(writer, (IList)fieldAccessor.GetValue((IMessage)value));
496                return;
497            }
498            if (descriptor.FullName == Value.Descriptor.FullName)
499            {
500                WriteStructFieldValue(writer, (IMessage)value);
501                return;
502            }
503            if (descriptor.FullName == Any.Descriptor.FullName)
504            {
505                WriteAny(writer, (IMessage)value);
506                return;
507            }
508            WriteMessage(writer, (IMessage)value);
509        }
510
511        private void WriteTimestamp(TextWriter writer, IMessage value)
512        {
513            // TODO: In the common case where this *is* using the built-in Timestamp type, we could
514            // avoid all the reflection at this point, by casting to Timestamp. In the interests of
515            // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
516            // it still works in that case.
517            int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value);
518            long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value);
519            writer.Write(Timestamp.ToJson(seconds, nanos, DiagnosticOnly));
520        }
521
522        private void WriteDuration(TextWriter writer, IMessage value)
523        {
524            // TODO: Same as for WriteTimestamp
525            int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
526            long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value);
527            writer.Write(Duration.ToJson(seconds, nanos, DiagnosticOnly));
528        }
529
530        private void WriteFieldMask(TextWriter writer, IMessage value)
531        {
532            var paths = (IList<string>) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
533            writer.Write(FieldMask.ToJson(paths, DiagnosticOnly));
534        }
535
536        private void WriteAny(TextWriter writer, IMessage value)
537        {
538            if (DiagnosticOnly)
539            {
540                WriteDiagnosticOnlyAny(writer, value);
541                return;
542            }
543
544            string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
545            ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
546            string typeName = Any.GetTypeName(typeUrl);
547            MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName);
548            if (descriptor == null)
549            {
550                throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'");
551            }
552            IMessage message = descriptor.Parser.ParseFrom(data);
553            writer.Write("{ ");
554            WriteString(writer, AnyTypeUrlField);
555            writer.Write(NameValueSeparator);
556            WriteString(writer, typeUrl);
557
558            if (descriptor.IsWellKnownType)
559            {
560                writer.Write(PropertySeparator);
561                WriteString(writer, AnyWellKnownTypeValueField);
562                writer.Write(NameValueSeparator);
563                WriteWellKnownTypeValue(writer, descriptor, message);
564            }
565            else
566            {
567                WriteMessageFields(writer, message, true);
568            }
569            writer.Write(" }");
570        }
571
572        private void WriteDiagnosticOnlyAny(TextWriter writer, IMessage value)
573        {
574            string typeUrl = (string) value.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.GetValue(value);
575            ByteString data = (ByteString) value.Descriptor.Fields[Any.ValueFieldNumber].Accessor.GetValue(value);
576            writer.Write("{ ");
577            WriteString(writer, AnyTypeUrlField);
578            writer.Write(NameValueSeparator);
579            WriteString(writer, typeUrl);
580            writer.Write(PropertySeparator);
581            WriteString(writer, AnyDiagnosticValueField);
582            writer.Write(NameValueSeparator);
583            writer.Write('"');
584            writer.Write(data.ToBase64());
585            writer.Write('"');
586            writer.Write(" }");
587        }
588
589        private void WriteStruct(TextWriter writer, IMessage message)
590        {
591            writer.Write("{ ");
592            IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
593            bool first = true;
594            foreach (DictionaryEntry entry in fields)
595            {
596                string key = (string) entry.Key;
597                IMessage value = (IMessage) entry.Value;
598                if (string.IsNullOrEmpty(key) || value == null)
599                {
600                    throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
601                }
602
603                if (!first)
604                {
605                    writer.Write(PropertySeparator);
606                }
607                WriteString(writer, key);
608                writer.Write(NameValueSeparator);
609                WriteStructFieldValue(writer, value);
610                first = false;
611            }
612            writer.Write(first ? "}" : " }");
613        }
614
615        private void WriteStructFieldValue(TextWriter writer, IMessage message)
616        {
617            var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
618            if (specifiedField == null)
619            {
620                throw new InvalidOperationException("Value message must contain a value for the oneof.");
621            }
622
623            object value = specifiedField.Accessor.GetValue(message);
624
625            switch (specifiedField.FieldNumber)
626            {
627                case Value.BoolValueFieldNumber:
628                case Value.StringValueFieldNumber:
629                case Value.NumberValueFieldNumber:
630                    WriteValue(writer, value);
631                    return;
632                case Value.StructValueFieldNumber:
633                case Value.ListValueFieldNumber:
634                    // Structs and ListValues are nested messages, and already well-known types.
635                    var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
636                    WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage);
637                    return;
638                case Value.NullValueFieldNumber:
639                    WriteNull(writer);
640                    return;
641                default:
642                    throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
643            }
644        }
645
646        internal void WriteList(TextWriter writer, IList list)
647        {
648            writer.Write("[ ");
649            bool first = true;
650            foreach (var value in list)
651            {
652                if (!first)
653                {
654                    writer.Write(PropertySeparator);
655                }
656                WriteValue(writer, value);
657                first = false;
658            }
659            writer.Write(first ? "]" : " ]");
660        }
661
662        internal void WriteDictionary(TextWriter writer, IDictionary dictionary)
663        {
664            writer.Write("{ ");
665            bool first = true;
666            // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal.
667            foreach (DictionaryEntry pair in dictionary)
668            {
669                if (!first)
670                {
671                    writer.Write(PropertySeparator);
672                }
673                string keyText;
674                if (pair.Key is string)
675                {
676                    keyText = (string) pair.Key;
677                }
678                else if (pair.Key is bool)
679                {
680                    keyText = (bool) pair.Key ? "true" : "false";
681                }
682                else if (pair.Key is int || pair.Key is uint | pair.Key is long || pair.Key is ulong)
683                {
684                    keyText = ((IFormattable) pair.Key).ToString("d", CultureInfo.InvariantCulture);
685                }
686                else
687                {
688                    if (pair.Key == null)
689                    {
690                        throw new ArgumentException("Dictionary has entry with null key");
691                    }
692                    throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType());
693                }
694                WriteString(writer, keyText);
695                writer.Write(NameValueSeparator);
696                WriteValue(writer, pair.Value);
697                first = false;
698            }
699            writer.Write(first ? "}" : " }");
700        }
701
702        /// <summary>
703        /// Writes a string (including leading and trailing double quotes) to a builder, escaping as required.
704        /// </summary>
705        /// <remarks>
706        /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc.
707        /// </remarks>
708        internal static void WriteString(TextWriter writer, string text)
709        {
710            writer.Write('"');
711            for (int i = 0; i < text.Length; i++)
712            {
713                char c = text[i];
714                if (c < 0xa0)
715                {
716                    writer.Write(CommonRepresentations[c]);
717                    continue;
718                }
719                if (char.IsHighSurrogate(c))
720                {
721                    // Encountered first part of a surrogate pair.
722                    // Check that we have the whole pair, and encode both parts as hex.
723                    i++;
724                    if (i == text.Length || !char.IsLowSurrogate(text[i]))
725                    {
726                        throw new ArgumentException("String contains low surrogate not followed by high surrogate");
727                    }
728                    HexEncodeUtf16CodeUnit(writer, c);
729                    HexEncodeUtf16CodeUnit(writer, text[i]);
730                    continue;
731                }
732                else if (char.IsLowSurrogate(c))
733                {
734                    throw new ArgumentException("String contains high surrogate not preceded by low surrogate");
735                }
736                switch ((uint) c)
737                {
738                    // These are not required by json spec
739                    // but used to prevent security bugs in javascript.
740                    case 0xfeff:  // Zero width no-break space
741                    case 0xfff9:  // Interlinear annotation anchor
742                    case 0xfffa:  // Interlinear annotation separator
743                    case 0xfffb:  // Interlinear annotation terminator
744
745                    case 0x00ad:  // Soft-hyphen
746                    case 0x06dd:  // Arabic end of ayah
747                    case 0x070f:  // Syriac abbreviation mark
748                    case 0x17b4:  // Khmer vowel inherent Aq
749                    case 0x17b5:  // Khmer vowel inherent Aa
750                        HexEncodeUtf16CodeUnit(writer, c);
751                        break;
752
753                    default:
754                        if ((c >= 0x0600 && c <= 0x0603) ||  // Arabic signs
755                            (c >= 0x200b && c <= 0x200f) ||  // Zero width etc.
756                            (c >= 0x2028 && c <= 0x202e) ||  // Separators etc.
757                            (c >= 0x2060 && c <= 0x2064) ||  // Invisible etc.
758                            (c >= 0x206a && c <= 0x206f))
759                        {
760                            HexEncodeUtf16CodeUnit(writer, c);
761                        }
762                        else
763                        {
764                            // No handling of surrogates here - that's done earlier
765                            writer.Write(c);
766                        }
767                        break;
768                }
769            }
770            writer.Write('"');
771        }
772
773        private const string Hex = "0123456789abcdef";
774        private static void HexEncodeUtf16CodeUnit(TextWriter writer, char c)
775        {
776            writer.Write("\\u");
777            writer.Write(Hex[(c >> 12) & 0xf]);
778            writer.Write(Hex[(c >> 8) & 0xf]);
779            writer.Write(Hex[(c >> 4) & 0xf]);
780            writer.Write(Hex[(c >> 0) & 0xf]);
781        }
782
783        /// <summary>
784        /// Settings controlling JSON formatting.
785        /// </summary>
786        public sealed class Settings
787        {
788            /// <summary>
789            /// Default settings, as used by <see cref="JsonFormatter.Default"/>
790            /// </summary>
791            public static Settings Default { get; }
792
793            // Workaround for the Mono compiler complaining about XML comments not being on
794            // valid language elements.
795            static Settings()
796            {
797                Default = new Settings(false);
798            }
799
800            /// <summary>
801            /// Whether fields which would otherwise not be included in the formatted data
802            /// should be formatted even when the value is not present, or has the default value.
803            /// This option only affects fields which don't support "presence" (e.g.
804            /// singular non-optional proto3 primitive fields).
805            /// </summary>
806            public bool FormatDefaultValues { get; }
807
808            /// <summary>
809            /// The type registry used to format <see cref="Any"/> messages.
810            /// </summary>
811            public TypeRegistry TypeRegistry { get; }
812
813            /// <summary>
814            /// Whether to format enums as ints. Defaults to false.
815            /// </summary>
816            public bool FormatEnumsAsIntegers { get; }
817
818
819            /// <summary>
820            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
821            /// and an empty type registry.
822            /// </summary>
823            /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
824            public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty)
825            {
826            }
827
828            /// <summary>
829            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values
830            /// and type registry.
831            /// </summary>
832            /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
833            /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
834            public Settings(bool formatDefaultValues, TypeRegistry typeRegistry) : this(formatDefaultValues, typeRegistry, false)
835            {
836            }
837
838            /// <summary>
839            /// Creates a new <see cref="Settings"/> object with the specified parameters.
840            /// </summary>
841            /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
842            /// <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>
843            /// <param name="formatEnumsAsIntegers"><c>true</c> to format the enums as integers; <c>false</c> to format enums as enum names.</param>
844            private Settings(bool formatDefaultValues,
845                            TypeRegistry typeRegistry,
846                            bool formatEnumsAsIntegers)
847            {
848                FormatDefaultValues = formatDefaultValues;
849                TypeRegistry = typeRegistry ?? TypeRegistry.Empty;
850                FormatEnumsAsIntegers = formatEnumsAsIntegers;
851            }
852
853            /// <summary>
854            /// Creates a new <see cref="Settings"/> object with the specified formatting of default values and the current settings.
855            /// </summary>
856            /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param>
857            public Settings WithFormatDefaultValues(bool formatDefaultValues) => new Settings(formatDefaultValues, TypeRegistry, FormatEnumsAsIntegers);
858
859            /// <summary>
860            /// Creates a new <see cref="Settings"/> object with the specified type registry and the current settings.
861            /// </summary>
862            /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param>
863            public Settings WithTypeRegistry(TypeRegistry typeRegistry) => new Settings(FormatDefaultValues, typeRegistry, FormatEnumsAsIntegers);
864
865            /// <summary>
866            /// Creates a new <see cref="Settings"/> object with the specified enums formatting option and the current settings.
867            /// </summary>
868            /// <param name="formatEnumsAsIntegers"><c>true</c> to format the enums as integers; <c>false</c> to format enums as enum names.</param>
869            public Settings WithFormatEnumsAsIntegers(bool formatEnumsAsIntegers) => new Settings(FormatDefaultValues, TypeRegistry, formatEnumsAsIntegers);
870        }
871
872        // Effectively a cache of mapping from enum values to the original name as specified in the proto file,
873        // fetched by reflection.
874        // The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues.
875        private static class OriginalEnumValueHelper
876        {
877            // TODO: In the future we might want to use ConcurrentDictionary, at the point where all
878            // the platforms we target have it.
879            private static readonly Dictionary<System.Type, Dictionary<object, string>> dictionaries
880                = new Dictionary<System.Type, Dictionary<object, string>>();
881
882            internal static string GetOriginalName(object value)
883            {
884                var enumType = value.GetType();
885                Dictionary<object, string> nameMapping;
886                lock (dictionaries)
887                {
888                    if (!dictionaries.TryGetValue(enumType, out nameMapping))
889                    {
890                        nameMapping = GetNameMapping(enumType);
891                        dictionaries[enumType] = nameMapping;
892                    }
893                }
894
895                string originalName;
896                // If this returns false, originalName will be null, which is what we want.
897                nameMapping.TryGetValue(value, out originalName);
898                return originalName;
899            }
900
901#if NET35
902            // TODO: Consider adding functionality to TypeExtensions to avoid this difference.
903            private static Dictionary<object, string> GetNameMapping(System.Type enumType) =>
904                enumType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
905                    .Where(f => (f.GetCustomAttributes(typeof(OriginalNameAttribute), false)
906                                 .FirstOrDefault() as OriginalNameAttribute)
907                                 ?.PreferredAlias ?? true)
908                    .ToDictionary(f => f.GetValue(null),
909                                  f => (f.GetCustomAttributes(typeof(OriginalNameAttribute), false)
910                                        .FirstOrDefault() as OriginalNameAttribute)
911                                        // If the attribute hasn't been applied, fall back to the name of the field.
912                                        ?.Name ?? f.Name);
913#else
914            private static Dictionary<object, string> GetNameMapping(System.Type enumType) =>
915                enumType.GetTypeInfo().DeclaredFields
916                    .Where(f => f.IsStatic)
917                    .Where(f => f.GetCustomAttributes<OriginalNameAttribute>()
918                                 .FirstOrDefault()?.PreferredAlias ?? true)
919                    .ToDictionary(f => f.GetValue(null),
920                                  f => f.GetCustomAttributes<OriginalNameAttribute>()
921                                        .FirstOrDefault()
922                                        // If the attribute hasn't been applied, fall back to the name of the field.
923                                        ?.Name ?? f.Name);
924#endif
925        }
926    }
927}
928