// 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);
}
}
}