AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs

1364 lines
48 KiB
C#
Raw Blame History

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