1364 lines
48 KiB
C#
1364 lines
48 KiB
C#
using System.Collections;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Frozen;
|
||
using System.Linq.Expressions;
|
||
using System.Reflection;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using AyCode.Core.Helpers;
|
||
using static AyCode.Core.Helpers.JsonUtilities;
|
||
|
||
namespace AyCode.Core.Serializers.Binaries;
|
||
|
||
/// <summary>
|
||
/// Exception thrown when binary deserialization fails.
|
||
/// </summary>
|
||
public class AcBinaryDeserializationException : Exception
|
||
{
|
||
public int Position { get; }
|
||
public Type? TargetType { get; }
|
||
|
||
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
|
||
: base(message, innerException)
|
||
{
|
||
Position = position;
|
||
TargetType = targetType;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// High-performance binary deserializer matching AcBinarySerializer.
|
||
/// Features:
|
||
/// - VarInt decoding for compact integers
|
||
/// - String intern table lookup
|
||
/// - Property name table for fast property resolution
|
||
/// - Reference resolution for circular/shared references
|
||
/// - Populate/Merge mode support
|
||
/// - Optimized with FrozenDictionary for type dispatch
|
||
/// - Zero-allocation hot paths using Span and MemoryMarshal
|
||
/// </summary>
|
||
public static partial class AcBinaryDeserializer
|
||
{
|
||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||
|
||
// Type dispatch table for fast ReadValue
|
||
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
||
|
||
private static readonly TypeReader?[] TypeReaders = new TypeReader[byte.MaxValue + 1];
|
||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||
|
||
static AcBinaryDeserializer()
|
||
{
|
||
RegisterReader(BinaryTypeCode.Null, static (ref BinaryDeserializationContext _, Type _, int _) => null);
|
||
RegisterReader(BinaryTypeCode.True, static (ref BinaryDeserializationContext _, Type _, int _) => true);
|
||
RegisterReader(BinaryTypeCode.False, static (ref BinaryDeserializationContext _, Type _, int _) => false);
|
||
RegisterReader(BinaryTypeCode.Int8, static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte());
|
||
RegisterReader(BinaryTypeCode.UInt8, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte());
|
||
RegisterReader(BinaryTypeCode.Int16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe());
|
||
RegisterReader(BinaryTypeCode.UInt16, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe());
|
||
RegisterReader(BinaryTypeCode.Int32, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type));
|
||
RegisterReader(BinaryTypeCode.UInt32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt());
|
||
RegisterReader(BinaryTypeCode.Int64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong());
|
||
RegisterReader(BinaryTypeCode.UInt64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong());
|
||
RegisterReader(BinaryTypeCode.Float32, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe());
|
||
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
||
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
||
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
||
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ref ctx));
|
||
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
||
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
||
RegisterReader(BinaryTypeCode.StringInternNew, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndRegisterInternedString(ref ctx));
|
||
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
||
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
||
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
||
RegisterReader(BinaryTypeCode.Guid, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe());
|
||
RegisterReader(BinaryTypeCode.Enum, static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type));
|
||
RegisterReader(BinaryTypeCode.Object, ReadObject);
|
||
RegisterReader(BinaryTypeCode.ObjectRef, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()));
|
||
RegisterReader(BinaryTypeCode.Array, ReadArray);
|
||
RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary);
|
||
RegisterReader(BinaryTypeCode.ByteArray, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx));
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void RegisterReader(byte typeCode, TypeReader reader) => TypeReaders[typeCode] = reader;
|
||
|
||
#region Public API
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static T? Deserialize<T>(byte[] data) => Deserialize<T>(data.AsSpan());
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySpan<byte> data)
|
||
{
|
||
if (data.Length == 0) return default;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
|
||
|
||
var targetType = typeof(T);
|
||
var context = new BinaryDeserializationContext(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var result = ReadValue(ref context, targetType, 0);
|
||
return (T?)result;
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType)
|
||
{
|
||
if (data.Length == 0) return null;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
|
||
|
||
var context = new BinaryDeserializationContext(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
return ReadValue(ref context, targetType, 0);
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, T target) where T : class
|
||
=> Populate(data.AsSpan(), target);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data.
|
||
/// </summary>
|
||
public static void Populate<T>(ReadOnlySpan<byte> data, T target) where T : class
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
if (data.Length == 0) return;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||
|
||
var targetType = target.GetType();
|
||
var context = new BinaryDeserializationContext(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte(); // consume Object marker
|
||
PopulateObject(ref context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte(); // consume Array marker
|
||
PopulateList(ref context, targetList, targetType, 0);
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to populate object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections.
|
||
/// </summary>
|
||
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target) where T : class
|
||
=> PopulateMerge(data, target, null);
|
||
|
||
/// <summary>
|
||
/// Populate with merge semantics for IId collections.
|
||
/// </summary>
|
||
/// <param name="data">Binary data to deserialize</param>
|
||
/// <param name="target">Target object to populate</param>
|
||
/// <param name="options">Optional serializer options. When RemoveOrphanedItems is true,
|
||
/// items in destination collections that have no matching Id in source will be removed.</param>
|
||
public static void PopulateMerge<T>(ReadOnlySpan<byte> data, T target, AcBinarySerializerOptions? options) where T : class
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
if (data.Length == 0) return;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;
|
||
|
||
var targetType = target.GetType();
|
||
var context = new BinaryDeserializationContext(data)
|
||
{
|
||
IsMergeMode = true,
|
||
RemoveOrphanedItems = options?.RemoveOrphanedItems ?? false
|
||
};
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte();
|
||
PopulateObjectMerge(ref context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte();
|
||
PopulateListMerge(ref context, targetList, targetType, 0);
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Value Reading
|
||
|
||
/// <summary>
|
||
/// Optimized value reader using FrozenDictionary dispatch table.
|
||
/// </summary>
|
||
private static object? ReadValue(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
if (context.IsAtEnd) return null;
|
||
|
||
var typeCode = context.ReadByte();
|
||
|
||
// Handle null first
|
||
if (typeCode == BinaryTypeCode.Null) return null;
|
||
|
||
// Handle tiny int (most common case for small integers)
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
{
|
||
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
return ConvertToTargetType(intValue, targetType);
|
||
}
|
||
|
||
var reader = TypeReaders[typeCode];
|
||
if (reader != null)
|
||
{
|
||
return reader(ref context, targetType, depth);
|
||
}
|
||
|
||
throw new AcBinaryDeserializationException(
|
||
$"Unknown type code: {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sima string olvas<61>sa - NEM regisztr<74>l az intern t<>bl<62>ba.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
return context.ReadStringUtf8(length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <20>j intern<72>lt string olvas<61>sa <20>s regisztr<74>l<EFBFBD>sa az intern t<>bl<62>ba.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
var str = context.ReadStringUtf8(length);
|
||
context.RegisterInternedString(str);
|
||
return str;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Read a string and register it in the intern table for future references.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadAndInternString(ref BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
var str = context.ReadStringUtf8(length);
|
||
// Always register strings that meet the minimum intern length threshold
|
||
if (str.Length >= context.MinStringInternLength)
|
||
{
|
||
context.RegisterInternedString(str);
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadInt32Value(ref BinaryDeserializationContext context, Type targetType)
|
||
{
|
||
var value = context.ReadVarInt();
|
||
return ConvertToTargetType(value, targetType);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ConvertToTargetType(int value, Type targetType)
|
||
{
|
||
var info = GetConversionInfo(targetType);
|
||
if (info.IsEnum)
|
||
return Enum.ToObject(info.UnderlyingType, value);
|
||
|
||
return info.TypeCode switch
|
||
{
|
||
TypeCode.Int32 => value,
|
||
TypeCode.Int64 => (long)value,
|
||
TypeCode.Int16 => (short)value,
|
||
TypeCode.Byte => (byte)value,
|
||
TypeCode.SByte => (sbyte)value,
|
||
TypeCode.UInt16 => (ushort)value,
|
||
TypeCode.UInt32 => (uint)value,
|
||
TypeCode.UInt64 => (ulong)value,
|
||
TypeCode.Double => (double)value,
|
||
TypeCode.Single => (float)value,
|
||
TypeCode.Decimal => (decimal)value,
|
||
_ => value
|
||
};
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeConversionInfo GetConversionInfo(Type targetType)
|
||
=> TypeConversionCache.GetOrAdd(targetType, static type =>
|
||
{
|
||
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
||
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
|
||
});
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
|
||
{
|
||
var info = GetConversionInfo(targetType);
|
||
var nextByte = context.ReadByte();
|
||
|
||
int intValue;
|
||
if (BinaryTypeCode.IsTinyInt(nextByte))
|
||
{
|
||
intValue = BinaryTypeCode.DecodeTinyInt(nextByte);
|
||
}
|
||
else if (nextByte == BinaryTypeCode.Int32)
|
||
{
|
||
intValue = context.ReadVarInt();
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Invalid enum encoding: {nextByte}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static byte[] ReadByteArray(ref BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return [];
|
||
return context.ReadBytes(length);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Object Reading
|
||
|
||
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
// Read reference ID if present
|
||
int refId = -1;
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
refId = context.ReadVarInt();
|
||
}
|
||
|
||
// Handle dictionary types
|
||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
var metadata = GetTypeMetadata(targetType);
|
||
var instance = CreateInstance(targetType, metadata);
|
||
if (instance == null) return null;
|
||
|
||
// Register reference
|
||
if (refId > 0)
|
||
{
|
||
context.RegisterObject(refId, instance);
|
||
}
|
||
|
||
PopulateObject(ref context, instance, metadata, depth);
|
||
return instance;
|
||
}
|
||
|
||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||
{
|
||
var metadata = GetTypeMetadata(targetType);
|
||
|
||
// Skip ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
var refId = context.ReadVarInt();
|
||
if (refId > 0)
|
||
{
|
||
context.RegisterObject(refId, target);
|
||
}
|
||
}
|
||
|
||
PopulateObject(ref context, target, metadata, depth);
|
||
}
|
||
|
||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth)
|
||
{
|
||
var propertyCount = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < propertyCount; i++)
|
||
{
|
||
string propertyName;
|
||
if (context.HasMetadata)
|
||
{
|
||
var propIndex = (int)context.ReadVarUInt();
|
||
propertyName = context.GetPropertyName(propIndex);
|
||
}
|
||
else
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode == BinaryTypeCode.String)
|
||
{
|
||
propertyName = ReadAndInternString(ref context);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.StringInterned)
|
||
{
|
||
propertyName = context.GetInternedString((int)context.ReadVarUInt());
|
||
}
|
||
else if (typeCode == BinaryTypeCode.StringEmpty)
|
||
{
|
||
propertyName = string.Empty;
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Expected string for property name, got: {typeCode}",
|
||
context.Position, target.GetType());
|
||
}
|
||
}
|
||
|
||
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
|
||
{
|
||
// Skip unknown property
|
||
SkipValue(ref context);
|
||
continue;
|
||
}
|
||
|
||
// OPTIMIZATION: Reuse existing nested objects instead of creating new ones
|
||
var peekCode = context.PeekByte();
|
||
|
||
// Handle nested complex objects - reuse existing if available
|
||
if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
|
||
{
|
||
var existingObj = propInfo.GetValue(target);
|
||
if (existingObj != null)
|
||
{
|
||
context.ReadByte(); // consume Object marker
|
||
PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Handle collections - reuse existing collection and populate items
|
||
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
|
||
{
|
||
var existingCollection = propInfo.GetValue(target);
|
||
if (existingCollection is IList existingList)
|
||
{
|
||
context.ReadByte(); // consume Array marker
|
||
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Default: read value and set (for primitives, strings, null cases)
|
||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||
propInfo.SetValue(target, value);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate nested object, reusing existing object and recursively updating properties.
|
||
/// </summary>
|
||
private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||
{
|
||
var metadata = GetTypeMetadata(targetType);
|
||
|
||
// Handle ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
var refId = context.ReadVarInt();
|
||
if (refId > 0)
|
||
{
|
||
context.RegisterObject(refId, target);
|
||
}
|
||
}
|
||
|
||
PopulateObject(ref context, target, metadata, depth);
|
||
}
|
||
|
||
private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||
{
|
||
var metadata = GetTypeMetadata(targetType);
|
||
|
||
// Skip ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
var refId = context.ReadVarInt();
|
||
if (refId > 0)
|
||
{
|
||
context.RegisterObject(refId, target);
|
||
}
|
||
}
|
||
|
||
var propertyCount = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < propertyCount; i++)
|
||
{
|
||
string propertyName;
|
||
if (context.HasMetadata)
|
||
{
|
||
var propIndex = (int)context.ReadVarUInt();
|
||
propertyName = context.GetPropertyName(propIndex);
|
||
}
|
||
else
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode == BinaryTypeCode.String)
|
||
{
|
||
propertyName = ReadAndInternString(ref context);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.StringInterned)
|
||
{
|
||
propertyName = context.GetInternedString((int)context.ReadVarUInt());
|
||
}
|
||
else if (typeCode == BinaryTypeCode.StringEmpty)
|
||
{
|
||
propertyName = string.Empty;
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Expected string for property name, got: {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
}
|
||
|
||
if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
|
||
{
|
||
SkipValue(ref context);
|
||
continue;
|
||
}
|
||
|
||
var peekCode = context.PeekByte();
|
||
|
||
// Handle IId collection merge
|
||
if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
|
||
{
|
||
var existingCollection = propInfo.GetValue(target);
|
||
if (existingCollection is IList existingList)
|
||
{
|
||
context.ReadByte(); // consume Array marker
|
||
MergeIIdCollection(ref context, existingList, propInfo, depth);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Handle nested object merge
|
||
if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
|
||
{
|
||
var existingObj = propInfo.GetValue(target);
|
||
if (existingObj != null)
|
||
{
|
||
context.ReadByte(); // consume Object marker
|
||
PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||
propInfo.SetValue(target, value);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized IId collection merge with capacity hints and reduced boxing.
|
||
/// </summary>
|
||
private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
||
{
|
||
var elementType = propInfo.ElementType!;
|
||
var idGetter = propInfo.ElementIdGetter!;
|
||
var idType = propInfo.ElementIdType!;
|
||
|
||
var count = existingList.Count;
|
||
var acObservable = existingList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
// Build lookup dictionary with capacity hint
|
||
Dictionary<object, object>? existingById = null;
|
||
if (count > 0)
|
||
{
|
||
existingById = new Dictionary<object, object>(count);
|
||
for (var idx = 0; idx < count; idx++)
|
||
{
|
||
var item = existingList[idx];
|
||
if (item != null)
|
||
{
|
||
var id = idGetter(item);
|
||
if (id != null && !IsDefaultValue(id, idType))
|
||
existingById[id] = item;
|
||
}
|
||
}
|
||
}
|
||
|
||
var arrayCount = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
var elementMetadata = GetTypeMetadata(elementType);
|
||
|
||
// Track which IDs we see in source (for orphan removal)
|
||
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
||
? new HashSet<object>(arrayCount)
|
||
: null;
|
||
|
||
for (int i = 0; i < arrayCount; i++)
|
||
{
|
||
var itemCode = context.PeekByte();
|
||
if (itemCode != BinaryTypeCode.Object)
|
||
{
|
||
var value = ReadValue(ref context, elementType, nextDepth);
|
||
if (value != null)
|
||
existingList.Add(value);
|
||
continue;
|
||
}
|
||
|
||
context.ReadByte(); // consume Object marker
|
||
var newItem = CreateInstance(elementType, elementMetadata);
|
||
if (newItem == null) continue;
|
||
|
||
// Read ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
var refId = context.ReadVarInt();
|
||
if (refId > 0)
|
||
context.RegisterObject(refId, newItem);
|
||
}
|
||
|
||
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
|
||
|
||
var itemId = idGetter(newItem);
|
||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||
{
|
||
// Track this ID as seen in source
|
||
sourceIds?.Add(itemId);
|
||
|
||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||
{
|
||
// Copy properties to existing item
|
||
CopyProperties(newItem, existingItem, elementMetadata);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
existingList.Add(newItem);
|
||
}
|
||
|
||
// Remove orphaned items (items in destination but not in source)
|
||
if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
|
||
{
|
||
// Find items to remove (those not in sourceIds)
|
||
var itemsToRemove = new List<object>();
|
||
foreach (var kvp in existingById)
|
||
{
|
||
if (!sourceIds.Contains(kvp.Key))
|
||
{
|
||
itemsToRemove.Add(kvp.Value);
|
||
}
|
||
}
|
||
|
||
// Remove orphaned items
|
||
foreach (var item in itemsToRemove)
|
||
{
|
||
existingList.Remove(item);
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
var props = metadata.PropertiesArray;
|
||
for (var i = 0; i < props.Length; i++)
|
||
{
|
||
var prop = props[i];
|
||
var value = prop.GetValue(source);
|
||
if (value != null)
|
||
prop.SetValue(target, value);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines if a type is a complex type (not primitive, string, or simple value type).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static bool IsComplexType(Type type)
|
||
{
|
||
if (type.IsPrimitive) return false;
|
||
if (ReferenceEquals(type, StringType)) return false;
|
||
if (type.IsEnum) return false;
|
||
if (ReferenceEquals(type, GuidType)) return false;
|
||
if (ReferenceEquals(type, DateTimeType)) return false;
|
||
if (ReferenceEquals(type, DecimalType)) return false;
|
||
if (ReferenceEquals(type, TimeSpanType)) return false;
|
||
if (ReferenceEquals(type, DateTimeOffsetType)) return false;
|
||
if (Nullable.GetUnderlyingType(type) != null) return false;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized list populate that reuses existing items when possible.
|
||
/// </summary>
|
||
private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
|
||
{
|
||
var elementType = propInfo.ElementType ?? typeof(object);
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
var acObservable = existingList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
var existingCount = existingList.Count;
|
||
var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var peekCode = context.PeekByte();
|
||
|
||
// If we have an existing item at this index and the incoming is an object, reuse it
|
||
if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
||
{
|
||
var existingItem = existingList[i];
|
||
if (existingItem != null)
|
||
{
|
||
context.ReadByte(); // consume Object marker
|
||
|
||
// Handle ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
var refId = context.ReadVarInt();
|
||
if (refId > 0)
|
||
{
|
||
context.RegisterObject(refId, existingItem);
|
||
}
|
||
}
|
||
|
||
PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Read new value
|
||
var value = ReadValue(ref context, elementType, nextDepth);
|
||
|
||
if (i < existingCount)
|
||
{
|
||
// Replace existing item
|
||
existingList[i] = value;
|
||
}
|
||
else
|
||
{
|
||
// Add new item
|
||
existingList.Add(value);
|
||
}
|
||
}
|
||
|
||
// Remove extra items if new list is shorter
|
||
while (existingList.Count > count)
|
||
{
|
||
existingList.RemoveAt(existingList.Count - 1);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Array Reading
|
||
|
||
private static object? ReadArray(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
var elementType = GetCollectionElementType(targetType);
|
||
if (elementType == null) elementType = typeof(object);
|
||
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
// Optimized path for primitive arrays
|
||
if (targetType.IsArray && count > 0)
|
||
{
|
||
var result = TryReadPrimitiveArray(ref context, elementType, count);
|
||
if (result != null) return result;
|
||
}
|
||
|
||
if (targetType.IsArray)
|
||
{
|
||
var array = Array.CreateInstance(elementType, count);
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(ref context, elementType, nextDepth);
|
||
array.SetValue(value, i);
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
IList? list = null;
|
||
try
|
||
{
|
||
var instance = Activator.CreateInstance(targetType);
|
||
if (instance is IList l) list = l;
|
||
}
|
||
catch
|
||
{
|
||
/* Fallback to List<T> */
|
||
}
|
||
|
||
list ??= GetOrCreateListFactory(elementType)();
|
||
|
||
var acObservable = list as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(ref context, elementType, nextDepth);
|
||
list.Add(value);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
|
||
return list;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized primitive array reader using bulk operations.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static Array? TryReadPrimitiveArray(ref BinaryDeserializationContext context, Type elementType, int count)
|
||
{
|
||
// Int32 array
|
||
if (ReferenceEquals(elementType, IntType))
|
||
{
|
||
var array = new int[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
else if (typeCode == BinaryTypeCode.Int32)
|
||
array[i] = context.ReadVarInt();
|
||
else
|
||
return null; // Fall back to generic path
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Double array
|
||
if (ReferenceEquals(elementType, DoubleType))
|
||
{
|
||
var array = new double[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Float64) return null;
|
||
array[i] = context.ReadDoubleUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Long array
|
||
if (ReferenceEquals(elementType, LongType))
|
||
{
|
||
var array = new long[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||
array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||
else if (typeCode == BinaryTypeCode.Int32)
|
||
array[i] = context.ReadVarInt();
|
||
else if (typeCode == BinaryTypeCode.Int64)
|
||
array[i] = context.ReadVarLong();
|
||
else
|
||
return null;
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Bool array
|
||
if (ReferenceEquals(elementType, BoolType))
|
||
{
|
||
var array = new bool[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode == BinaryTypeCode.True) array[i] = true;
|
||
else if (typeCode == BinaryTypeCode.False) array[i] = false;
|
||
else return null;
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Guid array
|
||
if (ReferenceEquals(elementType, GuidType))
|
||
{
|
||
var array = new Guid[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Guid) return null;
|
||
array[i] = context.ReadGuidUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// Decimal array
|
||
if (ReferenceEquals(elementType, DecimalType))
|
||
{
|
||
var array = new decimal[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.Decimal) return null;
|
||
array[i] = context.ReadDecimalUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
// DateTime array
|
||
if (ReferenceEquals(elementType, DateTimeType))
|
||
{
|
||
var array = new DateTime[count];
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
if (typeCode != BinaryTypeCode.DateTime) return null;
|
||
array[i] = context.ReadDateTimeUnsafe();
|
||
}
|
||
|
||
return array;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
|
||
{
|
||
var elementType = GetCollectionElementType(listType) ?? typeof(object);
|
||
|
||
var acObservable = targetList as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
targetList.Clear();
|
||
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(ref context, elementType, nextDepth);
|
||
targetList.Add(value);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
acObservable?.EndUpdate();
|
||
}
|
||
}
|
||
|
||
private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
|
||
{
|
||
var elementType = GetCollectionElementType(listType) ?? typeof(object);
|
||
var (isId, idType) = GetIdInfo(elementType);
|
||
|
||
if (!isId || idType == null)
|
||
{
|
||
// No IId, just replace
|
||
PopulateList(ref context, targetList, listType, depth);
|
||
return;
|
||
}
|
||
|
||
// IId merge logic
|
||
var idProp = elementType.GetProperty("Id");
|
||
if (idProp == null)
|
||
{
|
||
PopulateList(ref context, targetList, listType, depth);
|
||
return;
|
||
}
|
||
|
||
var idGetter = CreateCompiledGetter(elementType, idProp);
|
||
var propInfo = new BinaryPropertySetterInfo(
|
||
"Items", elementType, true, elementType, idType, idGetter);
|
||
|
||
MergeIIdCollection(ref context, targetList, propInfo, depth);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Dictionary Reading
|
||
|
||
private static object? ReadDictionary(ref BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
if (!IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
keyType = typeof(string);
|
||
valueType = typeof(object);
|
||
}
|
||
|
||
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
private static object ReadDictionaryAsObject(ref BinaryDeserializationContext context, Type keyType, Type valueType, int depth)
|
||
{
|
||
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
|
||
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
|
||
|
||
var count = (int)context.ReadVarUInt();
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var key = ReadValue(ref context, keyType, nextDepth);
|
||
var value = ReadValue(ref context, valueType, nextDepth);
|
||
if (key != null)
|
||
dict.Add(key, value);
|
||
}
|
||
|
||
return dict;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Skip Value
|
||
|
||
private static void SkipValue(ref BinaryDeserializationContext context)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Null) return;
|
||
|
||
if (BinaryTypeCode.IsTinyInt(typeCode)) return;
|
||
|
||
switch (typeCode)
|
||
{
|
||
case BinaryTypeCode.True:
|
||
case BinaryTypeCode.False:
|
||
case BinaryTypeCode.StringEmpty:
|
||
return;
|
||
case BinaryTypeCode.Int8:
|
||
case BinaryTypeCode.UInt8:
|
||
context.Skip(1);
|
||
return;
|
||
case BinaryTypeCode.Int16:
|
||
case BinaryTypeCode.UInt16:
|
||
case BinaryTypeCode.Char:
|
||
context.Skip(2);
|
||
return;
|
||
case BinaryTypeCode.Int32:
|
||
context.ReadVarInt(); // Skip VarInt
|
||
return;
|
||
case BinaryTypeCode.UInt32:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.Float32:
|
||
context.Skip(4);
|
||
return;
|
||
case BinaryTypeCode.Int64:
|
||
context.ReadVarLong();
|
||
return;
|
||
case BinaryTypeCode.UInt64:
|
||
context.ReadVarULong();
|
||
return;
|
||
case BinaryTypeCode.Float64:
|
||
case BinaryTypeCode.TimeSpan:
|
||
context.Skip(8);
|
||
return;
|
||
case BinaryTypeCode.DateTime:
|
||
context.Skip(9);
|
||
return;
|
||
case BinaryTypeCode.DateTimeOffset:
|
||
context.Skip(10);
|
||
return;
|
||
case BinaryTypeCode.Guid:
|
||
case BinaryTypeCode.Decimal:
|
||
context.Skip(16);
|
||
return;
|
||
case BinaryTypeCode.String:
|
||
// Sima string - nem regisztr<74>lunk
|
||
SkipPlainString(ref context);
|
||
return;
|
||
case BinaryTypeCode.StringInterned:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.StringInternNew:
|
||
// <20>j intern<72>lt string - regisztr<74>lni kell m<>g skip eset<65>n is
|
||
SkipAndRegisterInternedString(ref context);
|
||
return;
|
||
case BinaryTypeCode.ByteArray:
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
context.Skip(byteLen);
|
||
return;
|
||
case BinaryTypeCode.Enum:
|
||
var enumByte = context.ReadByte();
|
||
if (BinaryTypeCode.IsTinyInt(enumByte)) return;
|
||
if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
|
||
return;
|
||
case BinaryTypeCode.Object:
|
||
SkipObject(ref context);
|
||
return;
|
||
case BinaryTypeCode.ObjectRef:
|
||
context.ReadVarInt();
|
||
return;
|
||
case BinaryTypeCode.Array:
|
||
SkipArray(ref context);
|
||
return;
|
||
case BinaryTypeCode.Dictionary:
|
||
SkipDictionary(ref context);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sima string kihagy<67>sa - NEM regisztr<74>l.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
||
{
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen > 0)
|
||
{
|
||
context.Skip(byteLen);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// <20>j intern<72>lt string kihagy<67>sa - DE regisztr<74>lni kell!
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||
{
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen == 0) return;
|
||
var str = context.ReadStringUtf8(byteLen);
|
||
context.RegisterInternedString(str);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip a string but still register it in the intern table if it meets the length threshold.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipAndInternString(ref BinaryDeserializationContext context)
|
||
{
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen == 0) return;
|
||
|
||
var str = context.ReadStringUtf8(byteLen);
|
||
if (str.Length >= context.MinStringInternLength)
|
||
{
|
||
context.RegisterInternedString(str);
|
||
}
|
||
}
|
||
|
||
private static void SkipObject(ref BinaryDeserializationContext context)
|
||
{
|
||
// Skip ref ID if present
|
||
if (context.HasReferenceHandling)
|
||
{
|
||
context.ReadVarInt();
|
||
}
|
||
|
||
var propCount = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < propCount; i++)
|
||
{
|
||
// Skip property name - but must register in intern table!
|
||
if (context.HasMetadata)
|
||
{
|
||
context.ReadVarUInt();
|
||
}
|
||
else
|
||
{
|
||
var nameCode = context.ReadByte();
|
||
if (nameCode == BinaryTypeCode.String)
|
||
{
|
||
// CRITICAL FIX: Must register property name in intern table even when skipping!
|
||
SkipAndInternString(ref context);
|
||
}
|
||
else if (nameCode == BinaryTypeCode.StringInterned)
|
||
{
|
||
// Just read the index, no registration needed
|
||
context.ReadVarUInt();
|
||
}
|
||
// StringEmpty doesn't need any action
|
||
}
|
||
|
||
// Skip value
|
||
SkipValue(ref context);
|
||
}
|
||
}
|
||
|
||
private static void SkipArray(ref BinaryDeserializationContext context)
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(ref context);
|
||
}
|
||
}
|
||
|
||
private static void SkipDictionary(ref BinaryDeserializationContext context)
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(ref context); // key
|
||
SkipValue(ref context); // value
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Type Metadata
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
|
||
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
if (metadata.CompiledConstructor != null)
|
||
return metadata.CompiledConstructor();
|
||
|
||
try
|
||
{
|
||
return Activator.CreateInstance(type);
|
||
}
|
||
catch (MissingMethodException ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot create instance of type '{type.FullName}' because it does not have a parameterless constructor.",
|
||
0, type, ex);
|
||
}
|
||
}
|
||
|
||
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
|
||
{
|
||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||
var castExpr = Expression.Convert(objParam, declaringType);
|
||
var propAccess = Expression.Property(castExpr, prop);
|
||
var boxed = Expression.Convert(propAccess, typeof(object));
|
||
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
|
||
}
|
||
|
||
#endregion
|
||
|
||
// Implementation moved to AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
|
||
}
|
||
|
||
sealed class TypeConversionInfo
|
||
{
|
||
public Type UnderlyingType { get; }
|
||
public TypeCode TypeCode { get; }
|
||
public bool IsEnum { get; }
|
||
|
||
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
|
||
{
|
||
UnderlyingType = underlyingType;
|
||
TypeCode = typeCode;
|
||
IsEnum = isEnum;
|
||
}
|
||
}
|
||
|
||
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs
|