// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Storage.Nodes; namespace EpicGames.Horde.Storage { /// /// Handles serialization of blobs using instances. /// public static class BlobSerializer { /// /// Deserialize an object /// /// Return type for deserialization /// Data to deserialize from /// Options to control serialization public static T Deserialize(BlobData blobData, BlobSerializerOptions? options = null) { options ??= BlobSerializerOptions.Default; BlobReader reader = new BlobReader(blobData, options); return options.GetConverter().Read(reader, options); } /// /// Serialize an object into a blob /// /// Type of object to serialize /// Writer for the blob data /// Object to serialize /// Options to control serialization /// Type of the serialized blob public static BlobType Serialize(IBlobWriter writer, T value, BlobSerializerOptions? options = null) { options ??= BlobSerializerOptions.Default; return options.GetConverter().Write(writer, value, options); } } /// /// Options for serializing blobs /// public class BlobSerializerOptions { class FreezableList : IList { readonly BlobSerializerOptions _owner; readonly List _list; public FreezableList(BlobSerializerOptions owner) { _owner = owner; _list = new List(); } public T this[int index] { get => _list[index]; set { _owner.CheckMutable(); _list[index] = value; } } public int Count => _list.Count; public bool IsReadOnly => _owner._readOnly; public void Add(T item) { _owner.CheckMutable(); _list.Add(item); } public void Clear() { _owner.CheckMutable(); _list.Clear(); } public bool Contains(T item) => _list.Contains(item); public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); public IEnumerator GetEnumerator() => _list.GetEnumerator(); public int IndexOf(T item) => _list.IndexOf(item); public void Insert(int index, T item) { _owner.CheckMutable(); _list.Insert(index, item); } public bool Remove(T item) { _owner.CheckMutable(); return _list.Remove(item); } public void RemoveAt(int index) { _owner.CheckMutable(); _list.RemoveAt(index); } IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); } readonly ConcurrentDictionary _cachedConverters = new ConcurrentDictionary(); static readonly ConcurrentDictionary s_cachedDefaultConverters = new ConcurrentDictionary(); static readonly Func s_createDefaultConverter = CreateDefaultConverter; bool _readOnly; readonly FreezableList _converters; /// /// Known converter types /// public IList Converters => _converters; /// /// Default options instance /// public static BlobSerializerOptions Default { get; } = CreateDefaultOptions(); /// /// Constructor /// public BlobSerializerOptions() { _converters = new FreezableList(this); } void CheckMutable() { if (_readOnly) { throw new NotSupportedException("Options instance is read-only"); } } static BlobSerializerOptions CreateDefaultOptions() { BlobSerializerOptions options = new BlobSerializerOptions(); options.MakeReadOnly(); return options; } /// /// Create a read-only version of these options /// /// public void MakeReadOnly() => _readOnly = true; /// /// Gets a converter for the given type /// public BlobConverter GetConverter() { return (BlobConverter)_cachedConverters.GetOrAdd(typeof(T), CreateConverter); } BlobConverter CreateConverter(Type type) { foreach (BlobConverter converter in Converters) { if (converter.CanConvert(type)) { return converter; } } return s_cachedDefaultConverters.GetOrAdd(type, s_createDefaultConverter); } static BlobConverter CreateDefaultConverter(Type type) { BlobConverterAttribute? attribute = type.GetCustomAttribute(); if (attribute != null) { Type converterType = attribute.ConverterType; if (converterType.IsGenericTypeDefinition) { converterType = converterType.MakeGenericType(type.GetGenericArguments()); } return (BlobConverter)Activator.CreateInstance(converterType)!; } throw new NotSupportedException($"No converter is available to handle type {type.Name}"); } /// /// Creates options for serializing blobs compatible with a particular server API version /// /// The server API version public static BlobSerializerOptions Create(HordeApiVersion version) { BlobSerializerOptions options = new BlobSerializerOptions(); options.Converters.Add(new InteriorChunkedDataNodeConverter(version)); options.Converters.Add(new DirectoryNodeConverter(version)); return options; } } /// /// Extension methods for serializing blob types /// public static class BlobSerializerExtensions { /// /// Deserialize an object /// /// Return type for the deserialized object /// Handle to the blob to deserialize /// Options to control serialization /// Cancellation token for the operation public static async ValueTask ReadBlobAsync(this IBlobRef handle, BlobSerializerOptions? options = null, CancellationToken cancellationToken = default) { using BlobData data = await handle.ReadBlobDataAsync(cancellationToken); return BlobSerializer.Deserialize(data, options); } /// /// Deserialize an object /// /// Return type for the deserialized object /// Handle to the blob to deserialize /// Cancellation token for the operation public static async ValueTask ReadBlobAsync(this IBlobRef handle, CancellationToken cancellationToken = default) { using BlobData data = await handle.ReadBlobDataAsync(cancellationToken); return BlobSerializer.Deserialize(data, handle.SerializerOptions); } /// /// Deserialize an object /// /// Return type for the deserialized object /// Handle to the blob to deserialize /// Cancellation token for the operation public static async ValueTask ReadBlobAsync(this IHashedBlobRef handle, CancellationToken cancellationToken = default) { using BlobData data = await handle.ReadBlobDataAsync(cancellationToken); return BlobSerializer.Deserialize(data, handle.SerializerOptions); } /// /// Serialize an object to storage /// /// Writer for serialized data /// The object to serialize /// Cancellation token for the operation /// Handle to the serialized blob public static async ValueTask> WriteBlobAsync(this IBlobWriter writer, T value, CancellationToken cancellationToken = default) { BlobType blobType = BlobSerializer.Serialize(writer, value, writer.Options); return await writer.CompleteAsync(blobType, cancellationToken); } /// /// Reads data for a ref from the store, along with the node's contents. /// /// Store instance to write to /// The ref name /// Minimum coherency for any cached value to be returned /// Options to control serialization /// Cancellation token for the operation /// Node for the given ref, or null if it does not exist public static async Task?> TryReadRefAsync(this IStorageNamespace store, RefName name, DateTime cacheTime = default, BlobSerializerOptions? options = null, CancellationToken cancellationToken = default) where TNode : class { IHashedBlobRef? refTarget = await store.TryReadRefAsync(name, cacheTime, cancellationToken); if (refTarget == null) { return null; } return HashedBlobRef.Create(refTarget.Hash, refTarget, options ?? BlobSerializerOptions.Default); } /// /// Reads a ref from the store, throwing an exception if it does not exist /// /// Store instance to write to /// Id for the ref /// Minimum coherency of any cached result /// Options to control serialization /// Cancellation token for the operation /// The blob instance public static async Task> ReadRefAsync(this IStorageNamespace store, RefName name, DateTime cacheTime = default, BlobSerializerOptions? options = null, CancellationToken cancellationToken = default) where TNode : class { IHashedBlobRef? refValue = await store.TryReadRefAsync(name, cacheTime, options, cancellationToken); if (refValue == null) { throw new RefNameNotFoundException(name); } return refValue; } /// /// Reads data for a ref from the store, along with the node's contents. /// /// Store instance to write to /// The ref name /// Minimum coherency for any cached value to be returned /// Options to control serialization /// Cancellation token for the operation /// Node for the given ref, or null if it does not exist public static async Task TryReadRefTargetAsync(this IStorageNamespace store, RefName name, DateTime cacheTime = default, BlobSerializerOptions? options = null, CancellationToken cancellationToken = default) where TNode : class { IHashedBlobRef? refTarget = await store.TryReadRefAsync(name, cacheTime, options, cancellationToken); if (refTarget == null) { return null; } return await refTarget.ReadBlobAsync(cancellationToken); } /// /// Reads a ref from the store, throwing an exception if it does not exist /// /// Store instance to write to /// Id for the ref /// Minimum coherency of any cached result /// Options to control serialization /// Cancellation token for the operation /// The blob instance public static async Task ReadRefTargetAsync(this IStorageNamespace store, RefName name, DateTime cacheTime = default, BlobSerializerOptions? options = null, CancellationToken cancellationToken = default) where TNode : class { IHashedBlobRef blobRef = await ReadRefAsync(store, name, cacheTime, options, cancellationToken); return await blobRef.ReadBlobAsync(cancellationToken); } } }