// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using FlaxEngine.Interop; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; #nullable enable namespace FlaxEngine.Json.JsonCustomSerializers { internal class ExtendedSerializationBinder : SerializationBinder, ISerializationBinder { private record struct TypeKey(string? assemblyName, string typeName); private ConcurrentDictionary _typeCache; private Func _resolveType; /// Clear the cache /// Should be cleared on scripting domain reload to avoid out of date types participating in dynamic type resolution public void ResetCache() { _typeCache.Clear(); } public override Type BindToType(string? assemblyName, string typeName) { return FindCachedType(new(assemblyName, typeName)); } public override void BindToName(Type serializedType, out string? assemblyName, out string? typeName) { assemblyName = serializedType.Assembly.FullName; typeName = serializedType.FullName; } public ExtendedSerializationBinder() { _resolveType = ResolveType; _typeCache = new(); } Type ResolveType(TypeKey key) { Type? type = null; if (key.assemblyName is null) { // No assembly name, attempt to find globally type = FindTypeGlobal(key.typeName); } if (type is null && key.assemblyName is not null) { // Type not found yet, but we have assembly name var assembly = ResolveAssembly(new(key.assemblyName)); // We have assembly, attempt to load from assembly type = FindTypeInAssembly(key.typeName, assembly); } //if (type is null) // type = _fallBack.BindToType(key.assemblyName, key.typeName); // Use fallback if (type is null) throw MakeTypeResolutionException(key.assemblyName, key.typeName); return type; } Assembly ResolveAssembly(AssemblyName name) { Assembly? assembly = null; assembly = FindScriptingAssembly(name); // Attempt to find in scripting assemblies if (assembly is null) assembly = FindLoadAssembly(name); // Attempt to load if (assembly is null) assembly = FindDomainAssembly(name); // Attempt to find in the current domain if (assembly is null) throw MakeAsmResolutionException(name.FullName); // Assembly failed to resolve return assembly; } /// Attempt to find the assembly among loaded scripting assemblies Assembly? FindScriptingAssembly(AssemblyName assemblyName) { return NativeInterop.ResolveScriptingAssemblyByName(assemblyName, allowPartial: true); } /// Attempt to find the assembly in the current domain Assembly? FindDomainAssembly(AssemblyName assemblyName) { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { // Looking in domain may be necessary (in case of anon dynamic assembly for example) var curName = assembly.GetName(); if (curName == assemblyName || curName.Name == assemblyName.Name) return assembly; } return null; } /// Attempt to load the assembly Assembly? FindLoadAssembly(AssemblyName assemblyName) { Assembly? assembly = null; assembly = Assembly.Load(assemblyName); if (assembly is null && assemblyName.Name is not null) #pragma warning disable CS0618 // Type or member is obsolete assembly = Assembly.LoadWithPartialName(assemblyName.Name); // Copying behavior of DefaultSerializationBinder #pragma warning restore CS0618 // Type or member is obsolete return assembly; } /// Attempt to find a type in a specified assembly Type? FindTypeInAssembly(string typeName, Assembly assembly) { var type = assembly.GetType(typeName); // Attempt to load directly if (type is null && typeName.IndexOf('`') >= 0) // Attempt failed, but name has generic variant tick, try resolving generic manually type = FindTypeGeneric(typeName, assembly); return type; } /// Attempt to find unqualified type by only name Type? FindTypeGlobal(string typeName) { return Type.GetType(typeName); } /// Get type from the cache private Type FindCachedType(TypeKey key) { return _typeCache.GetOrAdd(key, _resolveType); } /********************************************* ** Below code is adapted from Newtonsoft.Json *********************************************/ /// Attempt to recursively resolve a generic type private Type? FindTypeGeneric(string typeName, Assembly assembly) { Type? type = null; int openBracketIndex = typeName.IndexOf('[', StringComparison.Ordinal); if (openBracketIndex >= 0) { string genericTypeDefName = typeName.Substring(0, openBracketIndex); // Find the unspecialized type Type? genericTypeDef = assembly.GetType(genericTypeDefName); if (genericTypeDef != null) { List genericTypeArguments = new List(); // Recursively resolve the arguments int scope = 0; int typeArgStartIndex = 0; int endIndex = typeName.Length - 1; for (int i = openBracketIndex + 1; i < endIndex; ++i) { char current = typeName[i]; switch (current) { case '[': if (scope == 0) { typeArgStartIndex = i + 1; } ++scope; break; case ']': --scope; if (scope == 0) { // All arguments resolved, compose our type string typeArgAssemblyQualifiedName = typeName.Substring(typeArgStartIndex, i - typeArgStartIndex); TypeKey typeNameKey = SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName); genericTypeArguments.Add(FindCachedType(typeNameKey)); } break; } } type = genericTypeDef.MakeGenericType(genericTypeArguments.ToArray()); } } return type; } /// Split a fully qualified type name into assembly name, and type name private static TypeKey SplitFullyQualifiedTypeName(string fullyQualifiedTypeName) { int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName); ReadOnlySpan typeName, assemblyName; if (assemblyDelimiterIndex != null) { typeName = fullyQualifiedTypeName.AsSpan().Slice(0, assemblyDelimiterIndex ?? 0); assemblyName = fullyQualifiedTypeName.AsSpan().Slice((assemblyDelimiterIndex ?? 0) + 1); } else { typeName = fullyQualifiedTypeName; assemblyName = null; } return new(new(assemblyName), new(typeName)); } /// Find the assembly name inside a fully qualified type name private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName) { // we need to get the first comma following all surrounded in brackets because of generic types // e.g. System.Collections.Generic.Dictionary`2[[System.String, mscorlib,Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 int scope = 0; for (int i = 0; i < fullyQualifiedTypeName.Length; i++) { char current = fullyQualifiedTypeName[i]; switch (current) { case '[': scope++; break; case ']': scope--; break; case ',': if (scope == 0) { return i; } break; } } return null; } private static JsonSerializationException MakeAsmResolutionException(string asmName) { return new($"Could not load assembly '{asmName}'."); } private static JsonSerializationException MakeTypeResolutionException(string? asmName, string typeName) { if (asmName is null) return new($"Could not find '{typeName}'"); else return new($"Could not find '{typeName}' in assembly '{asmName}'."); } } }