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