diff --git a/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs b/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs index 3fe58391..877094dd 100644 --- a/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs +++ b/YamlDotNet.Test/Serialization/DateTimeConverterTests.cs @@ -569,7 +569,7 @@ public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstForm public void JsonCompatible_EncaseDateTimesInDoubleQuotes() { var serializer = new SerializerBuilder().JsonCompatible().Build(); - var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2) }; + var testObject = new TestObject { DateTime = new DateTime(2023, 01, 14, 0, 1, 2, DateTimeKind.Utc) }; var actual = serializer.Serialize(testObject); actual.TrimNewLines().Should().ContainEquivalentOf("{\"DateTime\": \"01/14/2023 00:01:02\"}"); diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index a61d683b..7f851c26 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -298,7 +298,14 @@ public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYaml .Build(); Action act = () => sut.Deserialize(yaml); - act.ShouldThrow("Because there are duplicate name keys"); + act.ShouldThrow("Because there are duplicate name keys with concrete class"); + act = () => sut.Deserialize>(yaml); + act.ShouldThrow("Because there are duplicate name keys with dictionary"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldThrow("Because there are duplicate name keys with merging parser"); } [Fact] @@ -316,6 +323,13 @@ public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNot Action act = () => sut.Deserialize(yaml); act.ShouldNotThrow("Because duplicate key checking is not enabled"); + act = () => sut.Deserialize>(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + + var stream = Yaml.ReaderFrom("backreference.yaml"); + var parser = new MergingParser(new Parser(stream)); + act = () => sut.Deserialize>>(parser); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); } public class Test diff --git a/YamlDotNet.Test/Serialization/SerializationTests.cs b/YamlDotNet.Test/Serialization/SerializationTests.cs index bc5762b7..e77c1b6d 100644 --- a/YamlDotNet.Test/Serialization/SerializationTests.cs +++ b/YamlDotNet.Test/Serialization/SerializationTests.cs @@ -2300,6 +2300,29 @@ public void StringsThatMatchKeywordsAreQuoted(string input) Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); } + public static IEnumerable Yaml1_1SpecialStringsData = new[] + { + "-.inf", "-.Inf", "-.INF", "-0", "-0100_200", "-0b101", "-0x30", "-190:20:30", "-23", "-3.14", + "._", "._14", ".", ".0", ".1_4", ".14", ".3E-1", ".3e+3", ".inf", ".Inf", + ".INF", ".nan", ".NaN", ".NAN", "+.inf", "+.Inf", "+.INF", "+0.3e+3", "+0", + "+0100_200", "+0b100", "+190:20:30", "+23", "+3.14", "~", "0.0", "0", "00", "001.23", + "0011", "010", "02_0", "07", "0b0", "0b100_101", "0o0", "0o10", "0o7", "0x0", + "0x10", "0x2_0", "0x42", "0xa", "100_000", "190:20:30.15", "190:20:30", "23", "3.", "3.14", "3.3e+3", + "85_230.15", "85.230_15e+03", "false", "False", "FALSE", "n", "N", "no", "No", "NO", + "null", "Null", "NULL", "off", "Off", "OFF", "on", "On", "ON", "true", "True", "TRUE", + "y", "Y", "yes", "Yes", "YES" + }.Select(v => new object[] { v }).ToList(); + + [Theory] + [MemberData(nameof(Yaml1_1SpecialStringsData))] + public void StringsThatMatchYaml1_1KeywordsAreQuoted(string input) + { + var serializer = new SerializerBuilder().WithQuotingNecessaryStrings(true).Build(); + var o = new { text = input }; + var yaml = serializer.Serialize(o); + Assert.Equal($"text: \"{input}\"{Environment.NewLine}", yaml); + } + [Fact] public void KeysOnConcreteClassDontGetQuoted_TypeStringGetsQuoted() { diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 24653c68..6cbc3efa 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -93,7 +93,7 @@ public DeserializerBuilder() { typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() }, { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter) }, { typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer() }, - { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) }, + { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, duplicateKeyChecking) }, { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) }, { typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() }, { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter) } diff --git a/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs b/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs index 497bd1a6..13695d20 100644 --- a/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs +++ b/YamlDotNet/Serialization/EventEmitters/TypeAssigningEventEmitter.cs @@ -32,21 +32,64 @@ public sealed class TypeAssigningEventEmitter : ChainedEventEmitter private readonly bool requireTagWhenStaticAndActualTypesAreDifferent; private readonly IDictionary tagMappings; private readonly bool quoteNecessaryStrings; - private static readonly string IsSpecialStringValue_Regex = + private readonly Regex? isSpecialStringValue_Regex; + private static readonly string SpecialStrings_Pattern = @"^(" - + @"null|Null|NULL|\~" - + @"|true|True|TRUE|false|False|FALSE" - + @"|[-+]?[0-9]+|0o[0-7]+" - + @"|0x[0-9a-fA-F]+" - + @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?" - + @"|[-+]?(\.inf|\.Inf|\.INF)" - + @"|\.nan|\.NaN|\.NAN" + + @"null|Null|NULL|\~" + + @"|true|True|TRUE|false|False|FALSE" + + @"|[-+]?[0-9]+" // int base 10 + + @"|0o[0-7]+" // int base 8 + + @"|0x[0-9a-fA-F]+" // int base 16 + + @"|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?" // float number + + @"|[-+]?(\.inf|\.Inf|\.INF)" + + @"|\.nan|\.NaN|\.NAN" + @")$"; + /// + /// This pattern matches strings that are special both in YAML 1.1 and 1.2 + /// + private static readonly string CombinedYaml1_1SpecialStrings_Pattern = + @"^(" + + @"null|Null|NULL|\~" + + @"|true|True|TRUE|false|False|FALSE" + + @"|y|Y|yes|Yes|YES|n|N|no|No|NO" + + @"|on|On|ON|off|Off|OFF" + + @"|[-+]?0b[0-1_]+" // int base 2 + + @"|[-+]?0o?[0-7_]+" // int base 8 both with and without "o" + + @"|[-+]?(0|[1-9][0-9_]*)" // int base 10 + + @"|[-+]?0x[0-9a-fA-F_]+" // int base 16 + + @"|[-+]?[1-9][0-9_]*(:[0-5]?[0-9])+" // int base 60 + + @"|[-+]?([0-9][0-9_]*)?\.[0-9_]*([eE][-+][0-9]+)?" // float base 10 + + @"|[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*" // float base 60 + + @"|[-+]?\.(inf|Inf|INF)" + + @"|\.(nan|NaN|NAN)" + + @")$"; + + public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary tagMappings, bool quoteNecessaryStrings, bool quoteYaml1_1Strings) + : this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings) + { + this.quoteNecessaryStrings = quoteNecessaryStrings; + + var specialStringValuePattern = quoteYaml1_1Strings + ? CombinedYaml1_1SpecialStrings_Pattern + : SpecialStrings_Pattern; +#if NET40 + isSpecialStringValue_Regex = new Regex(specialStringValuePattern); +#else + isSpecialStringValue_Regex = new Regex(specialStringValuePattern, RegexOptions.Compiled); +#endif + } + public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary tagMappings, bool quoteNecessaryStrings) : this(nextEmitter, requireTagWhenStaticAndActualTypesAreDifferent, tagMappings) { this.quoteNecessaryStrings = quoteNecessaryStrings; + +#if NET40 + isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern); +#else + isSpecialStringValue_Regex = new Regex(SpecialStrings_Pattern, RegexOptions.Compiled); +#endif } public TypeAssigningEventEmitter(IEventEmitter nextEmitter, bool requireTagWhenStaticAndActualTypesAreDifferent, IDictionary tagMappings) @@ -204,9 +247,7 @@ private bool IsSpecialStringValue(string value) return true; } - return Regex.IsMatch( - value, - IsSpecialStringValue_Regex); + return isSpecialStringValue_Regex?.IsMatch(value) ?? false; } } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/DictionaryDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/DictionaryDeserializer.cs index 767c5236..4bd45950 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/DictionaryDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/DictionaryDeserializer.cs @@ -28,9 +28,25 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public abstract class DictionaryDeserializer { - protected void Deserialize(Type tKey, Type tValue, IParser parser, Func nestedObjectDeserializer, IDictionary result) + private readonly bool duplicateKeyChecking; + + public DictionaryDeserializer(bool duplicateKeyChecking) + { + this.duplicateKeyChecking = duplicateKeyChecking; + } + + private void TryAssign(IDictionary result, object key, object value, MappingStart propertyName) + { + if (duplicateKeyChecking && result.Contains(key)) + { + throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {key}"); + } + result[key] = value!; + } + + protected virtual void Deserialize(Type tKey, Type tValue, IParser parser, Func nestedObjectDeserializer, IDictionary result) { - parser.Consume(); + var property = parser.Consume(); while (!parser.TryConsume(out var _)) { var key = nestedObjectDeserializer(parser, tKey); @@ -53,7 +69,7 @@ protected void Deserialize(Type tKey, Type tValue, IParser parser, Func(); + var consumedProperties = new HashSet(StringComparer.Ordinal); while (!parser.TryConsume(out var _)) { var propertyName = parser.Consume(); - if (duplicateKeyChecking && consumedProperties.Contains(propertyName.Value)) + if (duplicateKeyChecking && !consumedProperties.Add(propertyName.Value)) { throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {propertyName.Value}"); } try { - consumedProperties.Add(propertyName.Value); var property = typeDescriptor.GetProperty(implementationType, null, propertyName.Value, ignoreUnmatched); if (property == null) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs index 31ac0f41..0eef081b 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/StaticDictionaryNodeDeserializer.cs @@ -29,7 +29,8 @@ public class StaticDictionaryNodeDeserializer : DictionaryDeserializer, INodeDes { private readonly ObjectFactories.StaticObjectFactory _objectFactory; - public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory) + public StaticDictionaryNodeDeserializer(ObjectFactories.StaticObjectFactory objectFactory, bool duplicateKeyChecking) + : base(duplicateKeyChecking) { _objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); } diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index c2d97741..5f323204 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -60,6 +60,7 @@ public sealed class SerializerBuilder : BuilderSkeleton private EmitterSettings emitterSettings = EmitterSettings.Default; private DefaultValuesHandling defaultValuesHandlingConfiguration = DefaultValuesHandling.Preserve; private bool quoteNecessaryStrings; + private bool quoteYaml1_1Strings; public SerializerBuilder() : base(new DynamicTypeResolver()) @@ -96,7 +97,7 @@ public SerializerBuilder() eventEmitterFactories = new LazyComponentRegistrationList { - { typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings) } + { typeof(TypeAssigningEventEmitter), inner => new TypeAssigningEventEmitter(inner, false, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings) } }; objectFactory = new DefaultObjectFactory(); @@ -110,9 +111,11 @@ public SerializerBuilder() /// /// Put double quotes around strings that need it, for example Null, True, False, a number. This should be called before any other "With" methods if you want this feature enabled. /// - public SerializerBuilder WithQuotingNecessaryStrings() + /// Also quote strings that are valid scalars in the YAML 1.1 specification (which includes boolean Yes/No/On/Off, base 60 numbers and more) + public SerializerBuilder WithQuotingNecessaryStrings(bool quoteYaml1_1Strings = false) { quoteNecessaryStrings = true; + this.quoteYaml1_1Strings = quoteYaml1_1Strings; return this; } @@ -269,7 +272,7 @@ public SerializerBuilder EnsureRoundtrip() settings, objectFactory ); - WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings), loc => loc.InsteadOf()); + WithEventEmitter(inner => new TypeAssigningEventEmitter(inner, true, tagMappings, quoteNecessaryStrings, quoteYaml1_1Strings), loc => loc.InsteadOf()); return WithTypeInspector(inner => new ReadableAndWritablePropertiesTypeInspector(inner), loc => loc.OnBottom()); } diff --git a/YamlDotNet/Serialization/StaticDeserializerBuilder.cs b/YamlDotNet/Serialization/StaticDeserializerBuilder.cs index 5dbcb875..cbb51ff6 100644 --- a/YamlDotNet/Serialization/StaticDeserializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticDeserializerBuilder.cs @@ -88,7 +88,7 @@ public StaticDeserializerBuilder(StaticContext context) { typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() }, { typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer(attemptUnknownTypeDeserialization, typeConverter) }, { typeof(StaticArrayNodeDeserializer), _ => new StaticArrayNodeDeserializer(factory) }, - { typeof(StaticDictionaryNodeDeserializer), _ => new StaticDictionaryNodeDeserializer(factory) }, + { typeof(StaticDictionaryNodeDeserializer), _ => new StaticDictionaryNodeDeserializer(factory, duplicateKeyChecking) }, { typeof(StaticCollectionNodeDeserializer), _ => new StaticCollectionNodeDeserializer(factory) }, { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(factory, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking, typeConverter) }, };