using System.IO; namespace Sandbox; /// /// Manages binary blobs during JSON serialization with support for nested contexts. /// internal static class BlobDataSerializer { public const string CompiledBlobName = "DBLOB"; private const int DefaultStreamSize = 4096; private const int HeaderSize = 8; private const int TocEntrySize = 32; private readonly record struct BlobEntry( Guid Guid, int Version, byte[] Data, long Offset ); private static BlobContext _current; /// /// Indicates whether a blob context is currently active. /// internal static bool IsActive => _current != null; /// /// Registers a instance for serialization in the current blob context. /// Returns a that can be used to reference the blob in JSON. /// If no blob context is active, returns . /// Called automatically by BinaryBlobJsonConverter during JSON serialization. /// internal static Guid RegisterBlob( BlobData blob ) { if ( _current == null ) return Guid.Empty; var blobs = _current.Blobs; var guid = Guid.NewGuid(); blobs[guid] = blob; return guid; } internal static BlobData ReadBlob( Guid guid, Type expectedType ) { if ( _current == null ) return null; var binaryData = _current.BinaryData; if ( binaryData == null || !binaryData.TryGetValue( guid, out var blobData ) ) return null; if ( Activator.CreateInstance( expectedType ) is not BlobData instance ) return null; var stream = ByteStream.CreateReader( blobData ); try { int dataVersion = stream.Read(); var reader = new BlobData.Reader { Stream = stream, DataVersion = dataVersion }; if ( dataVersion < instance.Version ) instance.Upgrade( ref reader, dataVersion ); else instance.Deserialize( ref reader ); } finally { stream.Dispose(); } return instance; } private static byte[] GetBlobData( Dictionary blobs ) { if ( blobs == null || blobs.Count == 0 ) return null; long dataStart = HeaderSize + blobs.Count * TocEntrySize; using var entries = new PooledSpan( blobs.Count ); var entrySpan = entries.Span; int entryCount = 0; long offset = dataStart; foreach ( var kvp in blobs ) { var blobStream = ByteStream.Create( DefaultStreamSize ); try { blobStream.Write( kvp.Value.Version ); var writer = new BlobData.Writer { Stream = blobStream }; kvp.Value.Serialize( ref writer ); blobStream = writer.Stream; byte[] data = blobStream.ToArray(); entrySpan[entryCount++] = new BlobEntry( kvp.Key, kvp.Value.Version, data, offset ); offset += data.Length; } finally { blobStream.Dispose(); } } var outputStream = ByteStream.Create( (int)offset ); try { outputStream.Write( 1 ); outputStream.Write( blobs.Count ); for ( int i = 0; i < entryCount; i++ ) { ref readonly var entry = ref entrySpan[i]; outputStream.Write( entry.Guid.ToByteArray() ); outputStream.Write( entry.Version ); outputStream.Write( entry.Offset ); outputStream.Write( entry.Data.Length ); } for ( int i = 0; i < entryCount; i++ ) { outputStream.Write( entrySpan[i].Data ); } return outputStream.ToArray(); } finally { outputStream.Dispose(); } } private static Dictionary ParseFile( ReadOnlySpan data ) { var result = new Dictionary(); if ( data.Length == 0 ) return result; try { var stream = ByteStream.CreateReader( data ); try { int version = stream.Read(); if ( version != 1 ) return result; int count = stream.Read(); var toc = new List<(Guid guid, long offset, int size)>( count ); for ( int i = 0; i < count; i++ ) { var guid = stream.Read(); stream.Read(); long blobOffset = stream.Read(); int size = stream.Read(); toc.Add( (guid, blobOffset, size) ); } foreach ( var (guid, blobOffset, size) in toc ) { stream.Position = (int)blobOffset; var buffer = new byte[size]; stream.Read( buffer, 0, size ); result[guid] = buffer; } } finally { stream.Dispose(); } } catch ( Exception e ) { Log.Warning( e, "Failed to parse blob data" ); } return result; } /// /// Create a blob serialization context for capturing blobs. /// public static BlobContext Capture() { var context = new BlobContext( _current ); _current = context; return context; } /// /// Load blob data from memory if available, otherwise from file path. /// public static BlobContext Load( byte[] data, string filePath ) { return data != null ? LoadFromMemory( data ) : LoadFrom( filePath ); } /// /// Create a blob deserialization context from in-memory data. /// public static BlobContext LoadFromMemory( ReadOnlySpan data ) { var binaryData = data.Length > 0 ? ParseFile( data ) : null; var context = new BlobContext( _current, binaryData ); _current = context; return context; } /// /// Create a blob deserialization context from a file. /// public static BlobContext LoadFrom( string filePath ) { Dictionary binaryData = null; if ( !string.IsNullOrEmpty( filePath ) ) { if ( filePath.EndsWith( "_c" ) ) filePath = filePath[..^2]; var path = filePath + "_d"; if ( FileSystem.Mounted?.FileExists( path ) == true ) binaryData = ParseFile( FileSystem.Mounted.ReadAllBytes( path ) ); else if ( File.Exists( path ) ) binaryData = ParseFile( File.ReadAllBytes( path ) ); } var context = new BlobContext( _current, binaryData ); _current = context; return context; } /// /// A disposable context for blob serialization/deserialization. /// public sealed class BlobContext : IDisposable { internal readonly Dictionary Blobs; internal readonly Dictionary BinaryData; private readonly BlobContext _previous; internal BlobContext( BlobContext previous, Dictionary binaryData = null ) { Blobs = new(); BinaryData = binaryData ?? new(); _previous = previous; } public byte[] ToByteArray() => GetBlobData( Blobs ); public bool SaveTo( string filePath ) { var data = GetBlobData( Blobs ); if ( data == null || data.Length == 0 ) return false; try { File.WriteAllBytes( filePath + "_d", data ); return true; } catch ( Exception e ) { Log.Warning( e, "Failed to write blob file" ); return false; } } public void Dispose() => _current = _previous; } }