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..7c5287f4ff --- /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. + /// + /// + /// + /// + 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..c07a044341 --- /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 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..339c7e57a1 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,58 @@ 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} 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; + } + + 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 +153,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 +182,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)