Files
RobustToolbox/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs
2023-07-30 20:17:22 +10:00

236 lines
8.8 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq.Expressions;
using Robust.Shared.Serialization.Manager.Definition;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Utility;
namespace Robust.Shared.Serialization.Manager;
public sealed partial class SerializationManager
{
#region DelegateElements
private delegate ValidationNode ValidationDelegate(DataNode node, ISerializationContext? context);
private readonly ConcurrentDictionary<(Type type, Type node), ValidationDelegate> _validationDelegates = new();
private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type node)
{
return _validationDelegates.GetOrAdd((type, node), static (key, manager) =>
{
var managerConst = Expression.Constant(manager);
var nodeParam = Expression.Parameter(typeof(DataNode), "node");
var contextParam = Expression.Parameter(typeof(ISerializationContext), "context");
Expression call;
bool couldRead = true;
if (manager._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), key.type, key.node, out var serializer))
{
var serializerConst = Expression.Constant(serializer);
call = Expression.Call(
managerConst,
nameof(ValidateNode),
new []{key.type, key.node},
serializerConst,
Expression.Convert(nodeParam, key.node),
contextParam);
}
else if (key.type.IsArray)
{
if (!key.node.IsAssignableTo(typeof(SequenceDataNode)))
{
call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for array.", true);
}
else
{
var elementType = key.type.GetElementType();
if (elementType == null)
throw new ArgumentException($"Failed to get ElementType of ArrayType {key.type}");
call = Expression.Call(
managerConst,
nameof(ValidateArray),
new[] { elementType },
Expression.Convert(nodeParam, typeof(SequenceDataNode)),
contextParam);
}
}
else if (key.type.IsEnum)
{
// Does not include cases where the target type is System.Enum.
// Those get handled by the generic enum serializer which uses reflection to resolve strings into enums.
DebugTools.Assert(key.type != typeof(Enum));
call = Expression.Call(
managerConst,
nameof(ValidateEnum),
new[] { key.type },
nodeParam);
}
else if (key.type.IsAssignableTo(typeof(ISelfSerialize)))
{
if (key.node.IsAssignableTo(typeof(ValueDataNode)))
{
call = manager.ValidateNodeExpression(nodeParam);
}
else
{
call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for ISelfSerialize");
}
}
else if (manager.TryGetDefinition(key.type, out var dataDefinition))
{
var dataDefConst = Expression.Constant(dataDefinition, typeof(DataDefinition<>).MakeGenericType(key.type));
call = Expression.Call(
managerConst,
nameof(ValidateDataDefinition),
new []{key.type},
nodeParam,
dataDefConst,
contextParam);
}
else
{
call = manager.ErrorNodeExpression(nodeParam, "Failed to read node.", false);
couldRead = false;
}
if (couldRead)
{
//insert a nullcheck at the beginning, but ONLY if we are actually found a way of validating this node
call = Expression.Condition(
Expression.Call(
typeof(SerializationManager),
nameof(IsNull),
Type.EmptyTypes,
nodeParam),
Expression.Convert(key.type.IsNullable()
? manager.ValidateNodeExpression(nodeParam)
: manager.ErrorNodeExpression(nodeParam, "Non-nullable field contained a null value", true), typeof(ValidationNode)),
Expression.Convert(call, typeof(ValidationNode)));
}
return Expression.Lambda<ValidationDelegate>(
call,
nodeParam,
contextParam).Compile();
}, this);
}
private Expression ErrorNodeExpression(ParameterExpression nodeParam, string message, bool alwaysRelevant = true)
{
return ExpressionUtils.NewExpression<ErrorNode>(nodeParam, message, alwaysRelevant);
}
private Expression ValidateNodeExpression(ParameterExpression nodeParam)
{
return ExpressionUtils.NewExpression<ValidatedValueNode>(nodeParam);
}
private ValidationNode ValidateArray<TElem>(SequenceDataNode sequenceDataNode, ISerializationContext? context)
{
var validatedList = new List<ValidationNode>();
foreach (var dataNode in sequenceDataNode.Sequence)
{
validatedList.Add(ValidateNode<TElem>(dataNode, context));
}
return new ValidatedSequenceNode(validatedList);
}
private ValidationNode ValidateEnum<T>(DataNode node)
{
var enumName = node switch
{
ValueDataNode valueNode => valueNode.Value,
SequenceDataNode sequenceNode => string.Join(", ", sequenceNode.Sequence),
_ => null
};
if (enumName == null)
{
return new ErrorNode(node, $"Invalid node type {node.GetType()} for enum {typeof(T)}.");
}
if (!Enum.TryParse(typeof(T), enumName, true, out var enumValue))
{
return new ErrorNode(node, $"{enumValue} is not a valid enum value of type {typeof(T)}", false);
}
return new ValidatedValueNode(node);
}
private ValidationNode ValidateDataDefinition<T>(DataNode node, DataDefinition<T> dataDefinition, ISerializationContext? context) where T : notnull
{
return node switch
{
ValueDataNode valueDataNode => valueDataNode.Value == ""
? new ValidatedValueNode(valueDataNode)
: new ErrorNode(node, "Invalid NodeType for DataDefinition", false),
MappingDataNode mappingDataNode => dataDefinition.Validate(this, mappingDataNode, context),
_ => new ErrorNode(node, "Invalid NodeType for DataDefinition")
};
}
#endregion
public ValidationNode ValidateNode<T>(DataNode node, ISerializationContext? context = null)
{
return ValidateNode(typeof(T), node, context);
}
public ValidationNode ValidateNode<T, TNode>(ITypeValidator<T, TNode> typeValidator, TNode node,
ISerializationContext? context = null) where TNode : DataNode
{
return typeValidator.Validate(this, node, DependencyCollection, context);
}
public ValidationNode ValidateNode<T, TNode, TValidator>(TNode node,
ISerializationContext? context = null) where TNode : DataNode where TValidator : ITypeValidator<T, TNode>
{
return ValidateNode(GetOrCreateCustomTypeSerializer<TValidator>(), node, context);
}
public ValidationNode ValidateNode(Type type, DataNode node, ISerializationContext? context = null)
{
var underlyingType = type.GetUnderlyingType();
if (underlyingType != null) // implies that type was nullable
{
if (IsNull(node))
return new ValidatedValueNode(node);
}
else
{
underlyingType = type;
}
if (node.Tag?.StartsWith("!type:") == true)
{
var typeString = node.Tag.Substring(6);
try
{
underlyingType = ResolveConcreteType(underlyingType, typeString);
}
catch (InvalidOperationException)
{
return new ErrorNode(node, $"Failed to resolve !type tag: {typeString}", false);
}
}
return GetOrCreateValidationDelegate(underlyingType, node.GetType())(node, context);
}
}