1584 lines
57 KiB
C#
1584 lines
57 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();
|
|
|
|
// Type dispatch table for fast ReadValue
|
|
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
|
|
private static readonly FrozenDictionary<byte, TypeReader> TypeReaders;
|
|
|
|
static AcBinaryDeserializer()
|
|
{
|
|
// Initialize type reader dispatch table
|
|
var readers = new Dictionary<byte, TypeReader>
|
|
{
|
|
[BinaryTypeCode.Null] = static (ref BinaryDeserializationContext _, Type _, int _) => null,
|
|
[BinaryTypeCode.True] = static (ref BinaryDeserializationContext _, Type _, int _) => true,
|
|
[BinaryTypeCode.False] = static (ref BinaryDeserializationContext _, Type _, int _) => false,
|
|
[BinaryTypeCode.Int8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => (sbyte)ctx.ReadByte(),
|
|
[BinaryTypeCode.UInt8] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadByte(),
|
|
[BinaryTypeCode.Int16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadInt16Unsafe(),
|
|
[BinaryTypeCode.UInt16] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadUInt16Unsafe(),
|
|
[BinaryTypeCode.Int32] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadInt32Value(ref ctx, type),
|
|
[BinaryTypeCode.UInt32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarUInt(),
|
|
[BinaryTypeCode.Int64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarLong(),
|
|
[BinaryTypeCode.UInt64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadVarULong(),
|
|
[BinaryTypeCode.Float32] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadSingleUnsafe(),
|
|
[BinaryTypeCode.Float64] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe(),
|
|
[BinaryTypeCode.Decimal] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe(),
|
|
[BinaryTypeCode.Char] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe(),
|
|
[BinaryTypeCode.String] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx),
|
|
[BinaryTypeCode.StringInterned] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()),
|
|
[BinaryTypeCode.StringEmpty] = static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty,
|
|
[BinaryTypeCode.DateTime] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe(),
|
|
[BinaryTypeCode.DateTimeOffset] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe(),
|
|
[BinaryTypeCode.TimeSpan] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe(),
|
|
[BinaryTypeCode.Guid] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadGuidUnsafe(),
|
|
[BinaryTypeCode.Enum] = static (ref BinaryDeserializationContext ctx, Type type, int _) => ReadEnumValue(ref ctx, type),
|
|
[BinaryTypeCode.Object] = ReadObject,
|
|
[BinaryTypeCode.ObjectRef] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetReferencedObject(ctx.ReadVarInt()),
|
|
[BinaryTypeCode.Array] = ReadArray,
|
|
[BinaryTypeCode.Dictionary] = ReadDictionary,
|
|
[BinaryTypeCode.ByteArray] = static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadByteArray(ref ctx),
|
|
};
|
|
TypeReaders = readers.ToFrozenDictionary();
|
|
}
|
|
|
|
#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);
|
|
}
|
|
|
|
// Use dispatch table for type-specific reading
|
|
if (TypeReaders.TryGetValue(typeCode, out var reader))
|
|
{
|
|
return reader(ref context, targetType, depth);
|
|
}
|
|
|
|
throw new AcBinaryDeserializationException(
|
|
$"Unknown type code: {typeCode}",
|
|
context.Position, targetType);
|
|
}
|
|
|
|
/// <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 underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
|
if (underlying.IsEnum)
|
|
return Enum.ToObject(underlying, value);
|
|
|
|
var typeCode = Type.GetTypeCode(underlying);
|
|
return 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 object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
|
|
{
|
|
var underlying = Nullable.GetUnderlyingType(targetType) ?? 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);
|
|
}
|
|
|
|
if (underlying.IsEnum)
|
|
return Enum.ToObject(underlying, intValue);
|
|
return 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;
|
|
}
|
|
|
|
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
|
propInfo.SetValue(target, value);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
#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:
|
|
// CRITICAL FIX: Must register string in intern table even when skipping!
|
|
SkipAndInternString(ref context);
|
|
return;
|
|
case BinaryTypeCode.StringInterned:
|
|
context.ReadVarUInt();
|
|
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>
|
|
/// 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 Dictionary<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);
|
|
}
|
|
|
|
_propertiesDict = new Dictionary<string, BinaryPropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
|
|
PropertiesArray = new BinaryPropertySetterInfo[propsList.Count];
|
|
|
|
for (int i = 0; i < propsList.Count; i++)
|
|
{
|
|
var prop = propsList[i];
|
|
var propInfo = new BinaryPropertySetterInfo(prop, type);
|
|
_propertiesDict[prop.Name] = propInfo;
|
|
PropertiesArray[i] = propInfo;
|
|
}
|
|
}
|
|
|
|
[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 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);
|
|
var isCollection = ElementType != null && ElementType != typeof(object) &&
|
|
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
|
|
!ReferenceEquals(PropertyType, StringType);
|
|
|
|
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;
|
|
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.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public string ReadStringUtf8(int byteCount)
|
|
{
|
|
if (_position + byteCount > _data.Length)
|
|
throw new AcBinaryDeserializationException("Unexpected end of data", _position);
|
|
var result = Encoding.UTF8.GetString(_data.Slice(_position, byteCount));
|
|
_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
|
|
}
|