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