378 lines
11 KiB
C#
378 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
public static partial class AcBinaryDeserializer
|
|
{
|
|
internal sealed partial class BinaryDeserializationContext
|
|
{
|
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
|
|
|
#region Buffer State — owned by context for zero virtual dispatch
|
|
|
|
internal byte[] _buffer = null!;
|
|
internal int _bufferLength;
|
|
internal int _position;
|
|
|
|
#endregion
|
|
|
|
// String caching state — needed for WASM optimization
|
|
// The cache dictionary is owned by context (pooled), passed in at init time.
|
|
private bool _useStringCaching;
|
|
private int _maxCachedStringLength;
|
|
|
|
public bool IsAtEnd
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => _position >= _bufferLength;
|
|
}
|
|
|
|
public int Position
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => _position;
|
|
}
|
|
|
|
#region Core Read Methods
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public byte ReadByte()
|
|
{
|
|
if (_position >= _bufferLength)
|
|
{
|
|
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
|
}
|
|
|
|
return _buffer[_position++];
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public byte PeekByte()
|
|
{
|
|
if (_position >= _bufferLength)
|
|
{
|
|
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
|
}
|
|
|
|
return _buffer[_position];
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Skip(int count)
|
|
{
|
|
EnsureAvailable(count);
|
|
_position += count;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fixed-Width Reads
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public short ReadInt16Unsafe()
|
|
{
|
|
EnsureAvailable(2);
|
|
var value = Unsafe.ReadUnaligned<short>(ref _buffer[_position]);
|
|
_position += 2;
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public ushort ReadUInt16Unsafe()
|
|
{
|
|
EnsureAvailable(2);
|
|
var value = Unsafe.ReadUnaligned<ushort>(ref _buffer[_position]);
|
|
_position += 2;
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public char ReadCharUnsafe()
|
|
{
|
|
EnsureAvailable(2);
|
|
var value = (char)Unsafe.ReadUnaligned<ushort>(ref _buffer[_position]);
|
|
_position += 2;
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public float ReadSingleUnsafe()
|
|
{
|
|
EnsureAvailable(4);
|
|
var bits = Unsafe.ReadUnaligned<int>(ref _buffer[_position]);
|
|
_position += 4;
|
|
return BitConverter.Int32BitsToSingle(bits);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public double ReadDoubleUnsafe()
|
|
{
|
|
EnsureAvailable(8);
|
|
var bits = Unsafe.ReadUnaligned<long>(ref _buffer[_position]);
|
|
_position += 8;
|
|
return BitConverter.Int64BitsToDouble(bits);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public decimal ReadDecimalUnsafe()
|
|
{
|
|
EnsureAvailable(16);
|
|
var span = _buffer.AsSpan(_position, 16);
|
|
var ints = MemoryMarshal.Cast<byte, int>(span);
|
|
var lo = ints[0];
|
|
var mid = ints[1];
|
|
var hi = ints[2];
|
|
var flags = ints[3];
|
|
var isNegative = (flags & unchecked((int)0x80000000)) != 0;
|
|
var scale = (byte)((flags >> 16) & 0x7F);
|
|
_position += 16;
|
|
return new decimal(lo, mid, hi, isNegative, scale);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public DateTime ReadDateTimeUnsafe()
|
|
{
|
|
EnsureAvailable(9);
|
|
var ticks = Unsafe.ReadUnaligned<long>(ref _buffer[_position]);
|
|
var kind = (DateTimeKind)_buffer[_position + 8];
|
|
_position += 9;
|
|
return new DateTime(ticks, kind);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public DateTimeOffset ReadDateTimeOffsetUnsafe()
|
|
{
|
|
EnsureAvailable(10);
|
|
var utcTicks = Unsafe.ReadUnaligned<long>(ref _buffer[_position]);
|
|
var offsetMinutes = Unsafe.ReadUnaligned<short>(ref _buffer[_position + 8]);
|
|
_position += 10;
|
|
var utcValue = new DateTime(utcTicks, DateTimeKind.Utc);
|
|
return new DateTimeOffset(utcValue).ToOffset(TimeSpan.FromMinutes(offsetMinutes));
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public TimeSpan ReadTimeSpanUnsafe()
|
|
{
|
|
EnsureAvailable(8);
|
|
var ticks = Unsafe.ReadUnaligned<long>(ref _buffer[_position]);
|
|
_position += 8;
|
|
return new TimeSpan(ticks);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Guid ReadGuidUnsafe()
|
|
{
|
|
EnsureAvailable(16);
|
|
var value = new Guid(_buffer.AsSpan(_position, 16));
|
|
_position += 16;
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public int ReadInt32Raw()
|
|
{
|
|
EnsureAvailable(4);
|
|
var value = Unsafe.ReadUnaligned<int>(ref _buffer[_position]);
|
|
_position += 4;
|
|
return value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VarInt Reading
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public int ReadVarInt()
|
|
{
|
|
var raw = ReadVarUInt();
|
|
var value = (int)(raw >> 1) ^ -(int)(raw & 1);
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public uint ReadVarUInt()
|
|
{
|
|
// Fast path: single byte (0-127) - ~70% of cases
|
|
var b0 = _buffer[_position];
|
|
if ((b0 & 0x80) == 0)
|
|
{
|
|
_position++;
|
|
return b0;
|
|
}
|
|
|
|
// Fast path: two bytes (128-16383) - ~25% of cases
|
|
if (_position + 1 < _bufferLength)
|
|
{
|
|
var b1 = _buffer[_position + 1];
|
|
if ((b1 & 0x80) == 0)
|
|
{
|
|
_position += 2;
|
|
return (uint)(b0 & 0x7F) | ((uint)b1 << 7);
|
|
}
|
|
}
|
|
|
|
// Slow path: 3+ bytes - ~5% of cases
|
|
return ReadVarUIntSlow();
|
|
}
|
|
|
|
private uint ReadVarUIntSlow()
|
|
{
|
|
uint value = 0;
|
|
var shift = 0;
|
|
while (true)
|
|
{
|
|
var b = ReadByte();
|
|
value |= (uint)(b & 0x7F) << shift;
|
|
if ((b & 0x80) == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
shift += 7;
|
|
if (shift > 35)
|
|
{
|
|
throw new AcBinaryDeserializationException("Invalid VarUInt encoding.", _position);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public long ReadVarLong()
|
|
{
|
|
var raw = ReadVarULong();
|
|
var value = (long)(raw >> 1) ^ -((long)raw & 1);
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public ulong ReadVarULong()
|
|
{
|
|
ulong value = 0;
|
|
var shift = 0;
|
|
while (true)
|
|
{
|
|
var b = ReadByte();
|
|
value |= (ulong)(b & 0x7F) << shift;
|
|
if ((b & 0x80) == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
shift += 7;
|
|
if (shift > 70)
|
|
{
|
|
throw new AcBinaryDeserializationException("Invalid VarULong encoding.", _position);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bytes & String Reading
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public byte[] ReadBytes(int length)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return Array.Empty<byte>();
|
|
}
|
|
|
|
EnsureAvailable(length);
|
|
var result = GC.AllocateUninitializedArray<byte>(length);
|
|
_buffer.AsSpan(_position, length).CopyTo(result);
|
|
_position += length;
|
|
return result;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public string ReadStringUtf8(int length)
|
|
{
|
|
if (length == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
EnsureAvailable(length);
|
|
|
|
// WASM optimization: cache short strings to reduce allocations
|
|
if (_useStringCaching && length <= _maxCachedStringLength)
|
|
{
|
|
return ReadStringUtf8Cached(length);
|
|
}
|
|
|
|
var value = Utf8NoBom.GetString(_buffer, _position, length);
|
|
_position += length;
|
|
return value;
|
|
}
|
|
|
|
private string ReadStringUtf8Cached(int length)
|
|
{
|
|
var slice = _buffer.AsSpan(_position, length);
|
|
var hash = ComputeStringHashFull(slice);
|
|
|
|
if (_stringCache!.TryGetValue(hash, out var cached))
|
|
{
|
|
if (cached.Length == length && VerifyAsciiUtf8Match(cached, slice))
|
|
{
|
|
_position += length;
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
var value = Utf8NoBom.GetString(slice);
|
|
_stringCache[hash] = value;
|
|
_position += length;
|
|
return value;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool VerifyAsciiUtf8Match(string cached, ReadOnlySpan<byte> utf8Bytes)
|
|
{
|
|
return Ascii.Equals(utf8Bytes, cached);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Full-content hash for string caching.
|
|
/// CRITICAL: DO NOT SIMPLIFY — prevents hash collisions for similar property names.
|
|
/// See BinaryDeserializationContext for full history.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int ComputeStringHashFull(ReadOnlySpan<byte> data)
|
|
{
|
|
if (data.Length <= 32)
|
|
{
|
|
var hash = new HashCode();
|
|
hash.AddBytes(data);
|
|
return hash.ToHashCode();
|
|
}
|
|
|
|
var h = new HashCode();
|
|
h.Add(data.Length);
|
|
h.AddBytes(data.Slice(0, 8));
|
|
h.AddBytes(data.Slice(data.Length - 8, 8));
|
|
h.AddBytes(data.Slice(data.Length / 2 - 4, 8));
|
|
return h.ToHashCode();
|
|
}
|
|
|
|
#endregion
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void EnsureAvailable(int length)
|
|
{
|
|
if (_position > _bufferLength - length)
|
|
{
|
|
throw new AcBinaryDeserializationException("Unexpected end of binary payload.", _position);
|
|
}
|
|
}
|
|
}
|
|
}
|