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 381 /// <summary> 382 /// Writes a single value to the given writer as JSON. Only types understood by 383 /// Protocol Buffers can be written in this way. This method is only exposed for 384 /// advanced use cases; most users should be using <see cref="Format(IMessage)"/> 385 /// or <see cref="Format(IMessage, TextWriter)"/>. 386 /// </summary> 387 /// <param name="writer">The writer to write the value to. Must not be null.</param> 388 /// <param name="value">The value to write. May be null.</param> WriteValue(TextWriter writer, object value)389 public void WriteValue(TextWriter writer, object value) 390 { 391 if (value == null) 392 { 393 WriteNull(writer); 394 } 395 else if (value is bool) 396 { 397 writer.Write((bool)value ? "true" : "false"); 398 } 399 else if (value is ByteString) 400 { 401 // Nothing in Base64 needs escaping 402 writer.Write('"'); 403 writer.Write(((ByteString)value).ToBase64()); 404 writer.Write('"'); 405 } 406 else if (value is string) 407 { 408 WriteString(writer, (string)value); 409 } 410 else if (value is IDictionary) 411 { 412 WriteDictionary(writer, (IDictionary)value); 413 } 414 else if (value is IList) 415 { 416 WriteList(writer, (IList)value); 417 } 418 else if (value is int || value is uint) 419 { 420 IFormattable formattable = (IFormattable) value; 421 writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture)); 422 } 423 else if (value is long || value is ulong) 424 { 425 writer.Write('"'); 426 IFormattable formattable = (IFormattable) value; 427 writer.Write(formattable.ToString("d", CultureInfo.InvariantCulture)); 428 writer.Write('"'); 429 } 430 else if (value is System.Enum) 431 { 432 string name = OriginalEnumValueHelper.GetOriginalName(value); 433 if (name != null) 434 { 435 WriteString(writer, name); 436 } 437 else 438 { 439 WriteValue(writer, (int)value); 440 } 441 } 442 else if (value is float || value is double) 443 { 444 string text = ((IFormattable) value).ToString("r", CultureInfo.InvariantCulture); 445 if (text == "NaN" || text == "Infinity" || text == "-Infinity") 446 { 447 writer.Write('"'); 448 writer.Write(text); 449 writer.Write('"'); 450 } 451 else 452 { 453 writer.Write(text); 454 } 455 } 456 else if (value is IMessage) 457 { 458 Format((IMessage)value, writer); 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 /// Writes a string (including leading and trailing double quotes) to a builder, escaping as required. 728 /// </summary> 729 /// <remarks> 730 /// Other than surrogate pair handling, this code is mostly taken from src/google/protobuf/util/internal/json_escaping.cc. 731 /// </remarks> WriteString(TextWriter writer, string text)732 internal static void WriteString(TextWriter writer, string text) 733 { 734 writer.Write('"'); 735 for (int i = 0; i < text.Length; i++) 736 { 737 char c = text[i]; 738 if (c < 0xa0) 739 { 740 writer.Write(CommonRepresentations[c]); 741 continue; 742 } 743 if (char.IsHighSurrogate(c)) 744 { 745 // Encountered first part of a surrogate pair. 746 // Check that we have the whole pair, and encode both parts as hex. 747 i++; 748 if (i == text.Length || !char.IsLowSurrogate(text[i])) 749 { 750 throw new ArgumentException("String contains low surrogate not followed by high surrogate"); 751 } 752 HexEncodeUtf16CodeUnit(writer, c); 753 HexEncodeUtf16CodeUnit(writer, text[i]); 754 continue; 755 } 756 else if (char.IsLowSurrogate(c)) 757 { 758 throw new ArgumentException("String contains high surrogate not preceded by low surrogate"); 759 } 760 switch ((uint) c) 761 { 762 // These are not required by json spec 763 // but used to prevent security bugs in javascript. 764 case 0xfeff: // Zero width no-break space 765 case 0xfff9: // Interlinear annotation anchor 766 case 0xfffa: // Interlinear annotation separator 767 case 0xfffb: // Interlinear annotation terminator 768 769 case 0x00ad: // Soft-hyphen 770 case 0x06dd: // Arabic end of ayah 771 case 0x070f: // Syriac abbreviation mark 772 case 0x17b4: // Khmer vowel inherent Aq 773 case 0x17b5: // Khmer vowel inherent Aa 774 HexEncodeUtf16CodeUnit(writer, c); 775 break; 776 777 default: 778 if ((c >= 0x0600 && c <= 0x0603) || // Arabic signs 779 (c >= 0x200b && c <= 0x200f) || // Zero width etc. 780 (c >= 0x2028 && c <= 0x202e) || // Separators etc. 781 (c >= 0x2060 && c <= 0x2064) || // Invisible etc. 782 (c >= 0x206a && c <= 0x206f)) 783 { 784 HexEncodeUtf16CodeUnit(writer, c); 785 } 786 else 787 { 788 // No handling of surrogates here - that's done earlier 789 writer.Write(c); 790 } 791 break; 792 } 793 } 794 writer.Write('"'); 795 } 796 797 private const string Hex = "0123456789abcdef"; HexEncodeUtf16CodeUnit(TextWriter writer, char c)798 private static void HexEncodeUtf16CodeUnit(TextWriter writer, char c) 799 { 800 writer.Write("\\u"); 801 writer.Write(Hex[(c >> 12) & 0xf]); 802 writer.Write(Hex[(c >> 8) & 0xf]); 803 writer.Write(Hex[(c >> 4) & 0xf]); 804 writer.Write(Hex[(c >> 0) & 0xf]); 805 } 806 807 /// <summary> 808 /// Settings controlling JSON formatting. 809 /// </summary> 810 public sealed class Settings 811 { 812 /// <summary> 813 /// Default settings, as used by <see cref="JsonFormatter.Default"/> 814 /// </summary> 815 public static Settings Default { get; } 816 817 // Workaround for the Mono compiler complaining about XML comments not being on 818 // valid language elements. Settings()819 static Settings() 820 { 821 Default = new Settings(false); 822 } 823 824 /// <summary> 825 /// Whether fields whose values are the default for the field type (e.g. 0 for integers) 826 /// should be formatted (true) or omitted (false). 827 /// </summary> 828 public bool FormatDefaultValues { get; } 829 830 /// <summary> 831 /// The type registry used to format <see cref="Any"/> messages. 832 /// </summary> 833 public TypeRegistry TypeRegistry { get; } 834 835 // TODO: Work out how we're going to scale this to multiple settings. "WithXyz" methods? 836 837 /// <summary> 838 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values 839 /// and an empty type registry. 840 /// </summary> 841 /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param> Settings(bool formatDefaultValues)842 public Settings(bool formatDefaultValues) : this(formatDefaultValues, TypeRegistry.Empty) 843 { 844 } 845 846 /// <summary> 847 /// Creates a new <see cref="Settings"/> object with the specified formatting of default values 848 /// and type registry. 849 /// </summary> 850 /// <param name="formatDefaultValues"><c>true</c> if default values (0, empty strings etc) should be formatted; <c>false</c> otherwise.</param> 851 /// <param name="typeRegistry">The <see cref="TypeRegistry"/> to use when formatting <see cref="Any"/> messages.</param> Settings(bool formatDefaultValues, TypeRegistry typeRegistry)852 public Settings(bool formatDefaultValues, TypeRegistry typeRegistry) 853 { 854 FormatDefaultValues = formatDefaultValues; 855 TypeRegistry = ProtoPreconditions.CheckNotNull(typeRegistry, nameof(typeRegistry)); 856 } 857 } 858 859 // Effectively a cache of mapping from enum values to the original name as specified in the proto file, 860 // fetched by reflection. 861 // The need for this is unfortunate, as is its unbounded size, but realistically it shouldn't cause issues. 862 private static class OriginalEnumValueHelper 863 { 864 // TODO: In the future we might want to use ConcurrentDictionary, at the point where all 865 // the platforms we target have it. 866 private static readonly Dictionary<System.Type, Dictionary<object, string>> dictionaries 867 = new Dictionary<System.Type, Dictionary<object, string>>(); 868 GetOriginalName(object value)869 internal static string GetOriginalName(object value) 870 { 871 var enumType = value.GetType(); 872 Dictionary<object, string> nameMapping; 873 lock (dictionaries) 874 { 875 if (!dictionaries.TryGetValue(enumType, out nameMapping)) 876 { 877 nameMapping = GetNameMapping(enumType); 878 dictionaries[enumType] = nameMapping; 879 } 880 } 881 882 string originalName; 883 // If this returns false, originalName will be null, which is what we want. 884 nameMapping.TryGetValue(value, out originalName); 885 return originalName; 886 } 887 888 #if DOTNET35 889 // TODO: Consider adding functionality to TypeExtensions to avoid this difference. 890 private static Dictionary<object, string> GetNameMapping(System.Type enumType) => 891 enumType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) 892 .ToDictionary(f => f.GetValue(null), 893 f => (f.GetCustomAttributes(typeof(OriginalNameAttribute), false) 894 .FirstOrDefault() as OriginalNameAttribute) 895 // If the attribute hasn't been applied, fall back to the name of the field. 896 ?.Name ?? f.Name); 897 #else 898 private static Dictionary<object, string> GetNameMapping(System.Type enumType) => 899 enumType.GetTypeInfo().DeclaredFields 900 .Where(f => f.IsStatic) 901 .ToDictionary(f => f.GetValue(null), 902 f => f.GetCustomAttributes<OriginalNameAttribute>() 903 .FirstOrDefault() 904 // If the attribute hasn't been applied, fall back to the name of the field. 905 ?.Name ?? f.Name); 906 #endif 907 } 908 } 909 } 910