1831 lines
66 KiB
C#
1831 lines
66 KiB
C#
using System.Buffers;
|
||
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.Extensions.JsonUtilities;
|
||
|
||
namespace AyCode.Core.Extensions;
|
||
|
||
/// <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 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
|
||
{
|
||
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 };
|
||
|
||
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);
|
||
|
||
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) && existingById != null)
|
||
{
|
||
if (existingById.TryGetValue(itemId, out var existingItem))
|
||
{
|
||
// Copy properties to existing item
|
||
CopyProperties(newItem, existingItem, elementMetadata);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
existingList.Add(newItem);
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
|
||
internal sealed class BinaryDeserializeTypeMetadata
|
||
{
|
||
private readonly FrozenDictionary<string, BinaryPropertySetterInfo> _propertiesDict;
|
||
public BinaryPropertySetterInfo[] PropertiesArray { get; }
|
||
public Func<object>? CompiledConstructor { get; }
|
||
|
||
public BinaryDeserializeTypeMetadata(Type type)
|
||
{
|
||
var ctor = type.GetConstructor(Type.EmptyTypes);
|
||
if (ctor != null)
|
||
{
|
||
var newExpr = Expression.New(type);
|
||
var boxed = Expression.Convert(newExpr, typeof(object));
|
||
CompiledConstructor = Expression.Lambda<Func<object>>(boxed).Compile();
|
||
}
|
||
|
||
var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||
var propsList = new List<PropertyInfo>();
|
||
|
||
foreach (var p in allProps)
|
||
{
|
||
if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue;
|
||
if (HasJsonIgnoreAttribute(p)) continue;
|
||
propsList.Add(p);
|
||
}
|
||
|
||
var propInfos = new BinaryPropertySetterInfo[propsList.Count];
|
||
for (int i = 0; i < propsList.Count; i++)
|
||
{
|
||
propInfos[i] = new BinaryPropertySetterInfo(propsList[i], type);
|
||
}
|
||
|
||
PropertiesArray = propInfos;
|
||
var dict = new Dictionary<string, BinaryPropertySetterInfo>(propInfos.Length, StringComparer.OrdinalIgnoreCase);
|
||
foreach (var propInfo in propInfos)
|
||
{
|
||
dict[propInfo.Name] = propInfo;
|
||
}
|
||
|
||
_propertiesDict = FrozenDictionary.ToFrozenDictionary(dict, StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propInfo)
|
||
=> _propertiesDict.TryGetValue(name, out propInfo);
|
||
}
|
||
|
||
internal sealed class BinaryPropertySetterInfo
|
||
{
|
||
public readonly string Name;
|
||
public readonly Type PropertyType;
|
||
public readonly Type UnderlyingType;
|
||
public readonly bool IsIIdCollection;
|
||
public readonly bool IsComplexType;
|
||
public readonly bool IsCollection;
|
||
public readonly Type? ElementType;
|
||
public readonly Type? ElementIdType;
|
||
public readonly Func<object, object?>? ElementIdGetter;
|
||
|
||
private readonly Action<object, object?> _setter;
|
||
private readonly Func<object, object?> _getter;
|
||
|
||
public BinaryPropertySetterInfo(PropertyInfo prop, Type declaringType)
|
||
{
|
||
Name = prop.Name;
|
||
PropertyType = prop.PropertyType;
|
||
UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType;
|
||
|
||
_setter = CreateCompiledSetter(declaringType, prop);
|
||
_getter = CreateCompiledGetter(declaringType, prop);
|
||
|
||
ElementType = GetCollectionElementType(PropertyType);
|
||
IsCollection = ElementType != null && ElementType != typeof(object) &&
|
||
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
|
||
!ReferenceEquals(PropertyType, StringType);
|
||
|
||
// Determine if this is a complex type that can be populated
|
||
IsComplexType = !PropertyType.IsPrimitive &&
|
||
!ReferenceEquals(PropertyType, StringType) &&
|
||
!PropertyType.IsEnum &&
|
||
!ReferenceEquals(PropertyType, GuidType) &&
|
||
!ReferenceEquals(PropertyType, DateTimeType) &&
|
||
!ReferenceEquals(PropertyType, DecimalType) &&
|
||
!ReferenceEquals(PropertyType, TimeSpanType) &&
|
||
!ReferenceEquals(PropertyType, DateTimeOffsetType) &&
|
||
Nullable.GetUnderlyingType(PropertyType) == null &&
|
||
!IsCollection;
|
||
|
||
if (IsCollection && ElementType != null)
|
||
{
|
||
var idInfo = GetIdInfo(ElementType);
|
||
if (idInfo.IsId)
|
||
{
|
||
IsIIdCollection = true;
|
||
ElementIdType = idInfo.IdType;
|
||
var idProp = ElementType.GetProperty("Id");
|
||
if (idProp != null)
|
||
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Constructor for manual creation (merge scenarios)
|
||
public BinaryPropertySetterInfo(string name, Type propertyType, bool isIIdCollection, Type? elementType, Type? elementIdType, Func<object, object?>? elementIdGetter)
|
||
{
|
||
Name = name;
|
||
PropertyType = propertyType;
|
||
UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType;
|
||
IsIIdCollection = isIIdCollection;
|
||
IsCollection = elementType != null;
|
||
IsComplexType = false;
|
||
ElementType = elementType;
|
||
ElementIdType = elementIdType;
|
||
ElementIdGetter = elementIdGetter;
|
||
_setter = (_, _) => { };
|
||
_getter = _ => null;
|
||
}
|
||
|
||
private static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
|
||
{
|
||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||
var valueParam = Expression.Parameter(typeof(object), "value");
|
||
var castObj = Expression.Convert(objParam, declaringType);
|
||
var castValue = Expression.Convert(valueParam, prop.PropertyType);
|
||
var propAccess = Expression.Property(castObj, prop);
|
||
var assign = Expression.Assign(propAccess, castValue);
|
||
return Expression.Lambda<Action<object, object?>>(assign, objParam, valueParam).Compile();
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void SetValue(object target, object? value) => _setter(target, value);
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public object? GetValue(object target) => _getter(target);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Deserialization Context
|
||
|
||
/// <summary>
|
||
/// Optimized deserialization context using ref struct for zero allocation.
|
||
/// Uses MemoryMarshal for fast primitive reads.
|
||
/// </summary>
|
||
internal ref struct BinaryDeserializationContext
|
||
{
|
||
private readonly ReadOnlySpan<byte> _data;
|
||
private int _position;
|
||
|
||
// Header info
|
||
public byte FormatVersion { get; private set; }
|
||
public bool HasMetadata { get; private set; }
|
||
public bool HasReferenceHandling { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Minimum string length for interning. Must match serializer's MinStringInternLength.
|
||
/// Default: 4 (from AcBinarySerializerOptions)
|
||
/// </summary>
|
||
public byte MinStringInternLength { get; private set; }
|
||
|
||
// Property name table
|
||
private string[]? _propertyNames;
|
||
|
||
// Interned strings - dynamically built during deserialization
|
||
private List<string>? _internedStrings;
|
||
|
||
// Reference map
|
||
private Dictionary<int, object>? _references;
|
||
|
||
public bool IsMergeMode { get; set; }
|
||
public int Position => _position;
|
||
public bool IsAtEnd => _position >= _data.Length;
|
||
|
||
public BinaryDeserializationContext(ReadOnlySpan<byte> data)
|
||
{
|
||
_data = data;
|
||
_position = 0;
|
||
FormatVersion = 0;
|
||
HasMetadata = false;
|
||
HasReferenceHandling = true; // Assume true by default
|
||
MinStringInternLength = 4; // Default from AcBinarySerializerOptions
|
||
_propertyNames = null;
|
||
_internedStrings = null;
|
||
_references = null;
|
||
IsMergeMode = false;
|
||
}
|
||
|
||
public void ReadHeader()
|
||
{
|
||
if (_data.Length < 2) return;
|
||
|
||
FormatVersion = ReadByte();
|
||
var flags = ReadByte();
|
||
|
||
// Handle new flag-based header format (34+)
|
||
if (flags >= BinaryTypeCode.HeaderFlagsBase)
|
||
{
|
||
HasMetadata = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
|
||
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
|
||
}
|
||
else
|
||
{
|
||
// Legacy format: MetadataHeader (32) or NoMetadataHeader (33)
|
||
// These always implied HasReferenceHandling = true
|
||
HasMetadata = flags == BinaryTypeCode.MetadataHeader;
|
||
HasReferenceHandling = true;
|
||
}
|
||
|
||
if (HasMetadata)
|
||
{
|
||
// Read property names
|
||
var propCount = (int)ReadVarUInt();
|
||
if (propCount > 0)
|
||
{
|
||
_propertyNames = new string[propCount];
|
||
for (int i = 0; i < propCount; i++)
|
||
{
|
||
var len = (int)ReadVarUInt();
|
||
_propertyNames[i] = ReadStringUtf8(len);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public byte ReadByte()
|
||
{
|
||
if (_position >= _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
return _data[_position++];
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public byte PeekByte()
|
||
{
|
||
if (_position >= _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
return _data[_position];
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void Skip(int count)
|
||
{
|
||
_position += count;
|
||
if (_position > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public byte[] ReadBytes(int count)
|
||
{
|
||
if (_position + count > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = _data.Slice(_position, count).ToArray();
|
||
_position += count;
|
||
return result;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public int ReadVarInt()
|
||
{
|
||
var encoded = ReadVarUInt();
|
||
// ZigZag decode
|
||
return (int)((encoded >> 1) ^ -(encoded & 1));
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public uint ReadVarUInt()
|
||
{
|
||
uint result = 0;
|
||
int shift = 0;
|
||
|
||
while (true)
|
||
{
|
||
var b = ReadByte();
|
||
result |= (uint)(b & 0x7F) << shift;
|
||
if ((b & 0x80) == 0) break;
|
||
shift += 7;
|
||
if (shift > 28)
|
||
throw new AcBinaryDeserializationException("Invalid VarInt", _position);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public long ReadVarLong()
|
||
{
|
||
var encoded = ReadVarULong();
|
||
// ZigZag decode
|
||
return (long)((encoded >> 1) ^ (0 - (encoded & 1)));
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public ulong ReadVarULong()
|
||
{
|
||
ulong result = 0;
|
||
int shift = 0;
|
||
|
||
while (true)
|
||
{
|
||
var b = ReadByte();
|
||
result |= (ulong)(b & 0x7F) << shift;
|
||
if ((b & 0x80) == 0) break;
|
||
shift += 7;
|
||
if (shift > 63)
|
||
throw new AcBinaryDeserializationException("Invalid VarLong", _position);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized Int16 read using direct memory access.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public short ReadInt16Unsafe()
|
||
{
|
||
if (_position + 2 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 2;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized UInt16 read using direct memory access.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public ushort ReadUInt16Unsafe()
|
||
{
|
||
if (_position + 2 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = Unsafe.ReadUnaligned<ushort>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 2;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized float read using direct memory access.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public float ReadSingleUnsafe()
|
||
{
|
||
if (_position + 4 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = Unsafe.ReadUnaligned<float>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 4;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized double read using direct memory access.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public double ReadDoubleUnsafe()
|
||
{
|
||
if (_position + 8 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = Unsafe.ReadUnaligned<double>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 8;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized decimal read using direct memory copy.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public decimal ReadDecimalUnsafe()
|
||
{
|
||
if (_position + 16 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
|
||
Span<int> bits = stackalloc int[4];
|
||
MemoryMarshal.Cast<byte, int>(_data.Slice(_position, 16)).CopyTo(bits);
|
||
_position += 16;
|
||
return new decimal(bits);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized char read.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public char ReadCharUnsafe()
|
||
{
|
||
if (_position + 2 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = Unsafe.ReadUnaligned<char>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 2;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized DateTime read.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public DateTime ReadDateTimeUnsafe()
|
||
{
|
||
if (_position + 9 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
|
||
var kind = (DateTimeKind)_data[_position + 8];
|
||
_position += 9;
|
||
return new DateTime(ticks, kind);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized DateTimeOffset read.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public DateTimeOffset ReadDateTimeOffsetUnsafe()
|
||
{
|
||
if (_position + 10 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var utcTicks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
|
||
var offsetMinutes = Unsafe.ReadUnaligned<short>(ref Unsafe.AsRef(in _data[_position + 8]));
|
||
_position += 10;
|
||
var offset = TimeSpan.FromMinutes(offsetMinutes);
|
||
var localTicks = utcTicks + offset.Ticks;
|
||
return new DateTimeOffset(localTicks, offset);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized TimeSpan read.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public TimeSpan ReadTimeSpanUnsafe()
|
||
{
|
||
if (_position + 8 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var ticks = Unsafe.ReadUnaligned<long>(ref Unsafe.AsRef(in _data[_position]));
|
||
_position += 8;
|
||
return new TimeSpan(ticks);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized Guid read.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public Guid ReadGuidUnsafe()
|
||
{
|
||
if (_position + 16 > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
var result = new Guid(_data.Slice(_position, 16));
|
||
_position += 16;
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized string read using UTF8 span decoding.
|
||
/// Uses String.Create to decode directly into the target string buffer to avoid intermediate allocations.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public string ReadStringUtf8(int byteCount)
|
||
{
|
||
if (_position + byteCount > _data.Length)
|
||
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
||
|
||
var src = _data.Slice(_position, byteCount);
|
||
var result = Utf8NoBom.GetString(src);
|
||
_position += byteCount;
|
||
return result;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public string GetPropertyName(int index)
|
||
{
|
||
if (_propertyNames == null || index < 0 || index >= _propertyNames.Length)
|
||
throw new AcBinaryDeserializationException($"Invalid property name index: {index}", _position);
|
||
return _propertyNames[index];
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void RegisterInternedString(string value)
|
||
{
|
||
_internedStrings ??= new List<string>(16);
|
||
_internedStrings.Add(value);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public string GetInternedString(int index)
|
||
{
|
||
if (_internedStrings == null || index < 0 || index >= _internedStrings.Count)
|
||
throw new AcBinaryDeserializationException($"Invalid interned string index: {index}. Interned strings count: {_internedStrings?.Count ?? 0}", _position);
|
||
return _internedStrings[index];
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void RegisterObject(int refId, object obj)
|
||
{
|
||
_references ??= new Dictionary<int, object>();
|
||
_references[refId] = obj;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public object? GetReferencedObject(int refId)
|
||
{
|
||
if (_references != null && _references.TryGetValue(refId, out var obj))
|
||
return obj;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
private 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;
|
||
}
|
||
}
|
||
}
|