From 9718cad2311641049e5879f5e718f75ee9fc45d8 Mon Sep 17 00:00:00 2001 From: VALERA771 Date: Wed, 17 Sep 2025 21:41:31 +0300 Subject: [PATCH 1/4] feat: config validators --- .../Validators/AvailableValuesAttribute.cs | 37 ++++++++++++ .../Validators/CustomValidatorAttribute.cs | 37 ++++++++++++ .../Validators/GreaterOrEqualAttribute.cs | 40 +++++++++++++ .../Validators/GreaterThanAttribute.cs | 40 +++++++++++++ .../Validators/LessOrEqualAttribute.cs | 40 +++++++++++++ .../Validators/LessThanAttribute.cs | 40 +++++++++++++ .../Validators/NonNegativeAttribute.cs | 26 ++++++++ .../Validators/NonPositiveAttribute.cs | 26 ++++++++ .../Attributes/Validators/RangeAttribute.cs | 57 ++++++++++++++++++ EXILED/Exiled.API/Interfaces/IValidator.cs | 22 +++++++ EXILED/Exiled.Loader/Config.cs | 6 ++ EXILED/Exiled.Loader/ConfigManager.cs | 59 ++++++++++++++++++- 12 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/AvailableValuesAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs create mode 100644 EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs create mode 100644 EXILED/Exiled.API/Interfaces/IValidator.cs 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..b13fe203c8 --- /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 value) => Values.Contains(value); + } +} \ 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..76c33cab55 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/CustomValidatorAttribute.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; + + /// + /// Check a value with custom function. + /// + [AttributeUsage(AttributeTargets.Property)] + public class CustomValidatorAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + public CustomValidatorAttribute(Func customFunction) + { + CustomFunction = customFunction; + } + + /// + /// Gets the custom check function. + /// + public Func CustomFunction { get; } + + /// + public bool Check(object value) => CustomFunction(value); + } +} \ 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..5e905971ec --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterOrEqualAttribute.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// 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. + /// + /// + public GreaterOrEqualAttribute(IComparable value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public IComparable Value { get; } + + /// + public bool Check(object value) + { + return Value.CompareTo(value) >= 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..91e4ea4106 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/GreaterThanAttribute.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// 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. + /// + /// + public GreaterThanAttribute(IComparable value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public IComparable Value { get; } + + /// + public bool Check(object value) + { + return Value.CompareTo(value) >= 1; + } + } +} \ 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..cc11854e5a --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessOrEqualAttribute.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// 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. + /// + /// + public LessOrEqualAttribute(IComparable value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public IComparable Value { get; } + + /// + public bool Check(object value) + { + return Value.CompareTo(value) <= 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..36f13b2f15 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/LessThanAttribute.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// 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. + /// + /// + public LessThanAttribute(IComparable value) + { + Value = value; + } + + /// + /// Gets the minimum value. + /// + public IComparable Value { get; } + + /// + public bool Check(object value) + { + return Value.CompareTo(value) <= -1; + } + } +} \ 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..f0475c8e8e --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonNegativeAttribute.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + /// + /// Checks if value is 0 or greater. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NonNegativeAttribute : GreaterOrEqualAttribute + { + /// + /// Initializes a new instance of the class. + /// + public NonNegativeAttribute() + : base(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..1bed2a8987 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Validators/NonPositiveAttribute.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Validators +{ + using System; + + /// + /// Check if value is 0 or less. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NonPositiveAttribute : LessOrEqualAttribute + { + /// + /// Initializes a new instance of the class. + /// + public NonPositiveAttribute() + : base(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..1010620a67 --- /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 is inside a specific range. + /// + [AttributeUsage(AttributeTargets.Property)] + public class RangeAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public RangeAttribute(IComparable min, IComparable max, bool inclusive = false) + { + Min = min; + Max = max; + Inclusive = inclusive; + } + + /// + /// Gets the minimum value. + /// + public IComparable Max { get; } + + /// + /// Gets the maximum value. + /// + public IComparable Min { get; } + + /// + /// Gets a value indicating whether check is inclusive. + /// + public bool Inclusive { get; } + + /// + public bool Check(object value) + { + int minResult = Inclusive ? 0 : -1; + int maxResult = Inclusive ? 0 : 1; + + return Max.CompareTo(value) <= minResult && Min.CompareTo(value) >= maxResult; + } + } +} \ 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..502dfe730e --- /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 condition. + /// + /// Value to check. + /// Whether the value has passed check. + public bool Check(object value); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Loader/Config.cs b/EXILED/Exiled.Loader/Config.cs index 6e07979ef2..35ea91aa36 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; } = true; } } \ No newline at end of file diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs index e925517be9..2c66bbbf6e 100644 --- a/EXILED/Exiled.Loader/ConfigManager.cs +++ b/EXILED/Exiled.Loader/ConfigManager.cs @@ -5,6 +5,8 @@ // // ----------------------------------------------------------------------- +using System.Reflection; + namespace Exiled.Loader { using System; @@ -65,6 +67,59 @@ 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)) + plugin.ValidateType(config, propertyInfo, ref validated); + + Log.Info($"Config has successfully passed {validated} 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(this IPlugin plugin, IConfig config, PropertyInfo propertyInfo, ref int validated) + { + foreach (Attribute attribute in propertyInfo.GetCustomAttributes()) + { + if (attribute is not IValidator validator) + continue; + + object value = propertyInfo.GetValue(config, null); + object defaultValue = propertyInfo.GetValue(plugin.Config, null); + if (!validator.Check(value)) + { + Log.Error($"Value {value} ({propertyInfo.Name.ToSnakeCase()}) hasn't pass validation {attribute.GetType().Name}." + + $"Default value ({defaultValue}) will be used instead."); + propertyInfo.SetValue(config, defaultValue); + continue; + } + + validated++; + } + + if (!LoaderPlugin.Config.EnableDeepValidation) + return; + + if (!(propertyInfo.PropertyType.Namespace?.Contains("System") ?? false)) + { + foreach (PropertyInfo property in propertyInfo.PropertyType.GetProperties().Where(x => x.GetMethod != null && x.SetMethod != null)) + plugin.ValidateType(config, property, ref validated); + } + } + /// /// Loads the config of a plugin using the distribution. /// @@ -99,7 +154,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) @@ -128,7 +183,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) From 3728e8b3f20d3de68e15ea3bd46817b6726b4651 Mon Sep 17 00:00:00 2001 From: VALERA771 <72030575+VALERA771@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:01:48 +0300 Subject: [PATCH 2/4] Update EXILED/Exiled.Loader/ConfigManager.cs Co-authored-by: @Someone <45270312+Someone-193@users.noreply.github.com> --- EXILED/Exiled.Loader/ConfigManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs index 2c66bbbf6e..339c7e57a1 100644 --- a/EXILED/Exiled.Loader/ConfigManager.cs +++ b/EXILED/Exiled.Loader/ConfigManager.cs @@ -101,8 +101,7 @@ public static void ValidateType(this IPlugin plugin, IConfig config, Pr object defaultValue = propertyInfo.GetValue(plugin.Config, null); if (!validator.Check(value)) { - Log.Error($"Value {value} ({propertyInfo.Name.ToSnakeCase()}) hasn't pass validation {attribute.GetType().Name}." + - $"Default value ({defaultValue}) will be used instead."); + 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(config, defaultValue); continue; } From ae13e3996e5acef5af00add2402f79673cd7acce Mon Sep 17 00:00:00 2001 From: VALERA771 <72030575+VALERA771@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:01:55 +0300 Subject: [PATCH 3/4] Update EXILED/Exiled.API/Interfaces/IValidator.cs Co-authored-by: @Someone <45270312+Someone-193@users.noreply.github.com> --- EXILED/Exiled.API/Interfaces/IValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Interfaces/IValidator.cs b/EXILED/Exiled.API/Interfaces/IValidator.cs index 502dfe730e..c07a044341 100644 --- a/EXILED/Exiled.API/Interfaces/IValidator.cs +++ b/EXILED/Exiled.API/Interfaces/IValidator.cs @@ -13,7 +13,7 @@ namespace Exiled.API.Interfaces public interface IValidator { /// - /// Checks if is satisfying condition. + /// Checks if is satisfying this attributes condition. /// /// Value to check. /// Whether the value has passed check. From 47970133b6f40b96710f5107884978a99b26187e Mon Sep 17 00:00:00 2001 From: VALERA771 <72030575+VALERA771@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:02:00 +0300 Subject: [PATCH 4/4] Update EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs Co-authored-by: @Someone <45270312+Someone-193@users.noreply.github.com> --- .../Exiled.API/Features/Attributes/Validators/RangeAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs index 1010620a67..7c5287f4ff 100644 --- a/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs +++ b/EXILED/Exiled.API/Features/Attributes/Validators/RangeAttribute.cs @@ -12,7 +12,7 @@ namespace Exiled.API.Features.Attributes.Validators using Exiled.API.Interfaces; /// - /// Check if is inside a specific range. + /// Check if an is inside a specific range. /// [AttributeUsage(AttributeTargets.Property)] public class RangeAttribute : Attribute, IValidator