CultLib/src/GameCult.Caching.MessagePack/CultMessagePackSecurity.cs
Meta Vens ea5a07b37a Own serialization options by the document's assembly
The static UntrustedData options are gone. An assembly declares its formatter
resolvers with [assembly: CultCacheFormatterResolver(typeof(R))];
CultDocumentMessagePackSerialization.OptionsFor(assembly) composes them ahead
of CultDocumentResolver and StandardResolver, once per assembly, and hands
assemblies that declare nothing the base Options. Generated codecs and the
reflective SerializeUntyped/DeserializeUntyped path both read
OptionsFor(typeof(X).Assembly). No registration call, no mutable options, no
load order.

CultMessagePackSecurity copies UntrustedData and supplies a collision-resistant
comparer over CultRecordRef<T>.Key.Value, so ref-keyed dictionaries
deserialize.

Tests: RefKeyedDictionaryRoundTrips failed on 825b4f7 ("No hash-resistant
equality comparer available"); DeclaredResolverEncodesValueTypeAsPositionalArray
and UnknownAssemblyGetsBaseOptions did not compile there (new API);
InteropNoteBytesUnchanged is a pin whose payload constant was captured on
825b4f7. BenchItem, the one unkeyed [CultDocument] in any consumer (in-memory
benchmark, no store), gains [Key]s in its existing slot order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:12:53 +02:00

35 lines
1.5 KiB
C#

using System;
using System.Collections.Generic;
using MessagePack;
namespace GameCult.Caching.MessagePack;
// UntrustedData, plus a collision-resistant comparer for CultRecordRef<T> dictionary keys over the key string.
public sealed class CultMessagePackSecurity : MessagePackSecurity
{
public static readonly CultMessagePackSecurity Instance = new(UntrustedData);
private CultMessagePackSecurity(MessagePackSecurity copyFrom) : base(copyFrom) { }
protected override IEqualityComparer<T> GetHashCollisionResistantEqualityComparer<T>()
{
var type = typeof(T);
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(CultRecordRef<>))
return base.GetHashCollisionResistantEqualityComparer<T>();
var comparerType = typeof(RecordRefComparer<>).MakeGenericType(type.GetGenericArguments()[0]);
return (IEqualityComparer<T>)Activator.CreateInstance(comparerType, GetEqualityComparer<string>())!;
}
protected override MessagePackSecurity Clone() => new CultMessagePackSecurity(this);
private sealed class RecordRefComparer<TDocument> : IEqualityComparer<CultRecordRef<TDocument>>
{
private readonly IEqualityComparer<string> _keys;
public RecordRefComparer(IEqualityComparer<string> keys) => _keys = keys;
public bool Equals(CultRecordRef<TDocument> x, CultRecordRef<TDocument> y) => _keys.Equals(x.Key.Value, y.Key.Value);
public int GetHashCode(CultRecordRef<TDocument> value) => _keys.GetHashCode(value.Key.Value);
}
}