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