diff --git a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Nrbf/CoreNrbfSerializerTests.cs b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Nrbf/CoreNrbfSerializerTests.cs index ff49f9ab22c..fd6a7d1c05e 100644 --- a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Nrbf/CoreNrbfSerializerTests.cs +++ b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Nrbf/CoreNrbfSerializerTests.cs @@ -1,19 +1,27 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Formats.Nrbf; using System.Reflection.Metadata; using System.Runtime.Serialization.Formatters.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace System.Private.Windows.Nrbf; +// Suppress CS0618 warnings for the entire class as we're deliberately testing backward compatibility +// with obsolete APIs like IHashCodeProvider to ensure proper serialization handling +#pragma warning disable CS0618 public class CoreNrbfSerializerTests { public static TheoryData TryWriteObjectData => new() { { 123, true }, { "test", true }, - { new object(), false } + { new object(), false }, + { new Hashtable() { { "key", "value" } }, true }, + { new Hashtable(StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, true } }; [Theory] @@ -94,4 +102,175 @@ public void IsSupportedType_ShouldReturnExpectedResult(Type type, bool expectedR { CoreNrbfSerializer.IsFullySupportedType(type).Should().Be(expectedResult); } + + [Fact] + public void HashtableType_IsNotFullySupportedType() + { + // Hashtable is not fully supported but can be round-tripped through TryWriteObject/TryGetObject + CoreNrbfSerializer.IsFullySupportedType(typeof(Hashtable)).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(HashtableTestData))] + public void Hashtable_TryWriteObject_TryGetObject_RoundTrip(Hashtable hashtable, bool expectSuccessfulDeserialization) + { + using MemoryStream stream = new(); + bool result = CoreNrbfSerializer.TryWriteObject(stream, hashtable); + result.Should().BeTrue(); // All Hashtables should be serializable + + stream.Position = 0; + SerializationRecord record = NrbfDecoder.Decode(stream); + bool deserializationResult = CoreNrbfSerializer.TryGetObject(record, out object? value); + deserializationResult.Should().Be(expectSuccessfulDeserialization); + + // When deserialization is expected to succeed, verify the key-value pairs + if (expectSuccessfulDeserialization) + { + value.Should().BeOfType(); + Hashtable? deserializedTable = value as Hashtable; + deserializedTable!.Count.Should().Be(hashtable.Count); + + foreach (DictionaryEntry entry in hashtable) + { + deserializedTable.Contains(entry.Key).Should().BeTrue(); + deserializedTable[entry.Key].Should().Be(entry.Value); + } + } + } + + [Theory] + [MemberData(nameof(HashtableTestData))] + public void BinaryFormatter_Hashtable_TryGetObject_RoundTrip(Hashtable hashtable, bool expectSuccessfulDeserialization) + { + using MemoryStream stream = new(); + using (BinaryFormatterScope scope = new(enable: true)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + new BinaryFormatter().Serialize(stream, hashtable); +#pragma warning restore SYSLIB0011 + } + + stream.Position = 0; + SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true); + bool deserializationResult = CoreNrbfSerializer.TryGetObject(record, out object? value); + deserializationResult.Should().Be(expectSuccessfulDeserialization); + + // When deserialization is expected to succeed, verify the key-value pairs + if (expectSuccessfulDeserialization) + { + value.Should().BeOfType(); + Hashtable? deserializedTable = value as Hashtable; + deserializedTable!.Count.Should().Be(hashtable.Count); + + foreach (DictionaryEntry entry in hashtable) + { + deserializedTable.Contains(entry.Key).Should().BeTrue(); + deserializedTable[entry.Key].Should().Be(entry.Value); + } + } + } + + public static TheoryData HashtableTestData => new() + { + // Standard hashtable should deserialize correctly + { new Hashtable() { { "key", "value" } }, true }, + { new Hashtable() { { 1, 2 }, { "text", 42 } }, true }, + + // Hashtable with custom comparer won't be deserialized by TryGetPrimitiveHashtable + { new Hashtable(StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, false }, + { new Hashtable(StringComparer.CurrentCulture) { { "key", "value" } }, false }, + + // Hashtable with hash code provider won't be deserialized by TryGetPrimitiveHashtable + { new Hashtable(new CustomHashCodeProvider(), null) { { "key", "value" } }, false }, + { new Hashtable(new CustomHashCodeProvider(), StringComparer.OrdinalIgnoreCase) { { "key", "value" } }, false } + }; + + [Fact] + public void HashtableWithCustomComparer_PreservesData_EvenWhenNotDeserialized() + { + // Create a hashtable with a custom comparer + Hashtable originalHashtable = new(StringComparer.OrdinalIgnoreCase) + { + { "Key", "Value" } + }; + + // Serialize with BinaryFormatter + using MemoryStream stream = new(); + using (BinaryFormatterScope scope = new(enable: true)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + new BinaryFormatter().Serialize(stream, originalHashtable); +#pragma warning restore SYSLIB0011 + } + + // First verify that CoreNrbfSerializer can't deserialize it + stream.Position = 0; + SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true); + CoreNrbfSerializer.TryGetObject(record, out _).Should().BeFalse(); + + // Now verify that BinaryFormatter can deserialize it with all data intact + stream.Position = 0; + using (BinaryFormatterScope scope = new(enable: true)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + Hashtable deserializedHashtable = (Hashtable)new BinaryFormatter().Deserialize(stream); +#pragma warning restore SYSLIB0011 + + // Verify key-value data + deserializedHashtable.Count.Should().Be(originalHashtable.Count); + deserializedHashtable["Key"].Should().Be("Value"); + + // Verify case-insensitivity was preserved (comparer was not lost) + deserializedHashtable["key"].Should().Be("Value"); + } + } + + [Fact] + public void HashtableWithHashCodeProvider_PreservesData_EvenWhenNotDeserialized() + { + // Create a hashtable with a custom hash code provider + Hashtable originalHashtable = new(new CustomHashCodeProvider(), null) + { + { "Key", "Value" } + }; + + // Serialize with BinaryFormatter + using MemoryStream stream = new(); + using (BinaryFormatterScope scope = new(enable: true)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + new BinaryFormatter().Serialize(stream, originalHashtable); +#pragma warning restore SYSLIB0011 + } + + // First verify that CoreNrbfSerializer can't deserialize it + stream.Position = 0; + SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true); + CoreNrbfSerializer.TryGetObject(record, out _).Should().BeFalse(); + + // Now verify that BinaryFormatter can deserialize it with all data intact + stream.Position = 0; + using (BinaryFormatterScope scope = new(enable: true)) + { +#pragma warning disable SYSLIB0011 // Type or member is obsolete + Hashtable deserializedHashtable = (Hashtable)new BinaryFormatter().Deserialize(stream); +#pragma warning restore SYSLIB0011 + + // Verify key-value data + deserializedHashtable.Count.Should().Be(originalHashtable.Count); + deserializedHashtable["Key"].Should().Be("Value"); + + // Verify the hash code provider was preserved + deserializedHashtable.GetType().GetField("_keycomparer", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) + .Should().NotBeNull(); + } + } + + [Serializable] + private class CustomHashCodeProvider : IHashCodeProvider + { + public int GetHashCode(object obj) => obj?.GetHashCode() ?? 0; + } } +#pragma warning restore CS0618 \ No newline at end of file