diff --git a/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs b/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs
new file mode 100644
index 0000000000..b31fe9f6e4
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/ValidateChildrenAttribute.cs
@@ -0,0 +1,19 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes
+{
+ using System;
+
+ ///
+ /// Checks all properties in the target object for validators.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ValidateChildrenAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs
new file mode 100644
index 0000000000..a92da9244a
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs
@@ -0,0 +1,37 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Checks if value is in list of available values.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class AvailableValuesAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public AvailableValuesAttribute(params object[] values)
+ {
+ Values = values;
+ }
+
+ ///
+ /// Gets the array of possible values.
+ ///
+ public object[] Values { get; }
+
+ ///
+ public bool Check(object other) => Values.Contains(other);
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs
new file mode 100644
index 0000000000..611b416070
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs
@@ -0,0 +1,49 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+ using System.Collections.Generic;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Check a value with custom function.
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+ public class CustomValidatorAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the custom check validator.
+ ///
+ /// The from customFunctionType must be a class inheriting IValidator with a parameterless constructor.
+ ///
+ public CustomValidatorAttribute(Type customFunctionType)
+ {
+ if (!customFunctionType.IsClass || customFunctionType.IsAbstract || !customFunctionType.GetInterfaces().Contains(typeof(IValidator)))
+ throw new ArgumentException($"{nameof(customFunctionType)} must be a type inheriting IValidator!");
+
+ CustomFunctionType = customFunctionType;
+ }
+
+ ///
+ /// Gets a from a type inheriting , to an instance of that class.
+ ///
+ public static Dictionary ValidatorInstances { get; } = new();
+
+ ///
+ /// Gets the type of the custom check validator.
+ ///
+ public Type CustomFunctionType { get; }
+
+ ///
+ public bool Check(object other) => ValidatorInstances.GetOrAdd(CustomFunctionType, () => (IValidator)Activator.CreateInstance(CustomFunctionType)).Check(other);
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs
new file mode 100644
index 0000000000..b31d3cbc83
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs
@@ -0,0 +1,38 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Checks if value greater or equal.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class GreaterOrEqualAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// value must be able to convert to your target type via .
+ public GreaterOrEqualAttribute(object value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets the minimum value.
+ ///
+ public object Value { get; }
+
+ ///
+ public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable min && min.CompareTo(other) <= 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs
new file mode 100644
index 0000000000..5bb1025bd9
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs
@@ -0,0 +1,38 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Interfaces;
+
+ ///
+ /// Check if value is greater.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class GreaterThanAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// value must be able to convert to your target type via .
+ public GreaterThanAttribute(object value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets the minimum value.
+ ///
+ public object Value { get; }
+
+ ///
+ public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable min && min.CompareTo(other) < 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs
new file mode 100644
index 0000000000..e521a47bd1
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs
@@ -0,0 +1,38 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Checks if value less or equal.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class LessOrEqualAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// value must be able to convert to your target type via .
+ public LessOrEqualAttribute(object value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets the maximum value.
+ ///
+ public object Value { get; }
+
+ ///
+ public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable max && max.CompareTo(other) >= 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs
new file mode 100644
index 0000000000..5497a99fa6
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs
@@ -0,0 +1,38 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Checks if value is less.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class LessThanAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// value must be able to convert to your target type via .
+ public LessThanAttribute(object value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Gets the maximum value.
+ ///
+ public object Value { get; }
+
+ ///
+ public bool Check(object other) => Convert.ChangeType(Value, other.GetType()) is IComparable max && max.CompareTo(other) > 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs
new file mode 100644
index 0000000000..914ce09d59
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs
@@ -0,0 +1,23 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Checks if value is 0 or greater.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class NonNegativeAttribute : Attribute, IValidator
+ {
+ ///
+ public bool Check(object other) => Convert.ToDecimal(other) >= 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs
new file mode 100644
index 0000000000..a473405e49
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs
@@ -0,0 +1,23 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Check if value is 0 or less.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class NonPositiveAttribute : Attribute, IValidator
+ {
+ ///
+ public bool Check(object other) => Convert.ToDecimal(other) <= 0;
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs
new file mode 100644
index 0000000000..ebf0c1d10d
--- /dev/null
+++ b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs
@@ -0,0 +1,57 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Features.Attributes.Validators
+{
+ using System;
+
+ using Exiled.API.Interfaces;
+
+ ///
+ /// Check if an is inside a specific range.
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public class RangeAttribute : Attribute, IValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ ///
+ /// min and max must inherit .
+ public RangeAttribute(object min, object max, bool inclusive = false)
+ {
+ Min = min;
+ Max = max;
+ Inclusive = inclusive;
+ }
+
+ ///
+ /// Gets the maximum value.
+ ///
+ public object Max { get; }
+
+ ///
+ /// Gets the minimum value.
+ ///
+ public object Min { get; }
+
+ ///
+ /// Gets a value indicating whether check is inclusive.
+ ///
+ public bool Inclusive { get; }
+
+ ///
+ public bool Check(object other) =>
+ Convert.ChangeType(Min, other.GetType()) is IComparable min &&
+ Convert.ChangeType(Max, other.GetType()) is IComparable max &&
+ (Inclusive
+ ? min.CompareTo(other) <= 0 && max.CompareTo(other) >= 0
+ : min.CompareTo(other) < 0 && max.CompareTo(other) > 0);
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Interfaces/IValidator.cs b/EXILED/Exiled.API/Interfaces/IValidator.cs
new file mode 100644
index 0000000000..f9e2004ee1
--- /dev/null
+++ b/EXILED/Exiled.API/Interfaces/IValidator.cs
@@ -0,0 +1,22 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.API.Interfaces
+{
+ ///
+ /// Interface for all validations attributes.
+ ///
+ public interface IValidator
+ {
+ ///
+ /// Checks if is satisfying this attributes condition.
+ ///
+ /// Value to check.
+ /// Whether the value has passed check.
+ public bool Check(object other);
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Loader/Config.cs b/EXILED/Exiled.Loader/Config.cs
index 6e07979ef2..4e720d7bb7 100644
--- a/EXILED/Exiled.Loader/Config.cs
+++ b/EXILED/Exiled.Loader/Config.cs
@@ -88,5 +88,11 @@ public sealed class Config : IConfig
///
[Description("Indicates whether Exiled should auto-update itself as soon as a new release is available.")]
public bool EnableAutoUpdates { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether config validator should check all properties inside config values' types.
+ ///
+ [Description("Indicating whether config validator should check all properties inside config values' types.")]
+ public bool EnableDeepValidation { get; set; } = false;
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs
index 706924ee45..ac52381049 100644
--- a/EXILED/Exiled.Loader/ConfigManager.cs
+++ b/EXILED/Exiled.Loader/ConfigManager.cs
@@ -18,6 +18,7 @@ namespace Exiled.Loader
using API.Interfaces;
using Exiled.API.Features;
+ using Exiled.API.Features.Attributes;
using Exiled.API.Features.Pools;
using LabApi.Loader.Features.Plugins.Configuration;
@@ -72,6 +73,93 @@ public static SortedDictionary LoadSorted(string rawConfigs)
}
}
+ ///
+ /// Validates plugin config.
+ ///
+ /// Plugin which config is validated.
+ /// Validated config.
+ /// Config after validation is passed.
+ public static IConfig ValidateConfig(this IPlugin plugin, IConfig config)
+ {
+ int validated = 0;
+ foreach (PropertyInfo propertyInfo in config.GetType().GetProperties().Where(x => x.GetMethod != null && x.SetMethod != null))
+ {
+ try
+ {
+ ValidateType(config, plugin.Config, propertyInfo, ref validated);
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Failed to validate config: {ex}");
+ }
+ }
+
+ if (validated > 0)
+ Log.Info($"Plugin {plugin.Name} has successfully passed {validated} config validations!");
+
+ return config;
+ }
+
+ ///
+ /// Performs a validation for property and all its properties in 's type.
+ ///
+ /// Plugin which config is validated.
+ /// Validated config.
+ /// Property which will be validated.
+ /// Amount of successfully passed validations.
+ public static void ValidateType(object instance, object defaultInstance, PropertyInfo propertyInfo, ref int validated)
+ {
+ object value = propertyInfo.GetValue(instance, null);
+ object defaultValue = propertyInfo.GetValue(defaultInstance, null);
+
+ bool hasValidateChildrenAttribute = false;
+ try
+ {
+ foreach (Attribute attribute in propertyInfo.GetCustomAttributes())
+ {
+ hasValidateChildrenAttribute |= attribute is ValidateChildrenAttribute;
+ if (attribute is not IValidator validator)
+ continue;
+
+ try
+ {
+ if (!validator.Check(value))
+ {
+ Log.Error($"Value {value} in config ({propertyInfo.Name.ToSnakeCase()}) has failed validation for attribute {attribute.GetType().Name}. Default value ({defaultValue}) will be used instead.");
+ propertyInfo.SetValue(instance, defaultValue);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Value {value} in config ({propertyInfo.Name.ToSnakeCase()}) has failed validation for attribute {attribute.GetType().Name}. Default value ({defaultValue}) will be used instead.");
+ Log.Error($"Validation error message: {ex.Message}");
+ propertyInfo.SetValue(instance, defaultValue);
+ continue;
+ }
+
+ validated++;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Error while validating value of property '{propertyInfo.Name}': {ex.Message}. Default value ({defaultValue}) will be used instead.");
+ return;
+ }
+
+ if (hasValidateChildrenAttribute || (!LoaderPlugin.Config.EnableDeepValidation && !(propertyInfo.PropertyType.Namespace?.Contains("System") ?? false)))
+ {
+ foreach (PropertyInfo property in propertyInfo.PropertyType.GetProperties().Where(x => x.GetMethod != null && x.SetMethod != null))
+ {
+ ConstructorInfo ctor = property.PropertyType.GetConstructor(Type.EmptyTypes);
+ if (ctor is null)
+ continue;
+
+ ValidateType(value, ctor.Invoke(null, null), property, ref validated);
+ }
+ }
+ }
+
///
/// Loads the config of a plugin using the distribution.
///
@@ -106,7 +194,7 @@ public static IConfig LoadDefaultConfig(this IPlugin plugin, Dictionary
try
{
string rawConfigString = Loader.Serializer.Serialize(rawDeserializedConfig);
- config = (IConfig)Loader.Deserializer.Deserialize(rawConfigString, plugin.Config.GetType());
+ config = ValidateConfig(plugin, (IConfig)Loader.Deserializer.Deserialize(rawConfigString, plugin.Config.GetType()));
plugin.Config.CopyProperties(config);
}
catch (YamlException yamlException)
@@ -135,7 +223,7 @@ public static IConfig LoadSeparatedConfig(this IPlugin plugin)
try
{
- config = (IConfig)Loader.Deserializer.Deserialize(File.ReadAllText(plugin.ConfigPath), plugin.Config.GetType());
+ config = ValidateConfig(plugin, (IConfig)Loader.Deserializer.Deserialize(File.ReadAllText(plugin.ConfigPath), plugin.Config.GetType()));
plugin.Config.CopyProperties(config);
}
catch (YamlException yamlException)