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 = 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> 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 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> ShouldFormatFieldValue(IMessage message, FieldDescriptor field, object value)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 ToJsonName(string name)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 FromJsonName(string name)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 WriteNull(TextWriter writer)298 private static void WriteNull(TextWriter writer) 299 { 300 writer.Write("null"); 301 } 302 IsDefaultValue(FieldDescriptor descriptor, object value)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> WriteValue(TextWriter writer, object value)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> WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value)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 WriteTimestamp(TextWriter writer, IMessage value)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 WriteDuration(TextWriter writer, IMessage value)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 WriteFieldMask(TextWriter writer, IMessage value)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 WriteAny(TextWriter writer, IMessage value)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 WriteDiagnosticOnlyAny(TextWriter writer, IMessage value)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 WriteStruct(TextWriter writer, IMessage message)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 WriteStructFieldValue(TextWriter writer, IMessage message)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 WriteList(TextWriter writer, IList list)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 WriteDictionary(TextWriter writer, IDictionary dictionary)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> WriteString(TextWriter writer, string text)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"; HexEncodeUtf16CodeUnit(TextWriter writer, char c)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. Settings()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> Settings(bool formatDefaultValues)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> Settings(bool formatDefaultValues, TypeRegistry typeRegistry)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> Settings(bool formatDefaultValues, TypeRegistry typeRegistry, bool formatEnumsAsIntegers)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 GetOriginalName(object value)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