using System; using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; using System.Diagnostics; 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; /// /// 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 /// public static partial class AcBinaryDeserializer { /// /// Diagnostic logger for deserializer-level debugging (DEBUG builds only). /// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results. /// public static Action? DiagnosticLogger { get; set; } private static readonly ConcurrentDictionary TypeConversionCache = new(); /// /// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path. /// internal static class GeneratedReaderRegistry { private static readonly ConcurrentDictionary Readers = new(); internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static IGeneratedBinaryReader? TryGet(Type type) => Readers.TryGetValue(type, out var reader) ? reader : null; } /// /// Registers a source-generated binary reader for the specified type. /// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop /// and calls the generated reader directly. /// internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader) { ArgumentNullException.ThrowIfNull(type); ArgumentNullException.ThrowIfNull(reader); GeneratedReaderRegistry.Register(type, reader); } /// /// ThreadLocal cache for type conversion info. /// [ThreadStatic] private static Dictionary? t_typeConversionLocalCache; // Type dispatch table for fast ReadValue — generic per TInput, JIT specializes each private delegate object? TypeReader(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase; private static class TypeReaderTable where TInput : struct, IBinaryInputBase { public static readonly TypeReader?[] Readers = InitReaders(); private static TypeReader?[] InitReaders() { var readers = new TypeReader?[byte.MaxValue + 1]; readers[BinaryTypeCode.Null] = static (_, _, _) => null; readers[BinaryTypeCode.True] = static (_, _, _) => true; readers[BinaryTypeCode.False] = static (_, _, _) => false; readers[BinaryTypeCode.Int8] = static (ctx, _, _) => (sbyte)ctx.ReadByte(); readers[BinaryTypeCode.UInt8] = static (ctx, _, _) => ctx.ReadByte(); readers[BinaryTypeCode.Int16] = static (ctx, _, _) => ctx.ReadInt16Unsafe(); readers[BinaryTypeCode.UInt16] = static (ctx, _, _) => ctx.ReadUInt16Unsafe(); readers[BinaryTypeCode.Int32] = static (ctx, type, _) => ReadInt32Value(ctx, type); readers[BinaryTypeCode.UInt32] = static (ctx, _, _) => ctx.ReadVarUInt(); readers[BinaryTypeCode.Int64] = static (ctx, _, _) => ctx.ReadVarLong(); readers[BinaryTypeCode.UInt64] = static (ctx, _, _) => ctx.ReadVarULong(); readers[BinaryTypeCode.Float32] = static (ctx, _, _) => ctx.ReadSingleUnsafe(); readers[BinaryTypeCode.Float64] = static (ctx, _, _) => ctx.ReadDoubleUnsafe(); readers[BinaryTypeCode.Decimal] = static (ctx, _, _) => ctx.ReadDecimalUnsafe(); readers[BinaryTypeCode.Char] = static (ctx, _, _) => ctx.ReadCharUnsafe(); readers[BinaryTypeCode.String] = static (ctx, _, _) => ReadPlainString(ctx); readers[BinaryTypeCode.StringInterned] = static (ctx, _, _) => ctx.GetInternedString((int)ctx.ReadVarUInt()); readers[BinaryTypeCode.StringEmpty] = static (_, _, _) => string.Empty; readers[BinaryTypeCode.StringInternFirst] = static (ctx, _, _) => ReadAndRegisterInternedString(ctx); readers[BinaryTypeCode.DateTime] = static (ctx, _, _) => ctx.ReadDateTimeUnsafe(); readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _, _) => ctx.ReadDateTimeOffsetUnsafe(); readers[BinaryTypeCode.TimeSpan] = static (ctx, _, _) => ctx.ReadTimeSpanUnsafe(); readers[BinaryTypeCode.Guid] = static (ctx, _, _) => ctx.ReadGuidUnsafe(); readers[BinaryTypeCode.Enum] = static (ctx, type, _) => ReadEnumValue(ctx, type); readers[BinaryTypeCode.Object] = ReadObject; readers[BinaryTypeCode.ObjectRefFirst] = ReadObjectRefFirst; readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata; readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst; readers[BinaryTypeCode.ObjectRef] = ReadObjectRef; readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName; readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst; readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex; readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst; readers[BinaryTypeCode.Array] = ReadArray; readers[BinaryTypeCode.Dictionary] = ReadDictionary; readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx); // Register FixStr readers for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++) { var length = BinaryTypeCode.DecodeFixStrLength(code); readers[code] = CreateFixStrReader(length); } // Register FixObj slot readers (0..SlotCount-1) for (int slot = 0; slot < BinaryTypeCode.SlotCount; slot++) { readers[slot] = CreateFixObjReader(slot); } return readers; } } /// /// Creates a reader for FixStr with the given length. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TypeReader CreateFixStrReader(int length) where TInput : struct, IBinaryInputBase { if (length == 0) return static (_, _, _) => string.Empty; return (ctx, _, _) => ctx.ReadStringUtf8(length); } /// /// Creates a reader for FixObj slot (0..SlotCount-1). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TypeReader CreateFixObjReader(int slot) where TInput : struct, IBinaryInputBase { return (ctx, targetType, depth) => ReadObjectFromSlot(ctx, slot, targetType, depth); } private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); #region Public API /// /// Deserialize binary data to object of type T. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? Deserialize(byte[] data) => Deserialize(data, AcBinarySerializerOptions.Default); /// /// Deserialize binary data to object of type T with options. /// Zero-copy: ArrayBinaryInput references the byte[] directly. /// public static T? Deserialize(byte[] data, AcBinarySerializerOptions options) { if (data.Length == 0) return default; if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default; var targetType = typeof(T); if (AcSerializerCommon.IsExpressionType(targetType)) return (T?)(object?)DeserializeExpression(data, targetType, options); var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data)); try { return (T?)DeserializeCore(context, targetType); } finally { DeserializationContextPool.Return(context); } } /// /// Deserialize binary data to object of type T from a sub-range of a byte[]. /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? Deserialize(byte[] data, int offset, int length) => Deserialize(data, offset, length, AcBinarySerializerOptions.Default); /// /// Deserialize binary data to object of type T from a sub-range with options. /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset. /// public static T? Deserialize(byte[] data, int offset, int length, AcBinarySerializerOptions options) { if (length == 0) return default; if (length == 1 && data[offset] == BinaryTypeCode.Null) return default; var targetType = typeof(T); if (AcSerializerCommon.IsExpressionType(targetType)) return (T?)(object?)DeserializeExpression(data, offset, length, targetType, options); var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data, offset, length)); try { return (T?)DeserializeCore(context, targetType); } finally { DeserializationContextPool.Return(context); } } /// /// Deserialize binary data to specified type. /// public static object? Deserialize(byte[] data, Type targetType) => Deserialize(data, 0, data.Length, targetType, AcBinarySerializerOptions.Default); /// /// Deserialize binary data to specified type with options. /// public static object? Deserialize(byte[] data, Type targetType, AcBinarySerializerOptions options) => Deserialize(data, 0, data.Length, targetType, options); /// /// Deserialize binary data to specified type from a sub-range. /// public static object? Deserialize(byte[] data, int offset, int length, Type targetType) => Deserialize(data, offset, length, targetType, AcBinarySerializerOptions.Default); /// /// Deserialize binary data to specified type from a sub-range with options. /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset. /// public static object? Deserialize(byte[] data, int offset, int length, Type targetType, AcBinarySerializerOptions options) { if (length == 0) return null; if (length == 1 && data[offset] == BinaryTypeCode.Null) return null; if (AcSerializerCommon.IsExpressionType(targetType)) return DeserializeExpression(data, offset, length, targetType, options); var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data, offset, length)); try { return DeserializeCore(context, targetType); } finally { DeserializationContextPool.Return(context); } } /// /// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes). /// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer. /// public static T? Deserialize(ReadOnlySequence data) => Deserialize(data, AcBinarySerializerOptions.Default); /// /// Deserialize binary data from a ReadOnlySequence with options. /// Single-segment: zero-copy via FirstSpan. Multi-segment: uses SequenceBinaryInput for true streaming. /// public static T? Deserialize(ReadOnlySequence data, AcBinarySerializerOptions options) { if (data.Length == 0) return default; if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg)) return Deserialize(seg.Array!, seg.Offset, seg.Count, options); VerifyAgainstLinearized(data, typeof(T), options); return DeserializeSequence(new SequenceBinaryInput(data), typeof(T), options); } /// /// Deserialize binary data from a ReadOnlySequence to specified type. /// public static object? Deserialize(ReadOnlySequence data, Type targetType) => Deserialize(data, targetType, AcBinarySerializerOptions.Default); /// /// Deserialize binary data from a ReadOnlySequence to specified type with options. /// public static object? Deserialize(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options) { if (data.Length == 0) return null; if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg2)) return Deserialize(seg2.Array!, seg2.Offset, seg2.Count, targetType, options); VerifyAgainstLinearized(data, targetType, options); return DeserializeSequence(new SequenceBinaryInput(data), targetType, options); } /// /// Deserialize from PipeReader with segment streaming (read per chunk via PipeReaderBinaryInput). /// Data is consumed as it arrives from the network, enabling pipeline parallelism. /// public static T? Deserialize(System.IO.Pipelines.PipeReader pipeReader, AcBinarySerializerOptions options) => DeserializeSequence(new PipeReaderBinaryInput(pipeReader), typeof(T), options); /// /// Deserialize from PipeReader to specified type with segment streaming. /// public static object? Deserialize(System.IO.Pipelines.PipeReader pipeReader, Type targetType, AcBinarySerializerOptions options) => DeserializeSequence(new PipeReaderBinaryInput(pipeReader), targetType, options); /// /// Internal: Deserialize with any TInput (multi-segment or other future input types). /// private static T? DeserializeSequence(TInput input, Type targetType, AcBinarySerializerOptions options) where TInput : struct, IBinaryInputBase { if (AcSerializerCommon.IsExpressionType(targetType)) { var (buf, off, len) = LinearizeSequence(input); return Deserialize(buf, off, len, options); } var context = DeserializationContextPool.Get(options); context.InitInput(input); try { return (T?)DeserializeCore(context, targetType); } finally { context.Input.Release(); DeserializationContextPool.Return(context); } } /// /// Internal: Deserialize with any TInput (multi-segment, non-generic result). /// private static object? DeserializeSequence(TInput input, Type targetType, AcBinarySerializerOptions options) where TInput : struct, IBinaryInputBase { if (AcSerializerCommon.IsExpressionType(targetType)) { var (buf, off, len) = LinearizeSequence(input); return Deserialize(buf, off, len, targetType, options); } var context = DeserializationContextPool.Get(options); context.InitInput(input); try { return DeserializeCore(context, targetType); } finally { context.Input.Release(); DeserializationContextPool.Return(context); } } /// /// Fallback: linearize a TInput into a contiguous byte[] range (for Expression deserialization). /// private static (byte[] buffer, int offset, int length) LinearizeSequence(TInput input) where TInput : struct, IBinaryInputBase { input.Initialize(out var buffer, out var position, out var bufferLength); return (buffer, position, bufferLength - position); } /// /// Core deserialization: ReadHeader + ReadValue with unified error handling. /// All public Deserialize overloads delegate here after pool/init setup. /// private static object? DeserializeCore(BinaryDeserializationContext context, Type targetType) where TInput : struct, IBinaryInputBase { 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); } } /// /// DEBUG-only verification: linearizes multi-segment data to byte[] and deserializes /// with ArrayBinaryInput to determine if drift is caused by SequenceBinaryInput or by /// the serialized data itself (generated reader / serializer bug). /// /// Result: VERIFY_OK → SequenceBinaryInput is the culprit. /// VERIFY_FAIL → bug is in serializer or generated reader (data itself is bad). /// [Conditional("DEBUG")] private static void VerifyAgainstLinearized(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options) { if (DiagnosticLogger == null) return; var bytes = data.ToArray(); var segmentCount = 0; foreach (var _ in data) segmentCount++; try { var result = Deserialize(bytes, targetType, options); DiagnosticLogger($"[VERIFY_OK] ArrayBinaryInput succeeded for {targetType.Name}, " + $"{bytes.Length} bytes, {segmentCount} segments → SequenceBinaryInput is suspect"); } catch (Exception ex) { DiagnosticLogger($"[VERIFY_FAIL] ArrayBinaryInput ALSO FAILED for {targetType.Name}, " + $"{bytes.Length} bytes, {segmentCount} segments: {ex.Message}"); } } /// /// Deserialize Expression from binary data. /// private static Expression? DeserializeExpression(byte[] data, Type targetExpressionType, AcBinarySerializerOptions options) => DeserializeExpression(data, 0, data.Length, targetExpressionType, options); /// /// Deserialize Expression from binary data sub-range. /// private static Expression? DeserializeExpression(byte[] data, int offset, int length, Type targetExpressionType, AcBinarySerializerOptions options) { var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data, offset, length)); try { var node = (AcExpressionNode?)DeserializeCore(context, typeof(AcExpressionNode)); if (node == null) return null; var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType); return AcExpressionRebuilder.FromNode(node, entityType); } finally { DeserializationContextPool.Return(context); } } /// /// Core populate: ReadHeader + dispatch by typeCode. Used by Populate, PopulateMerge, ThenPopulate. /// private static void PopulateCore(BinaryDeserializationContext context, object target) where TInput : struct, IBinaryInputBase { var targetType = target.GetType(); try { context.ReadHeader(); var typeCode = context.PeekByte(); if (typeCode < BinaryTypeCode.SlotCount) { // FixObj slot: marker byte is the slot index context.ReadByte(); context.GetWrapper(targetType, typeCode); if (typeCode >= context._nextRuntimeSlot) context._nextRuntimeSlot = typeCode + 1; PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.Object) { context.ReadByte(); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectRefFirst) { // ObjectRefFirst: [marker][VarUInt cacheIndex][props...] // Read marker + cacheIndex, register target in intern cache, then populate context.ReadByte(); var cacheIndex = (int)context.ReadVarUInt(); context.RegisterInternedValueAt(cacheIndex, target); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectWithMetadata) { context.ReadByte(); ReadInlineMetadataForPopulate(context, targetType); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst) { // ObjectWithMetadataRefFirst: [marker][VarUInt cacheIndex][metadata...][props...] context.ReadByte(); var cacheIndex = (int)context.ReadVarUInt(); context.RegisterInternedValueAt(cacheIndex, target); 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); } } catch (AcBinaryDeserializationException) { throw; } catch (Exception ex) { throw new AcBinaryDeserializationException( $"Failed to populate object of type '{targetType.Name}': {ex.Message}", context.Position, targetType, ex); } } /// /// Populate existing object from binary data. /// public static void Populate(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 context = DeserializationContextPool.Get(AcBinarySerializerOptions.Default); context.InitInput(new ArrayBinaryInput(data)); try { PopulateCore(context, target); } finally { DeserializationContextPool.Return(context); } } /// /// Populate existing object from binary data with options. /// public static void Populate(byte[] data, T target, AcBinarySerializerOptions options) where T : class => Populate(data, 0, data.Length, target, options); /// /// Populate existing object from binary data sub-range. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Populate(byte[] data, int offset, int length, T target) where T : class => Populate(data, offset, length, target, AcBinarySerializerOptions.Default); /// /// Populate existing object from binary data sub-range with options. /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset. /// public static void Populate(byte[] data, int offset, int length, T target, AcBinarySerializerOptions options) where T : class { ArgumentNullException.ThrowIfNull(target); if (length == 0) return; if (length == 1 && data[offset] == BinaryTypeCode.Null) return; var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data, offset, length)); try { PopulateCore(context, target); } finally { DeserializationContextPool.Return(context); } } /// /// Populate with merge semantics for IId collections from byte[] (zero-copy). /// public static void PopulateMerge(byte[] data, T target) where T : class => PopulateMerge(data, 0, data.Length, target, AcBinarySerializerOptions.Default); /// /// Populate with merge semantics for IId collections from byte[] with options (zero-copy). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void PopulateMerge(byte[] data, T target, AcBinarySerializerOptions options) where T : class => PopulateMerge(data, 0, data.Length, target, options); /// /// Populate with merge semantics for IId collections from a sub-range of byte[]. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void PopulateMerge(byte[] data, int offset, int length, T target) where T : class => PopulateMerge(data, offset, length, target, AcBinarySerializerOptions.Default); /// /// Populate with merge semantics for IId collections from a sub-range with options. /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset. /// public static void PopulateMerge(byte[] data, int offset, int length, T target, AcBinarySerializerOptions? options) where T : class { ArgumentNullException.ThrowIfNull(target); if (length == 0) return; if (length == 1 && data[offset] == BinaryTypeCode.Null) return; var opts = options ?? AcBinarySerializerOptions.Default; var context = DeserializationContextPool.Get(opts); context.InitInput(new ArrayBinaryInput(data, offset, length)); context.IsMergeMode = true; context.RemoveOrphanedItems = opts.RemoveOrphanedItems; try { PopulateMergeCore(context, target); } finally { DeserializationContextPool.Return(context); } } /// /// Core merge populate: handles IId collection merge for top-level arrays. /// private static void PopulateMergeCore(BinaryDeserializationContext context, object target) where TInput : struct, IBinaryInputBase { var targetType = target.GetType(); try { context.ReadHeader(); var typeCode = context.PeekByte(); if (typeCode < BinaryTypeCode.SlotCount) { // FixObj slot: marker byte is the slot index context.ReadByte(); context.GetWrapper(targetType, typeCode); if (typeCode >= context._nextRuntimeSlot) context._nextRuntimeSlot = typeCode + 1; PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.Object) { context.ReadByte(); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectRefFirst) { context.ReadByte(); var cacheIndex = (int)context.ReadVarUInt(); context.RegisterInternedValueAt(cacheIndex, target); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectWithMetadata) { context.ReadByte(); ReadInlineMetadataForPopulate(context, targetType); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst) { context.ReadByte(); var cacheIndex = (int)context.ReadVarUInt(); context.RegisterInternedValueAt(cacheIndex, target); ReadInlineMetadataForPopulate(context, targetType); PopulateObject(context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.Array && target is IList targetList) { context.ReadByte(); 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); return; } } PopulateList(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 Chain API /// /// Create a deserialize chain that parses binary data once and allows multiple deserializations. /// Maintains reference identity for IId objects across chain operations. /// public static IDeserializeChain CreateDeserializeChain(byte[] data) => CreateDeserializeChain(data, AcBinarySerializerOptions.Default); /// /// Create a deserialize chain with options. /// public static IDeserializeChain CreateDeserializeChain(byte[] data, AcBinarySerializerOptions options) { if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null)) return EmptyDeserializeChain.Instance; var chainTracker = new AcSerializerCommon.ChainReferenceTracker(); var context = DeserializationContextPool.Get(options); context.InitInput(new ArrayBinaryInput(data)); context.ChainTracker = chainTracker; try { var result = (T?)DeserializeCore(context, typeof(T)); return new BinaryDeserializeChain(data, options, chainTracker, result); } finally { DeserializationContextPool.Return(context); } } #endregion #region Chain Implementations /// /// Binary implementation of deserialize chain. /// Maintains reference identity for IId objects across chain operations. /// private sealed class BinaryDeserializeChain : IDeserializeChain { 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() { ThrowIfDisposed(); var context = DeserializationContextPool.Get(_options); context.InitInput(new ArrayBinaryInput(_data)); context.ChainTracker = _chainTracker; try { return (TResult?)DeserializeCore(context, typeof(TResult)); } finally { DeserializationContextPool.Return(context); } } public IDeserializeChain ThenPopulate(object target) { ArgumentNullException.ThrowIfNull(target); ThrowIfDisposed(); var context = DeserializationContextPool.Get(_options); context.InitInput(new ArrayBinaryInput(_data)); context.ChainTracker = _chainTracker; try { PopulateCore(context, target); return this; } 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 /// /// Tries to read and set a primitive value directly using typed setters to avoid boxing. /// The type code marker byte is already consumed by the caller. /// Returns true if handled, false if should fall back to generic path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte typeCode) where TInput : struct, IBinaryInputBase { // 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. // The marker byte (typeCode) is already consumed — no ReadByte() needed. switch (propInfo.AccessorType) { case PropertyAccessorType.Int32: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.Int32) { propInfo.SetInt32(target, context.ReadVarInt()); return true; } break; case PropertyAccessorType.Int64: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.Int32) { propInfo.SetInt64(target, context.ReadVarInt()); return true; } if (typeCode == BinaryTypeCode.Int64) { propInfo.SetInt64(target, context.ReadVarLong()); return true; } break; case PropertyAccessorType.Boolean: if (typeCode == BinaryTypeCode.True) { propInfo.SetBoolean(target, true); return true; } if (typeCode == BinaryTypeCode.False) { propInfo.SetBoolean(target, false); return true; } break; case PropertyAccessorType.Double: if (typeCode == BinaryTypeCode.Float64) { propInfo.SetDouble(target, context.ReadDoubleUnsafe()); return true; } break; case PropertyAccessorType.Single: if (typeCode == BinaryTypeCode.Float32) { propInfo.SetSingle(target, context.ReadSingleUnsafe()); return true; } break; case PropertyAccessorType.Decimal: if (typeCode == BinaryTypeCode.Decimal) { propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); return true; } break; case PropertyAccessorType.DateTime: if (typeCode == BinaryTypeCode.DateTime) { propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); return true; } break; case PropertyAccessorType.Guid: if (typeCode == BinaryTypeCode.Guid) { propInfo.SetGuid(target, context.ReadGuidUnsafe()); return true; } break; case PropertyAccessorType.Byte: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.UInt8) { propInfo.SetByte(target, context.ReadByte()); return true; } break; case PropertyAccessorType.Int16: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.Int16) { propInfo.SetInt16(target, context.ReadInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt16: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.UInt16) { propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt32: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.UInt32) { propInfo.SetUInt32(target, context.ReadVarUInt()); return true; } break; case PropertyAccessorType.UInt64: if (BinaryTypeCode.IsTinyInt(typeCode)) { propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } if (typeCode == BinaryTypeCode.UInt64) { propInfo.SetUInt64(target, context.ReadVarULong()); return true; } break; case PropertyAccessorType.Enum: if (typeCode == BinaryTypeCode.Enum) { 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(typeCode)) { propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } break; case PropertyAccessorType.String: if (BinaryTypeCode.IsFixStr(typeCode)) { var length = BinaryTypeCode.DecodeFixStrLength(typeCode); propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length)); return true; } if (typeCode == BinaryTypeCode.String) { propInfo.SetValue(target, ReadPlainString(context)); return true; } if (typeCode == BinaryTypeCode.StringEmpty) { propInfo.SetValue(target, string.Empty); return true; } if (typeCode == BinaryTypeCode.StringInterned) { propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt())); return true; } if (typeCode == BinaryTypeCode.StringInternFirst) { propInfo.SetValue(target, ReadAndRegisterInternedString(context)); return true; } break; } return false; } /// /// Bridge for generated readers to call ReadValue for unknown/complex/collection property types. /// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static object? ReadValueGenerated(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { return ReadValue(context, targetType, depth); } /// /// Optimized value reader using FrozenDictionary dispatch table. /// private static object? ReadValue(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { if (context.IsAtEnd) return null; var typeCode = context.ReadByte(); // Handle tiny int first (most common case for small integers, >= 192) if (BinaryTypeCode.IsTinyInt(typeCode)) { var intValue = BinaryTypeCode.DecodeTinyInt(typeCode); return ConvertToTargetType(intValue, targetType); } // Handle null if (typeCode == BinaryTypeCode.Null) return null; // 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 = TypeReaderTable.Readers[typeCode]; if (reader != null) { return reader(context, targetType, depth); } throw new AcBinaryDeserializationException( $"Unknown type code: {typeCode}", context.Position, targetType); } /// /// Sima string olvas�sa - NEM regisztr�l az intern t�bl�ba. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string ReadPlainString(BinaryDeserializationContext context) where TInput : struct, IBinaryInputBase { var length = (int)context.ReadVarUInt(); if (length == 0) return string.Empty; return context.ReadStringUtf8(length); } /// /// Read interned string (StringInternFirst marker) and register in cache at specified index. /// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string ReadAndRegisterInternedString(BinaryDeserializationContext context) where TInput : struct, IBinaryInputBase { // First StringInternFirst marker proves payload uses string interning → // plain String entries appear only once, so _stringCache would never hit context.DisableStringCaching(); 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; } ///// ///// Read a string and register it in the intern table for future references. ///// //[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) where TInput : struct, IBinaryInputBase { 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 }; } /// /// Gets type conversion info with ThreadLocal caching. /// [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(); // 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) where TInput : struct, IBinaryInputBase { 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) where TInput : struct, IBinaryInputBase { var length = (int)context.ReadVarUInt(); if (length == 0) return []; return context.ReadBytes(length); } #endregion #region Object Reading /// /// 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ReadObjectRef(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return context.GetInternedObject(cacheIndex); } /// /// FixObj slot read: marker byte (0..SlotCount-1) is the slot index. /// First occurrence: wrapper is null in slot → resolve from targetType, cache in slot. /// Subsequent: direct array access (~1-2ns). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ReadObjectFromSlot( BinaryDeserializationContext context, int slot, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var wrapper = context.GetWrapper(targetType, slot); // Track highest slot used for Clear() if (slot >= context._nextRuntimeSlot) context._nextRuntimeSlot = slot + 1; // SGen fast path (same as ReadObjectCore) if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode) { var generatedReader = wrapper.GeneratedReader; if (generatedReader != null) return generatedReader.ReadObject(context, depth, cacheIndex: -1); } return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex: -1); } /// /// Object olvasása (nem tracked, vagy UseMetadata nélkül). /// Wire format: [Object][props...] /// private static object? ReadObject(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { return ReadObjectCore(context, targetType, depth, cacheIndex: -1); } /// /// 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. /// private static object? ReadObjectRefFirst(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex); } /// /// Polymorphic PREFIX marker: declared type ≠ runtime type. /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...] /// Reads the runtime type name, resolves it, registers wrapper in poly slot cache, /// then reads the inner marker via ReadValue. /// private static object? ReadObjectWithTypeName(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var typeName = ReadPlainString(context); var resolvedType = AcSerializerCommon.ResolveTypeName(typeName) ?? throw new AcBinaryDeserializationException( $"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.", context.Position, null); var wrapper = context.GetWrapper(resolvedType); context.RegisterPolymorphicWrapper(wrapper); // Next byte is the actual inner marker (Object/Array/Dict/etc.) — read it via ReadValue return ReadValue(context, resolvedType, depth); } /// /// Polymorphic COMBINED marker: first type occurrence + ref tracking first occurrence. /// Wire format: [ObjectWithTypeNameRefFirst (69)] [TypeName string] [VarUInt refCacheIndex] [properties...] /// Object body follows directly — no inner Object/ObjectRefFirst marker. /// private static object? ReadObjectWithTypeNameRefFirst(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var typeName = ReadPlainString(context); var resolvedType = AcSerializerCommon.ResolveTypeName(typeName) ?? throw new AcBinaryDeserializationException( $"Cannot resolve type '{typeName}' for ObjectWithTypeNameRefFirst at position {context.Position}.", context.Position, null); var wrapper = context.GetWrapper(resolvedType); context.RegisterPolymorphicWrapper(wrapper); var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex); } /// /// Polymorphic PREFIX marker with cached type index. /// Wire format: [ObjectWithTypeIndex (70)] [VarUInt typeIndex] [inner marker: Object/Array/Dict/...] [body...] /// Looks up the previously registered wrapper by index (~1-2ns array access), /// then reads the inner marker via ReadValue. /// private static object? ReadObjectWithTypeIndex(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var typeIndex = (int)context.ReadVarUInt(); var wrapper = context.GetPolymorphicWrapper(typeIndex); return ReadValue(context, wrapper.Metadata.SourceType, depth); } /// /// Polymorphic COMBINED marker: cached type index + ref tracking first occurrence. /// Wire format: [ObjectWithTypeIndexRefFirst (71)] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...] /// Object body follows directly — no inner Object/ObjectRefFirst marker. 0 dictionary lookup. /// private static object? ReadObjectWithTypeIndexRefFirst(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var typeIndex = (int)context.ReadVarUInt(); var wrapper = context.GetPolymorphicWrapper(typeIndex); var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex); } /// /// Object olvasás core implementáció. /// /// -1 = not cached, 0+ = register at this cache index private static object? ReadObjectCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) where TInput : struct, IBinaryInputBase { // Handle dictionary types if (IsDictionaryType(targetType, out var keyType, out var valueType)) { return ReadDictionaryAsObject(context, keyType!, valueType!, depth); } var wrapper = context.GetWrapper(targetType); // SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely. // Only when not in UseMetadata mode (cross-type CacheMap not known at compile time), // not in ChainMode (needs post-read identity tracking), // and UseGeneratedCode is enabled (matching serializer-side check). if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode) { var generatedReader = wrapper.GeneratedReader; if (generatedReader != null) return generatedReader.ReadObject(context, depth, cacheIndex); } return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex); } /// /// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup). /// private static object? ReadObjectCoreWithWrapper(BinaryDeserializationContext context, TypeMetadataWrapper wrapper, int depth, int cacheIndex) where TInput : struct, IBinaryInputBase { var metadata = wrapper.Metadata; var targetType = metadata.SourceType; 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; } /// /// 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...] /// private static object? ReadObjectWithMetadata(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: -1); } /// /// 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. /// private static object? ReadObjectWithMetadataRefFirst(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); return ReadObjectWithMetadataCore(context, targetType, depth, cacheIndex: cacheIndex); } /// /// ObjectWithMetadata olvasás core implementáció. /// /// -1 = not cached, 0+ = register at this cache index private static object? ReadObjectWithMetadataCore(BinaryDeserializationContext context, Type targetType, int depth, int cacheIndex) where TInput : struct, IBinaryInputBase { // 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; } /// /// 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. /// private static void ReadInlineMetadataForPopulate(BinaryDeserializationContext context, Type targetType) where TInput : struct, IBinaryInputBase { 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); } /// /// 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. /// private static void BuildCacheMap(TypeMetadataWrapper 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) where TInput : struct, IBinaryInputBase { 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 */ } 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; } /// /// Optimized primitive array reader using bulk operations. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Array? TryReadPrimitiveArray(BinaryDeserializationContext context, Type elementType, int count) where TInput : struct, IBinaryInputBase { // 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; } // Float array if (ReferenceEquals(elementType, FloatType)) { var array = new float[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.Float32) return null; array[i] = context.ReadSingleUnsafe(); } return array; } // Short (Int16) array if (ReferenceEquals(elementType, ShortType)) { var array = new short[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.Int16) return null; array[i] = context.ReadInt16Unsafe(); } return array; } // UShort (UInt16) array if (ReferenceEquals(elementType, UShortType)) { var array = new ushort[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.UInt16) return null; array[i] = context.ReadUInt16Unsafe(); } return array; } // UInt32 array if (ReferenceEquals(elementType, UIntType)) { var array = new uint[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.UInt32) return null; array[i] = context.ReadVarUInt(); } return array; } // UInt64 array if (ReferenceEquals(elementType, ULongType)) { var array = new ulong[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.UInt64) return null; array[i] = context.ReadVarULong(); } return array; } // SByte (Int8) array if (ReferenceEquals(elementType, SByteType)) { var array = new sbyte[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.Int8) return null; array[i] = unchecked((sbyte)context.ReadByte()); } return array; } // Char array if (ReferenceEquals(elementType, CharType)) { var array = new char[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.Char) return null; array[i] = context.ReadCharUnsafe(); } return array; } // DateTimeOffset array if (ReferenceEquals(elementType, DateTimeOffsetType)) { var array = new DateTimeOffset[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.DateTimeOffset) return null; array[i] = context.ReadDateTimeOffsetUnsafe(); } return array; } // TimeSpan array if (ReferenceEquals(elementType, TimeSpanType)) { var array = new TimeSpan[count]; for (int i = 0; i < count; i++) { var typeCode = context.ReadByte(); if (typeCode != BinaryTypeCode.TimeSpan) return null; array[i] = context.ReadTimeSpanUnsafe(); } return array; } return null; } #endregion #region Dictionary Reading private static object? ReadDictionary(BinaryDeserializationContext context, Type targetType, int depth) where TInput : struct, IBinaryInputBase { 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) where TInput : struct, IBinaryInputBase { 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) where TInput : struct, IBinaryInputBase { 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; } } /// /// Sima string kihagy�sa - NEM regisztr�l. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SkipPlainString(BinaryDeserializationContext context) where TInput : struct, IBinaryInputBase { var byteLen = (int)context.ReadVarUInt(); if (byteLen > 0) { context.Skip(byteLen); } } /// /// Skip an interned string (StringInternFirst) - must still read cacheIndex and register in cache. /// Wire format: [StringInternFirst][VarUInt cacheIndex][VarUInt length][UTF8 bytes] /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SkipAndRegisterInternedString(BinaryDeserializationContext context) where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); var byteLen = (int)context.ReadVarUInt(); if (byteLen == 0) return; var str = context.ReadStringUtf8(byteLen); context.RegisterInternedValueAt(cacheIndex, str); } /// /// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache. /// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...] /// private static void SkipObjectRefFirst(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) where TInput : struct, IBinaryInputBase { var cacheIndex = (int)context.ReadVarUInt(); // Register placeholder (stream position as boxed int for potential lazy load) context.RegisterInternedValueAt(cacheIndex, context.Position); SkipObject(context, metaData); } ///// ///// Skip a string but still register it in the intern table if it meets the length threshold. ///// ///// Deserialization context ///// Position before the type code was read //[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); // } //} /// /// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot. /// private static void SkipObject(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) where TInput : struct, IBinaryInputBase { 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."); } /// /// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst. /// /// -1 = not cached, 0+ = register at this cache index private static void SkipObjectWithMetadata(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData, int cacheIndex) where TInput : struct, IBinaryInputBase { 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) where TInput : struct, IBinaryInputBase { var count = (int)context.ReadVarUInt(); for (int i = 0; i < count; i++) { SkipValue(context, metaData); } } private static void SkipDictionary(BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) where TInput : struct, IBinaryInputBase { 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 /// /// Gets Id as boxed object - only used for ChainMode (rare path). /// [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 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>(boxed, objParam).Compile(); } #endregion } // Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs