From 5345eb58efdd384b2dc452703a457f08e02ef3cf Mon Sep 17 00:00:00 2001 From: Rolf Kristensen Date: Sat, 29 Sep 2018 13:16:02 +0200 Subject: [PATCH] JSON encoding should create valid JSON for non-standard string dictionary-keys --- src/NLog/Targets/DefaultJsonSerializer.cs | 283 ++++++++++-------- .../Targets/DefaultJsonSerializerTests.cs | 40 +++ 2 files changed, 193 insertions(+), 130 deletions(-) diff --git a/src/NLog/Targets/DefaultJsonSerializer.cs b/src/NLog/Targets/DefaultJsonSerializer.cs index 9e718e9adc..215bf39e7a 100644 --- a/src/NLog/Targets/DefaultJsonSerializer.cs +++ b/src/NLog/Targets/DefaultJsonSerializer.cs @@ -200,14 +200,14 @@ public bool SerializeObject(object value, StringBuilder destination, JsonSeriali } else if (value is IDictionary dict) { - using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(dict, ref objectsInPath, true)) + using (StartScope(ref objectsInPath, dict)) { SerializeDictionaryObject(dict, destination, options, objectsInPath, depth); } } else if (value is IEnumerable enumerable) { - using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, true)) + using (StartScope(ref objectsInPath, value)) { SerializeCollectionObject(enumerable, destination, options, objectsInPath, depth); } @@ -235,6 +235,11 @@ public bool SerializeObject(object value, StringBuilder destination, JsonSeriali return true; } + private static SingleItemOptimizedHashSet.SingleItemScopedInsert StartScope(ref SingleItemOptimizedHashSet objectsInPath, object value) + { + return new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, true); + } + private bool SerializeWithFormatProvider(object value, StringBuilder destination, JsonSerializeOptions options, IFormattable formattable, string format, bool hasFormat) { int originalLength = destination.Length; @@ -300,35 +305,47 @@ private void SerializeDictionaryObject(IDictionary value, StringBuilder destinat destination.Append(','); } - //only serialize, if key and value are serialized without error (e.g. due to reference loop) - if (!SerializeObject(de.Key, destination, options, objectsInPath, nextDepth)) + if (options.QuoteKeys) { - destination.Length = originalLength; + var typeCode = Convert.GetTypeCode(de.Key); + if (!SerializeObjectAsString(de.Key, typeCode, destination, options)) + { + destination.Length = originalLength; + continue; + } } else { - if (options.SanitizeDictionaryKeys) - { - int quoteSkipCount = options.QuoteKeys ? 1 : 0; - int keyEndIndex = destination.Length - quoteSkipCount; - int keyStartIndex = originalLength + (first ? 0 : 1) + quoteSkipCount; - if (!SanitizeDictionaryKey(destination, keyStartIndex, keyEndIndex - keyStartIndex)) - { - destination.Length = originalLength; // Empty keys are not allowed - continue; - } - } - - destination.Append(':'); - if (!SerializeObject(de.Value, destination, options, objectsInPath, nextDepth)) + if (!SerializeObject(de.Key, destination, options, objectsInPath, nextDepth)) { destination.Length = originalLength; + continue; } - else + } + + if (options.SanitizeDictionaryKeys) + { + int quoteSkipCount = options.QuoteKeys ? 1 : 0; + int keyEndIndex = destination.Length - quoteSkipCount; + int keyStartIndex = originalLength + (first ? 0 : 1) + quoteSkipCount; + if (!SanitizeDictionaryKey(destination, keyStartIndex, keyEndIndex - keyStartIndex)) { - first = false; + destination.Length = originalLength; // Empty keys are not allowed + continue; } } + + destination.Append(':'); + + //only serialize, if key and value are serialized without error (e.g. due to reference loop) + if (!SerializeObject(de.Value, destination, options, objectsInPath, nextDepth)) + { + destination.Length = originalLength; + } + else + { + first = false; + } } destination.Append('}'); } @@ -399,104 +416,101 @@ private bool SerializeTypeCodeValue(object value, StringBuilder destination, Jso { //object without property, to string QuoteValue(destination, Convert.ToString(value, CultureInfo.InvariantCulture)); + return true; } else if (value is DateTimeOffset) { QuoteValue(destination, $"{value:yyyy-MM-dd HH:mm:ss zzz}"); + return true; } else { - int originalLength = destination.Length; - if (originalLength > MaxJsonLength) - { - return false; - } - - if (depth < options.MaxRecursionLimit) - { - try - { - if (value is Exception && ReferenceEquals(options, instance._serializeOptions)) - { - // Exceptions are seldom under control, and can include random Data-Dictionary-keys, so we sanitize by default - options = instance._exceptionSerializeOptions; - } - - using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, false)) - { - return SerializeProperties(value, destination, options, objectsInPath, depth); - } - } - catch - { - //nothing to add, so return is OK - destination.Length = originalLength; - return false; - } - } - else - { - try - { - string str = Convert.ToString(value, CultureInfo.InvariantCulture); - destination.Append('"'); - AppendStringEscape(destination, str, options.EscapeUnicode); - destination.Append('"'); - } - catch - { - return false; - } - } + return SerializeObjectWithProperties(value, destination, options, ref objectsInPath, depth); } } else { - if (IsNumericTypeCode(objTypeCode, false)) - { - SerializeNumber(value, destination, options, objTypeCode); - } - else + return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options); + } + } + + private bool SerializeObjectWithProperties(object value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet objectsInPath, int depth) + { + int originalLength = destination.Length; + if (originalLength > MaxJsonLength) + { + return false; + } + + if (depth < options.MaxRecursionLimit) + { + try { - string str = XmlHelper.XmlConvertToString(value, objTypeCode); - if (str == null) + if (value is Exception && ReferenceEquals(options, instance._serializeOptions)) { - return false; + // Exceptions are seldom under control, and can include random Data-Dictionary-keys, so we sanitize by default + options = instance._exceptionSerializeOptions; } - if (SkipQuotes(value, objTypeCode)) + + using (new SingleItemOptimizedHashSet.SingleItemScopedInsert(value, ref objectsInPath, false)) { - destination.Append(str); - } - else - { - if (objTypeCode == TypeCode.Char) - { - destination.Append('"'); - AppendStringEscape(destination, str, options.EscapeUnicode); - destination.Append('"'); - } - else - { - QuoteValue(destination, str); - } + return SerializeProperties(value, destination, options, objectsInPath, depth); } } + catch + { + //nothing to add, so return is OK + destination.Length = originalLength; + return false; + } + } + else + { + return SerializeObjectAsString(value, TypeCode.Object, destination, options); } - - return true; } - private void SerializeNumber(object value, StringBuilder destination, JsonSerializeOptions options, TypeCode objTypeCode) + private bool SerializeSimpleTypeCodeValue(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceQuotes = false) { - Enum enumValue; - if (!options.EnumAsInteger && (enumValue = value as Enum) != null) + if (objTypeCode == TypeCode.String || objTypeCode == TypeCode.Char) { - QuoteValue(destination, EnumAsString(enumValue)); + destination.Append('"'); + AppendStringEscape(destination, value.ToString(), options.EscapeUnicode); + destination.Append('"'); + } + else if (IsNumericTypeCode(objTypeCode, false)) + { + if (!options.EnumAsInteger && value is Enum enumValue) + { + QuoteValue(destination, EnumAsString(enumValue)); + } + else + { + if (forceQuotes) + destination.Append('"'); + destination.AppendIntegerAsString(value, objTypeCode); + if (forceQuotes) + destination.Append('"'); + } } else { - destination.AppendIntegerAsString(value, objTypeCode); + string str = XmlHelper.XmlConvertToString(value, objTypeCode); + if (str == null) + { + return false; + } + + if (!forceQuotes && SkipQuotes(value, objTypeCode)) + { + destination.Append(str); + } + else + { + QuoteValue(destination, str); + } } + return true; } private static CultureInfo CreateFormatProvider() @@ -541,30 +555,27 @@ private string EnumAsString(Enum value) /// private static bool SkipQuotes(object value, TypeCode objTypeCode) { - if (objTypeCode != TypeCode.String) + switch (objTypeCode) { - if (objTypeCode == TypeCode.Empty || objTypeCode == TypeCode.Boolean) - return true; // Don't put quotes around null values - - if (IsNumericTypeCode(objTypeCode, false) || objTypeCode == TypeCode.Decimal) - return true; - - if (objTypeCode == TypeCode.Double) - { - double dblValue = (double)value; - if (!double.IsNaN(dblValue) && !double.IsInfinity(dblValue)) - return true; - } - - if (objTypeCode == TypeCode.Single) - { - float floatValue = (float)value; - if (!float.IsNaN(floatValue) && !float.IsInfinity(floatValue)) - return true; - } + case TypeCode.String: return false; + case TypeCode.Char: return false; + case TypeCode.DateTime: return false; + case TypeCode.Empty: return true; + case TypeCode.Boolean: return true; + case TypeCode.Decimal: return true; + case TypeCode.Double: + { + double dblValue = (double)value; + return !double.IsNaN(dblValue) && !double.IsInfinity(dblValue); + } + case TypeCode.Single: + { + float floatValue = (float)value; + return !float.IsNaN(floatValue) && !float.IsInfinity(floatValue); + } + default: + return IsNumericTypeCode(objTypeCode, false); } - - return false; } /// @@ -705,19 +716,8 @@ private static bool EscapeChar(char ch, bool escapeUnicode) var props = GetProps(value); if (props.Key.Length == 0) { - try - { - //no props - var str = Convert.ToString(value, CultureInfo.InvariantCulture); - destination.Append('"'); - AppendStringEscape(destination, str, options.EscapeUnicode); - destination.Append('"'); - return true; - } - catch - { - return false; - } + //no props + return SerializeObjectAsString(value, TypeCode.Object, destination, options); } destination.Append('{'); @@ -771,6 +771,29 @@ private static bool EscapeChar(char ch, bool escapeUnicode) return true; } + private bool SerializeObjectAsString(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options) + { + try + { + if (objTypeCode == TypeCode.Object) + { + var str = Convert.ToString(value, CultureInfo.InvariantCulture); + destination.Append('"'); + AppendStringEscape(destination, str, options.EscapeUnicode); + destination.Append('"'); + return true; + } + else + { + return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options, true); + } + } + catch + { + return false; + } + } + /// /// Get properties, cached for a type /// @@ -779,13 +802,13 @@ private static bool EscapeChar(char ch, bool escapeUnicode) private KeyValuePair GetProps(object value) { var type = value.GetType(); - KeyValuePair props; + KeyValuePair props; if (_propsCache.TryGetValue(type, out props)) { if (props.Key.Length != 0 && props.Value.Length == 0) { var lateBoundMethods = new ReflectionHelpers.LateBoundMethod[props.Key.Length]; - for(int i = 0; i < props.Key.Length; i++) + for (int i = 0; i < props.Key.Length; i++) { lateBoundMethods[i] = ReflectionHelpers.CreateLateBoundMethod(props.Key[i].GetGetMethod()); } diff --git a/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTests.cs b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTests.cs index 976cc12461..201c36c40a 100644 --- a/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTests.cs +++ b/tests/NLog.UnitTests/Targets/DefaultJsonSerializerTests.cs @@ -326,6 +326,46 @@ public void SerializeDict_Test() Assert.Equal("{\"key1\":13,\"key 2\":1.3}", actual); } + [Fact] + public void SerializeIntegerKeyDict_Test() + { + var dictionary = new Dictionary(); + dictionary.Add(1, "One"); + dictionary.Add(2, "Two"); + var actual = _serializer.SerializeObject(dictionary); + Assert.Equal("{\"1\":\"One\",\"2\":\"Two\"}", actual); + } + + [Fact] + public void SerializeEnumKeyDict_Test() + { + var dictionary = new Dictionary(); + dictionary.Add(ExceptionRenderingFormat.Method, 4); + dictionary.Add(ExceptionRenderingFormat.StackTrace, 5); + var actual = _serializer.SerializeObject(dictionary); + Assert.Equal("{\"Method\":4,\"StackTrace\":5}", actual); + } + + [Fact] + public void SerializeObjectKeyDict_Test() + { + var dictionary = new Dictionary(); + dictionary.Add(new { Name = "Hello" }, "World"); + dictionary.Add(new { Name = "Goodbye" }, "Money"); + var actual = _serializer.SerializeObject(dictionary); + Assert.Equal("{\"{ Name = Hello }\":\"World\",\"{ Name = Goodbye }\":\"Money\"}", actual); + } + + [Fact] + public void SerializeBadStringKeyDict_Test() + { + var dictionary = new Dictionary(); + dictionary.Add("\t", "Tab"); + dictionary.Add("\n", "Newline"); + var actual = _serializer.SerializeObject(dictionary); + Assert.Equal("{\"\\t\":\"Tab\",\"\\n\":\"Newline\"}", actual); + } + [Fact] public void SerializeNull_Test() {