From a1a225258af0c783e56efad54d5aff735b9932b1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Jul 2026 17:50:37 +0900 Subject: [PATCH 1/2] Add failing tests --- ...TestMultiplayerMessagePackSerialization.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs index da21ff6fcbf8..6891b214d0a7 100644 --- a/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestMultiplayerMessagePackSerialization.cs @@ -69,5 +69,28 @@ public void TestSerialiseUnionSucceedsWithWorkaround() var deserialized = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); ClassicAssert.True(deserialized is TeamVersusUserState); } + + [Test] + public void TestEnumValidation() + { + Assert.DoesNotThrow(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(SimpleEnum.Value, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + Assert.Throws(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize((SimpleEnum?)null, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + Assert.Throws(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize((SimpleEnum)100, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + + Assert.DoesNotThrow(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(SimpleEnum.Value, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + Assert.DoesNotThrow(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize((SimpleEnum?)null, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + Assert.Throws(() => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize((SimpleEnum)100, SignalRUnionWorkaroundResolver.OPTIONS), SignalRUnionWorkaroundResolver.OPTIONS)); + } + + public enum SimpleEnum + { + Value + } } } From b7205d14b2cb6953621faa97b9217856e0d023df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 1 Jul 2026 17:51:21 +0900 Subject: [PATCH 2/2] Add enum validation to MessagePack serializer --- .../Online/SignalRUnionWorkaroundResolver.cs | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs index e58cd8347752..8252d68d8258 100644 --- a/osu.Game/Online/SignalRUnionWorkaroundResolver.cs +++ b/osu.Game/Online/SignalRUnionWorkaroundResolver.cs @@ -1,14 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; +using osu.Framework.Extensions.TypeExtensions; namespace osu.Game.Online { @@ -22,6 +22,7 @@ public class SignalRUnionWorkaroundResolver : IFormatterResolver MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver()); private static readonly IReadOnlyDictionary formatter_map = createFormatterMap(); + private static readonly ConcurrentDictionary enum_formatter_map = []; private static IReadOnlyDictionary createFormatterMap() { @@ -33,7 +34,7 @@ private static IReadOnlyDictionary createFormatterM return new Dictionary(baseMap.Select(t => { - var formatter = (IMessagePackFormatter)Activator.CreateInstance(typeof(TypeRedirectingFormatter<,>).MakeGenericType(t.derivedType, t.baseType)); + var formatter = (IMessagePackFormatter)Activator.CreateInstance(typeof(TypeRedirectingFormatter<,>).MakeGenericType(t.derivedType, t.baseType))!; return new KeyValuePair(t.derivedType, formatter); })); } @@ -43,7 +44,15 @@ public IMessagePackFormatter GetFormatter() if (formatter_map.TryGetValue(typeof(T), out var formatter)) return (IMessagePackFormatter)formatter; - return StandardResolver.Instance.GetFormatter(); + if (typeof(T).IsEnum) + { + if (enum_formatter_map.TryGetValue(typeof(T), out formatter)) + return (IMessagePackFormatter)formatter; + + return (IMessagePackFormatter)(enum_formatter_map[typeof(T)] = (IMessagePackFormatter)Activator.CreateInstance(typeof(EnumFormatter<>).MakeGenericType(typeof(T)))!); + } + + return StandardResolver.Instance.GetFormatterWithVerify(); } public class TypeRedirectingFormatter : IMessagePackFormatter @@ -52,14 +61,55 @@ public class TypeRedirectingFormatter : IMessagePackFormatter(); + baseFormatter = StandardResolver.Instance.GetFormatterWithVerify(); + } + + public void Serialize(ref MessagePackWriter writer, TActual value, MessagePackSerializerOptions options) + => baseFormatter.Serialize(ref writer, (TBase)(object)value!, StandardResolver.Options); + + public TActual Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + => (TActual)(object)baseFormatter.Deserialize(ref reader, StandardResolver.Options)!; + } + + public class EnumFormatter : IMessagePackFormatter + where T : Enum + { + private readonly IMessagePackFormatter formatter; + + public EnumFormatter() + { + formatter = StandardResolver.Instance.GetFormatterWithVerify(); } - public void Serialize(ref MessagePackWriter writer, TActual value, MessagePackSerializerOptions options) => - baseFormatter.Serialize(ref writer, (TBase)(object)value, StandardResolver.Options); + public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options) + { + EnumValueOutOfRangeException.ThrowIfNotDefined(value); + formatter.Serialize(ref writer, value, StandardResolver.Options); + } - public TActual Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => - (TActual)(object)baseFormatter.Deserialize(ref reader, StandardResolver.Options); + public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + T result = formatter.Deserialize(ref reader, StandardResolver.Options); + EnumValueOutOfRangeException.ThrowIfNotDefined(result); + return result; + } + } + + public class EnumValueOutOfRangeException : Exception + where T : Enum + { + public EnumValueOutOfRangeException(T value) + : base($"Enum value '{value}' out of range for type '{typeof(T).ReadableName()}'") + { + } + + public static void ThrowIfNotDefined(T value) + { + if (Enum.IsDefined(typeof(T), value)) + return; + + throw new EnumValueOutOfRangeException(value); + } } } }