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