1881 lines
70 KiB
C#
1881 lines
70 KiB
C#
using System;
|
||
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 AyCode.Core.Serializers.Expressions;
|
||
using static AyCode.Core.Helpers.JsonUtilities;
|
||
|
||
namespace AyCode.Core.Serializers.Binaries;
|
||
|
||
/// <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, TypeConversionInfo> TypeConversionCache = new();
|
||
|
||
/// <summary>
|
||
/// ThreadLocal cache for type conversion info.
|
||
/// </summary>
|
||
[ThreadStatic]
|
||
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
||
|
||
// Type dispatch table for fast ReadValue
|
||
private delegate object? TypeReader(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 (BinaryDeserializationContext _, Type _, int _) => null);
|
||
RegisterReader(BinaryTypeCode.True, static (BinaryDeserializationContext _, Type _, int _) => true);
|
||
RegisterReader(BinaryTypeCode.False, static (BinaryDeserializationContext _, Type _, int _) => false);
|
||
RegisterReader(BinaryTypeCode.Int8, static (BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte());
|
||
RegisterReader(BinaryTypeCode.UInt8, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte());
|
||
RegisterReader(BinaryTypeCode.Int16, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe());
|
||
RegisterReader(BinaryTypeCode.UInt16, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe());
|
||
RegisterReader(BinaryTypeCode.Int32, static (BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ctx, type));
|
||
RegisterReader(BinaryTypeCode.UInt32, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt());
|
||
RegisterReader(BinaryTypeCode.Int64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong());
|
||
RegisterReader(BinaryTypeCode.UInt64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong());
|
||
RegisterReader(BinaryTypeCode.Float32, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe());
|
||
RegisterReader(BinaryTypeCode.Float64, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
||
RegisterReader(BinaryTypeCode.Decimal, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
||
RegisterReader(BinaryTypeCode.Char, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
||
RegisterReader(BinaryTypeCode.String, static (BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ctx));
|
||
RegisterReader(BinaryTypeCode.StringInterned, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
||
RegisterReader(BinaryTypeCode.StringEmpty, static (BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
||
// StringInternFirst: first occurrence of interned string - read cacheIndex + content + register in cache
|
||
RegisterReader(BinaryTypeCode.StringInternFirst, static (BinaryDeserializationContext ctx, Type _, int _) =>
|
||
ReadAndRegisterInternedString(ctx));
|
||
RegisterReader(BinaryTypeCode.DateTime, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
||
RegisterReader(BinaryTypeCode.DateTimeOffset, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
||
RegisterReader(BinaryTypeCode.TimeSpan, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
||
RegisterReader(BinaryTypeCode.Guid, static (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe());
|
||
RegisterReader(BinaryTypeCode.Enum, static (BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ctx, type));
|
||
RegisterReader(BinaryTypeCode.Object, ReadObject);
|
||
RegisterReader(BinaryTypeCode.ObjectRefFirst, ReadObjectRefFirst);
|
||
RegisterReader(BinaryTypeCode.ObjectWithMetadata, ReadObjectWithMetadata);
|
||
RegisterReader(BinaryTypeCode.ObjectWithMetadataRefFirst, ReadObjectWithMetadataRefFirst);
|
||
RegisterReader(BinaryTypeCode.ObjectRef, ReadObjectRef);
|
||
RegisterReader(BinaryTypeCode.Array, ReadArray);
|
||
RegisterReader(BinaryTypeCode.Dictionary, ReadDictionary);
|
||
RegisterReader(BinaryTypeCode.ByteArray, static (BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ctx));
|
||
|
||
// Register FixStr readers (34-65)
|
||
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(code);
|
||
RegisterReader(code, CreateFixStrReader(length));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a reader for FixStr with the given length.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeReader CreateFixStrReader(int length)
|
||
{
|
||
if (length == 0)
|
||
return static (BinaryDeserializationContext _, Type _, int _) => string.Empty;
|
||
|
||
return (BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadStringUtf8(length);
|
||
}
|
||
|
||
[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(), AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T with options.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static T? Deserialize<T>(byte[] data, AcBinarySerializerOptions options) => Deserialize<T>(data.AsSpan(), options);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySpan<byte> data) => Deserialize<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to object of type T with options.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return default;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
|
||
|
||
var targetType = typeof(T);
|
||
|
||
// Handle Expression types - deserialize as AcExpressionNode and rebuild
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
{
|
||
return (T?)(object?)DeserializeExpression(data, targetType, options);
|
||
}
|
||
|
||
var context = DeserializationContextPool.Get(options);
|
||
context.InitBufferFromSpan(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var result = ReadValue(context, targetType, 0);
|
||
// Position-based string interning - no validation needed
|
||
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);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType)
|
||
=> Deserialize(data, targetType, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data to specified type with options.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySpan<byte> data, Type targetType, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return null;
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
|
||
|
||
// Handle Expression types - deserialize as AcExpressionNode and rebuild
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
{
|
||
return DeserializeExpression(data, targetType, options);
|
||
}
|
||
|
||
var context = DeserializationContextPool.Get(options);
|
||
context.InitBufferFromSpan(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var result = ReadValue(context, targetType, 0);
|
||
// Position-based string interning - no validation needed
|
||
return 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);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes).
|
||
/// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySequence<byte> data)
|
||
=> Deserialize<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence with options.
|
||
/// </summary>
|
||
public static T? Deserialize<T>(ReadOnlySequence<byte> data, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return default;
|
||
|
||
if (data.IsSingleSegment)
|
||
return Deserialize<T>(data.FirstSpan, options);
|
||
|
||
var context = DeserializationContextPool.Get(options);
|
||
try
|
||
{
|
||
var buffer = context.RentLinearizedBuffer((int)data.Length);
|
||
data.CopyTo(buffer);
|
||
return Deserialize<T>(buffer.AsSpan(0, (int)data.Length), context);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence to specified type.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType)
|
||
=> Deserialize(data, targetType, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Deserialize binary data from a ReadOnlySequence to specified type with options.
|
||
/// </summary>
|
||
public static object? Deserialize(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0) return null;
|
||
|
||
if (data.IsSingleSegment)
|
||
return Deserialize(data.FirstSpan, targetType, options);
|
||
|
||
var context = DeserializationContextPool.Get(options);
|
||
try
|
||
{
|
||
var buffer = context.RentLinearizedBuffer((int)data.Length);
|
||
data.CopyTo(buffer);
|
||
return Deserialize(buffer.AsSpan(0, (int)data.Length), targetType, context);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Internal: Deserialize with pre-pooled context (used by ReadOnlySequence multi-segment path).
|
||
/// </summary>
|
||
private static T? Deserialize<T>(ReadOnlySpan<byte> data, BinaryDeserializationContext context)
|
||
{
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;
|
||
|
||
var targetType = typeof(T);
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
return (T?)(object?)DeserializeExpression(data, targetType, context);
|
||
|
||
context.InitBufferFromSpan(data);
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
return (T?)ReadValue(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>
|
||
/// Internal: Deserialize with pre-pooled context (used by ReadOnlySequence multi-segment path).
|
||
/// </summary>
|
||
private static object? Deserialize(ReadOnlySpan<byte> data, Type targetType, BinaryDeserializationContext context)
|
||
{
|
||
if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return null;
|
||
|
||
if (AcSerializerCommon.IsExpressionType(targetType))
|
||
return DeserializeExpression(data, targetType, context);
|
||
|
||
context.InitBufferFromSpan(data);
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
return ReadValue(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>
|
||
/// Internal: DeserializeExpression with pre-pooled context.
|
||
/// </summary>
|
||
private static Expression? DeserializeExpression(ReadOnlySpan<byte> data, Type targetExpressionType, BinaryDeserializationContext context)
|
||
{
|
||
context.InitBufferFromSpan(data);
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var node = (AcExpressionNode?)ReadValue(context, typeof(AcExpressionNode), 0);
|
||
if (node == null) return null;
|
||
|
||
var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
|
||
return AcExpressionRebuilder.FromNode(node, entityType);
|
||
}
|
||
catch (AcBinaryDeserializationException) { throw; }
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to deserialize Expression from binary data: {ex.Message}",
|
||
context.Position, targetExpressionType, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Deserialize Expression from binary data.
|
||
/// First deserializes as AcExpressionNode, then rebuilds the Expression tree.
|
||
/// </summary>
|
||
private static Expression? DeserializeExpression(ReadOnlySpan<byte> data, Type targetExpressionType, AcBinarySerializerOptions options)
|
||
{
|
||
var context = DeserializationContextPool.Get(options);
|
||
context.InitBufferFromSpan(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var node = (AcExpressionNode?)ReadValue(context, typeof(AcExpressionNode), 0);
|
||
// Position-based string interning - no validation needed
|
||
if (node == null) return null;
|
||
|
||
var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
|
||
return AcExpressionRebuilder.FromNode(node, entityType);
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to deserialize Expression from binary data: {ex.Message}",
|
||
context.Position, targetExpressionType, ex);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, T target) where T : class
|
||
=> Populate(data.AsSpan(), target, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data with options.
|
||
/// </summary>
|
||
public static void Populate<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
|
||
=> Populate(data.AsSpan(), target, options);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data.
|
||
/// </summary>
|
||
public static void Populate<T>(ReadOnlySpan<byte> data, T target) where T : class
|
||
=> Populate(data, target, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Populate existing object from binary data with options.
|
||
/// </summary>
|
||
public static void Populate<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 = DeserializationContextPool.Get(options);
|
||
context.InitBufferFromSpan(data);
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte(); // consume Object marker
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
|
||
{
|
||
context.ReadByte(); // consume ObjectWithMetadata marker
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte(); // consume Array marker
|
||
PopulateList(context, targetList, targetType, 0);
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
// Position-based string interning - no validation needed
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to populate object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
/// <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, AcBinarySerializerOptions.Default);
|
||
|
||
/// <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 opts = options ?? AcBinarySerializerOptions.Default;
|
||
var targetType = target.GetType();
|
||
var context = DeserializationContextPool.Get(opts);
|
||
context.InitBufferFromSpan(data);
|
||
context.IsMergeMode = true;
|
||
context.RemoveOrphanedItems = opts.RemoveOrphanedItems;
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte();
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
|
||
{
|
||
context.ReadByte();
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte();
|
||
// For top-level list merge, check if it's an IId collection
|
||
var elementType = GetCollectionElementType(targetType);
|
||
if (elementType != null)
|
||
{
|
||
var wrapper = context.GetWrapper(elementType);
|
||
var elementMetadata = wrapper.Metadata;
|
||
|
||
if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null)
|
||
{
|
||
MergeIIdCollectionWithMetadata(context, targetList, elementType, wrapper, 0);
|
||
// Position-based string interning - no validation needed
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Non-IId collection, just populate
|
||
PopulateList(context, targetList, targetType, 0);
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
// Position-based string interning - no validation needed
|
||
}
|
||
catch (AcBinaryDeserializationException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}",
|
||
context.Position, targetType, ex);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Chain API
|
||
|
||
/// <summary>
|
||
/// Create a deserialize chain that parses binary data once and allows multiple deserializations.
|
||
/// Maintains reference identity for IId objects across chain operations.
|
||
/// </summary>
|
||
public static IDeserializeChain<T> CreateDeserializeChain<T>(ReadOnlySpan<byte> data)
|
||
=> CreateDeserializeChain<T>(data, AcBinarySerializerOptions.Default);
|
||
|
||
/// <summary>
|
||
/// Create a deserialize chain with options.
|
||
/// </summary>
|
||
public static IDeserializeChain<T> CreateDeserializeChain<T>(ReadOnlySpan<byte> data, AcBinarySerializerOptions options)
|
||
{
|
||
if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null))
|
||
return EmptyDeserializeChain<T>.Instance;
|
||
|
||
var targetType = typeof(T);
|
||
|
||
// Copy data to array for chain storage
|
||
var dataArray = data.ToArray();
|
||
var chainTracker = new AcSerializerCommon.ChainReferenceTracker();
|
||
var context = DeserializationContextPool.Get(options);
|
||
context.InitBuffer(dataArray, dataArray.Length);
|
||
context.ChainTracker = chainTracker;
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var result = ReadValue(context, targetType, 0);
|
||
// Position-based string interning - no validation needed
|
||
return new BinaryDeserializeChain<T>(dataArray, options, chainTracker, (T?)result);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Chain Implementations
|
||
|
||
/// <summary>
|
||
/// Binary implementation of deserialize chain.
|
||
/// Maintains reference identity for IId objects across chain operations.
|
||
/// </summary>
|
||
private sealed class BinaryDeserializeChain<T> : IDeserializeChain<T>
|
||
{
|
||
private readonly byte[] _data;
|
||
private readonly AcBinarySerializerOptions _options;
|
||
private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker;
|
||
private bool _isDisposed;
|
||
|
||
public T? Value { get; }
|
||
|
||
public BinaryDeserializeChain(byte[] data, AcBinarySerializerOptions options, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value)
|
||
{
|
||
_data = data;
|
||
_options = options;
|
||
_chainTracker = chainTracker;
|
||
Value = value;
|
||
}
|
||
|
||
public TResult? ThenDeserialize<TResult>()
|
||
{
|
||
ThrowIfDisposed();
|
||
|
||
var targetType = typeof(TResult);
|
||
var context = DeserializationContextPool.Get(_options);
|
||
context.InitBuffer(_data, _data.Length);
|
||
context.ChainTracker = _chainTracker;
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var result = ReadValue(context, targetType, 0);
|
||
// Position-based string interning - no validation needed
|
||
return (TResult?)result;
|
||
}
|
||
catch (AcBinaryDeserializationException) { throw; }
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}",
|
||
0, targetType, ex);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
public IDeserializeChain<T> ThenPopulate(object target)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(target);
|
||
ThrowIfDisposed();
|
||
|
||
var targetType = target.GetType();
|
||
var context = DeserializationContextPool.Get(_options);
|
||
context.InitBuffer(_data, _data.Length);
|
||
context.ChainTracker = _chainTracker;
|
||
|
||
try
|
||
{
|
||
context.ReadHeader();
|
||
var typeCode = context.PeekByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Object)
|
||
{
|
||
context.ReadByte();
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
|
||
{
|
||
context.ReadByte();
|
||
ReadInlineMetadataForPopulate(context, targetType);
|
||
PopulateObject(context, target, targetType, 0);
|
||
}
|
||
else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
|
||
{
|
||
context.ReadByte();
|
||
PopulateList(context, targetList, targetType, 0);
|
||
}
|
||
else
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
|
||
context.Position, targetType);
|
||
}
|
||
|
||
// Position-based string interning - no validation needed
|
||
return this;
|
||
}
|
||
catch (AcBinaryDeserializationException) { throw; }
|
||
catch (Exception ex)
|
||
{
|
||
throw new AcBinaryDeserializationException(
|
||
$"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}",
|
||
0, targetType, ex);
|
||
}
|
||
finally
|
||
{
|
||
DeserializationContextPool.Return(context);
|
||
}
|
||
}
|
||
|
||
private void ThrowIfDisposed()
|
||
{
|
||
ObjectDisposedException.ThrowIf(_isDisposed, this);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_isDisposed) return;
|
||
_isDisposed = true;
|
||
_chainTracker.Clear();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Value Reading
|
||
|
||
/// <summary>
|
||
/// Tries to read and set a primitive value directly using typed setters to avoid boxing.
|
||
/// Returns true if handled, false if should fall back to generic path.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte peekCode)
|
||
{
|
||
// Only handle if we have a typed setter
|
||
if (propInfo.AccessorType == PropertyAccessorType.Object)
|
||
return false;
|
||
|
||
// Handle based on property setter type and incoming data type
|
||
switch (propInfo.AccessorType)
|
||
{
|
||
case PropertyAccessorType.Int32:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.Int32)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt32(target, context.ReadVarInt());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Int64:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.Int32)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt64(target, context.ReadVarInt());
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.Int64)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt64(target, context.ReadVarLong());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Boolean:
|
||
if (peekCode == BinaryTypeCode.True)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetBoolean(target, true);
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.False)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetBoolean(target, false);
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Double:
|
||
if (peekCode == BinaryTypeCode.Float64)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetDouble(target, context.ReadDoubleUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Single:
|
||
if (peekCode == BinaryTypeCode.Float32)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetSingle(target, context.ReadSingleUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Decimal:
|
||
if (peekCode == BinaryTypeCode.Decimal)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.DateTime:
|
||
if (peekCode == BinaryTypeCode.DateTime)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Guid:
|
||
if (peekCode == BinaryTypeCode.Guid)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetGuid(target, context.ReadGuidUnsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Byte:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.UInt8)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetByte(target, context.ReadByte());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Int16:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.Int16)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetInt16(target, context.ReadInt16Unsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt16:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.UInt16)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt32:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.UInt32)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt32(target, context.ReadVarUInt());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.UInt64:
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.UInt64)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetUInt64(target, context.ReadVarULong());
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.Enum:
|
||
if (peekCode == BinaryTypeCode.Enum)
|
||
{
|
||
context.ReadByte();
|
||
var enumByte = context.ReadByte();
|
||
int enumValue;
|
||
if (BinaryTypeCode.IsTinyInt(enumByte))
|
||
enumValue = BinaryTypeCode.DecodeTinyInt(enumByte);
|
||
else if (enumByte == BinaryTypeCode.Int32)
|
||
enumValue = context.ReadVarInt();
|
||
else
|
||
return false;
|
||
propInfo.SetEnumAsInt32(target, enumValue);
|
||
return true;
|
||
}
|
||
// Enum can also be encoded as TinyInt directly
|
||
if (BinaryTypeCode.IsTinyInt(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode));
|
||
return true;
|
||
}
|
||
break;
|
||
|
||
case PropertyAccessorType.String:
|
||
if (BinaryTypeCode.IsFixStr(peekCode))
|
||
{
|
||
context.ReadByte();
|
||
var length = BinaryTypeCode.DecodeFixStrLength(peekCode);
|
||
propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.String)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetValue(target, ReadPlainString(context));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.StringEmpty)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetValue(target, string.Empty);
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.StringInterned)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
|
||
return true;
|
||
}
|
||
if (peekCode == BinaryTypeCode.StringInternFirst)
|
||
{
|
||
context.ReadByte();
|
||
propInfo.SetValue(target, ReadAndRegisterInternedString(context));
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Optimized value reader using FrozenDictionary dispatch table.
|
||
/// </summary>
|
||
private static object? ReadValue(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);
|
||
}
|
||
|
||
// Handle FixStr (short strings with length in type code)
|
||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||
return length == 0 ? string.Empty : context.ReadStringUtf8(length);
|
||
}
|
||
|
||
var reader = TypeReaders[typeCode];
|
||
if (reader != null)
|
||
{
|
||
return reader(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(BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
return context.ReadStringUtf8(length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Read interned string (StringInternFirst marker) and register in cache at specified index.
|
||
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static string ReadAndRegisterInternedString(BinaryDeserializationContext context)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return string.Empty;
|
||
var str = context.ReadStringUtf8(length);
|
||
context.RegisterInternedValueAt(cacheIndex, 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(BinaryDeserializationContext context, int streamPosition)
|
||
//{
|
||
// 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, streamPosition);
|
||
// }
|
||
|
||
// return str;
|
||
//}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadInt32Value(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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets type conversion info with ThreadLocal caching.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static TypeConversionInfo GetConversionInfo(Type targetType)
|
||
{
|
||
// Fast path: check ThreadLocal cache first
|
||
var localCache = t_typeConversionLocalCache;
|
||
if (localCache != null && localCache.TryGetValue(targetType, out var cached))
|
||
{
|
||
return cached;
|
||
}
|
||
|
||
// Slow path
|
||
return GetConversionInfoSlow(targetType);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
|
||
{
|
||
var info = TypeConversionCache.GetOrAdd(targetType, static type =>
|
||
{
|
||
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
||
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
|
||
});
|
||
|
||
// Get or create local cache
|
||
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
||
|
||
// Clear when full - reuses internal array, better than new Dictionary()
|
||
if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize)
|
||
{
|
||
localCache.Clear();
|
||
}
|
||
|
||
localCache[targetType] = info;
|
||
return info;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object ReadEnumValue(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(BinaryDeserializationContext context)
|
||
{
|
||
var length = (int)context.ReadVarUInt();
|
||
if (length == 0) return [];
|
||
return context.ReadBytes(length);
|
||
}
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region Object Reading
|
||
|
||
/// <summary>
|
||
/// Reads an ObjectRef - looks up previously registered object from shared intern cache.
|
||
/// Wire format: [ObjectRef][VarUInt cacheIndex]
|
||
///
|
||
/// IMPORTANT / BUG FIX TODO:
|
||
/// Cross-type deserialization esetén ha egy objektum SkipObject-tel lett átugorva
|
||
/// (mert a target típuson nincs megfelelő property), de később ObjectRef hivatkozik rá,
|
||
/// az intern cache-ben nincs objektum → exception.
|
||
///
|
||
/// Megoldás: SkipObject során a stream pozíciót kell beírni a cache-be (boxolt int).
|
||
/// Itt az "is int" check-kel meg kell különböztetni:
|
||
/// - Ha a cached value valódi objektum → visszaadjuk (jelenlegi működés)
|
||
/// - Ha a cached value boxolt int (stream pozíció) → reposition + ReadObject a target type-ra,
|
||
/// majd az eredményt visszaírjuk a cache-be (hogy a következő ref ne olvasson újra)
|
||
///
|
||
/// Az "is int" check biztonságos, mert az intern cache-be csak string és class instance
|
||
/// kerülhet — egyik sem matchel az "is int"-re. A check az ObjectRef path-ban van,
|
||
/// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? ReadObjectRef(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return context.GetInternedObject(cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
|
||
/// Wire format: [Object][props...]
|
||
/// </summary>
|
||
private static object? ReadObject(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
return ReadObjectCore(context, targetType, depth, cacheIndex: -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása első előforduláskor (ObjectRefFirst marker).
|
||
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
|
||
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
|
||
/// </summary>
|
||
private static object? ReadObjectRefFirst(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasás core implementáció.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static object? ReadObjectCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex)
|
||
{
|
||
// Handle dictionary types
|
||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
var metadata = wrapper.Metadata;
|
||
|
||
var instance = CreateInstance(targetType, metadata);
|
||
if (instance == null) return null;
|
||
|
||
if (cacheIndex >= 0)
|
||
{
|
||
context.RegisterInternedValueAt(cacheIndex, instance);
|
||
}
|
||
|
||
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
|
||
|
||
// ChainMode kezelés
|
||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||
{
|
||
var id = GetIdBoxed(instance, metadata);
|
||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||
{
|
||
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
|
||
{
|
||
CopyProperties(instance, existingObj!, metadata);
|
||
return existingObj;
|
||
}
|
||
context.ChainTracker.TryRegisterIIdObject(instance);
|
||
}
|
||
}
|
||
|
||
return instance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása UseMetadata módban (nem tracked).
|
||
/// Wire format:
|
||
/// Első előfordulás: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
|
||
/// Ismételt: [ObjectWithMetadata][propNameHash (4b)][props...]
|
||
/// </summary>
|
||
private static object? ReadObjectWithMetadata(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: -1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Object olvasása UseMetadata módban, első tracked előfordulás (ObjectWithMetadataRefFirst marker).
|
||
/// Wire format: [ObjectWithMetadataRefFirst][VarUInt cacheIndex][propNameHash (4b)][...][props...]
|
||
/// Az objektumot regisztráljuk a cache-be a megadott index-re.
|
||
/// </summary>
|
||
private static object? ReadObjectWithMetadataRefFirst(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: cacheIndex);
|
||
}
|
||
|
||
/// <summary>
|
||
/// ObjectWithMetadata olvasás core implementáció.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static object? ReadObjectWithMetadataCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex)
|
||
{
|
||
// Inline metadata: propNameHash mindig jön
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
// Source hash-ek keresése — ha nincs, első előfordulás → olvasás a stream-ből
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
// Handle dictionary types
|
||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
var metadata = wrapper.Metadata;
|
||
|
||
var instance = CreateInstance(targetType, metadata);
|
||
if (instance == null) return null;
|
||
|
||
if (cacheIndex >= 0)
|
||
{
|
||
context.RegisterInternedValueAt(cacheIndex, instance);
|
||
}
|
||
|
||
// CacheMap felépítése ha még nincs
|
||
if (wrapper.CacheMap == null)
|
||
BuildCacheMap(wrapper, sourceHashes);
|
||
|
||
PopulateObject(context, instance, wrapper, depth, skipDefaultWrite: true);
|
||
|
||
// ChainMode kezelés
|
||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||
{
|
||
var id = GetIdBoxed(instance, metadata);
|
||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||
{
|
||
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
|
||
{
|
||
CopyProperties(instance, existingObj!, metadata);
|
||
return existingObj;
|
||
}
|
||
context.ChainTracker.TryRegisterIIdObject(instance);
|
||
}
|
||
}
|
||
|
||
return instance;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Inline metadata olvasása a Populate path-hoz.
|
||
/// Az ObjectWithMetadata marker már consume-álva van.
|
||
/// Beolvassa a propNameHash-t + hash-eket (első előfordulásnál), és felépíti a cacheMap-et.
|
||
/// </summary>
|
||
private static void ReadInlineMetadataForPopulate(BinaryDeserializationContext context, Type targetType)
|
||
{
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
var wrapper = context.GetWrapper(targetType);
|
||
if (wrapper.CacheMap == null)
|
||
BuildCacheMap(wrapper, sourceHashes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// CacheMap felépítése: source property hash-ek → target PropertySetter mapping.
|
||
/// Fast path: same-type esetén props[index+1] egyezik. Cross-type: keresés index+1-től.
|
||
/// </summary>
|
||
private static void BuildCacheMap(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int[] sourceHashes)
|
||
{
|
||
var properties = wrapper.Metadata.PropertiesArray;
|
||
var cacheMap = new BinaryPropertySetterBase?[sourceHashes.Length];
|
||
var index = -1;
|
||
for (var i = 0; i < sourceHashes.Length; i++)
|
||
{
|
||
var hash = sourceHashes[i];
|
||
var nextIdx = index + 1;
|
||
if (nextIdx < properties.Length && properties[nextIdx].PropertyNameHash == hash)
|
||
{
|
||
cacheMap[i] = properties[nextIdx];
|
||
index = nextIdx;
|
||
}
|
||
else
|
||
{
|
||
for (var j = nextIdx; j < properties.Length; j++)
|
||
{
|
||
if (properties[j].PropertyNameHash == hash)
|
||
{
|
||
cacheMap[i] = properties[j];
|
||
index = j;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
wrapper.CacheMap = cacheMap;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Array Reading
|
||
|
||
private static object? ReadArray(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(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(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)(count);
|
||
|
||
var acObservable = list as IAcObservableCollection;
|
||
acObservable?.BeginUpdate();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var value = ReadValue(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(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;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Dictionary Reading
|
||
|
||
private static object? ReadDictionary(BinaryDeserializationContext context, Type targetType, int depth)
|
||
{
|
||
if (!IsDictionaryType(targetType, out var keyType, out var valueType))
|
||
{
|
||
keyType = typeof(string);
|
||
valueType = typeof(object);
|
||
}
|
||
|
||
return ReadDictionaryAsObject(context, keyType!, valueType!, depth);
|
||
}
|
||
|
||
private static object ReadDictionaryAsObject(BinaryDeserializationContext context, Type keyType, Type valueType, int depth)
|
||
{
|
||
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
|
||
var count = (int)context.ReadVarUInt();
|
||
var dict = (IDictionary)Activator.CreateInstance(dictType, count)!;
|
||
var nextDepth = depth + 1;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var key = ReadValue(context, keyType, nextDepth);
|
||
var value = ReadValue(context, valueType, nextDepth);
|
||
if (key != null)
|
||
dict.Add(key, value);
|
||
}
|
||
|
||
return dict;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Skip Value
|
||
|
||
private static void SkipValue(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||
{
|
||
var typeCode = context.ReadByte();
|
||
|
||
if (typeCode == BinaryTypeCode.Null) return;
|
||
|
||
if (BinaryTypeCode.IsTinyInt(typeCode)) return;
|
||
|
||
// Handle FixStr (short strings)
|
||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||
{
|
||
var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
|
||
if (length > 0)
|
||
context.Skip(length);
|
||
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();
|
||
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:
|
||
SkipPlainString(context);
|
||
return;
|
||
case BinaryTypeCode.StringInterned:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.StringInternFirst:
|
||
// First occurrence - must register even when skipping
|
||
SkipAndRegisterInternedString(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(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.ObjectRefFirst:
|
||
SkipObjectRefFirst(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.ObjectWithMetadata:
|
||
SkipObjectWithMetadata(context, metaData, cacheIndex: -1);
|
||
return;
|
||
case BinaryTypeCode.ObjectWithMetadataRefFirst:
|
||
{
|
||
var cacheIdx = (int)context.ReadVarUInt();
|
||
SkipObjectWithMetadata(context, metaData, cacheIndex: cacheIdx);
|
||
return;
|
||
}
|
||
case BinaryTypeCode.ObjectRef:
|
||
context.ReadVarUInt();
|
||
return;
|
||
case BinaryTypeCode.Array:
|
||
SkipArray(context, metaData);
|
||
return;
|
||
case BinaryTypeCode.Dictionary:
|
||
SkipDictionary(context, metaData);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sima string kihagy<67>sa - NEM regisztr<74>l.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipPlainString(BinaryDeserializationContext context)
|
||
{
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen > 0)
|
||
{
|
||
context.Skip(byteLen);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip an interned string (StringInternFirst) - must still read cacheIndex and register in cache.
|
||
/// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes]
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void SkipAndRegisterInternedString(BinaryDeserializationContext context)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
var byteLen = (int)context.ReadVarUInt();
|
||
if (byteLen == 0) return;
|
||
var str = context.ReadStringUtf8(byteLen);
|
||
context.RegisterInternedValueAt(cacheIndex, str);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache.
|
||
/// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
|
||
/// </summary>
|
||
private static void SkipObjectRefFirst(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||
{
|
||
var cacheIndex = (int)context.ReadVarUInt();
|
||
// Register placeholder (stream position as boxed int for potential lazy load)
|
||
context.RegisterInternedValueAt(cacheIndex, context.Position);
|
||
SkipObject(context, metaData);
|
||
}
|
||
|
||
///// <summary>
|
||
///// Skip a string but still register it in the intern table if it meets the length threshold.
|
||
///// </summary>
|
||
///// <param name="context">Deserialization context</param>
|
||
///// <param name="streamPosition">Position before the type code was read</param>
|
||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
//private static void SkipAndInternString(BinaryDeserializationContext context, int streamPosition)
|
||
//{
|
||
// var byteLen = (int)context.ReadVarUInt();
|
||
// if (byteLen == 0) return;
|
||
|
||
// var str = context.ReadStringUtf8(byteLen);
|
||
// if (str.Length >= context.MinStringInternLength)
|
||
// {
|
||
// context.RegisterInternedString(str, streamPosition);
|
||
// }
|
||
//}
|
||
|
||
/// <summary>
|
||
/// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot.
|
||
/// </summary>
|
||
private static void SkipObject(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||
{
|
||
throw new NotSupportedException(
|
||
"SkipObject nem támogatott metadata nélkül. " +
|
||
"A property szám nem határozható meg típus metadata nélkül.");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst.
|
||
/// </summary>
|
||
/// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
|
||
private static void SkipObjectWithMetadata(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData, int cacheIndex)
|
||
{
|
||
if (cacheIndex >= 0)
|
||
{
|
||
// Register placeholder for potential lazy load
|
||
context.RegisterInternedValueAt(cacheIndex, context.Position);
|
||
}
|
||
|
||
var propNameHash = context.ReadInt32Raw();
|
||
|
||
var sourceHashes = context.FindSourceHashes(propNameHash);
|
||
if (sourceHashes == null)
|
||
{
|
||
var propCount = (int)context.ReadVarUInt();
|
||
sourceHashes = new int[propCount];
|
||
for (var i = 0; i < propCount; i++)
|
||
{
|
||
sourceHashes[i] = context.ReadInt32Raw();
|
||
}
|
||
context.RegisterInlineMetadata(propNameHash, sourceHashes);
|
||
}
|
||
|
||
// Skip all properties
|
||
for (var i = 0; i < sourceHashes.Length; i++)
|
||
{
|
||
SkipValue(context, metaData);
|
||
}
|
||
}
|
||
|
||
private static void SkipArray(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(context, metaData);
|
||
}
|
||
}
|
||
|
||
private static void SkipDictionary(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
|
||
{
|
||
var count = (int)context.ReadVarUInt();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
SkipValue(context, metaData); // key
|
||
SkipValue(context, metaData); // value
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region IId Registration Helpers
|
||
|
||
/// <summary>
|
||
/// Gets Id as boxed object - only used for ChainMode (rare path).
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
|
||
{
|
||
return metadata.IdAccessorType switch
|
||
{
|
||
IdAccessorType.Int32 => metadata.GetIdInt32(instance),
|
||
IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
||
IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
||
_ => null
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Type Metadata
|
||
|
||
[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.TypeConversionInfo.cs
|
||
|