From e9c920f84656668dc3e3d67abffde043c4382567 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Wed, 2 Sep 2026 10:59:11 +0200 Subject: [PATCH] fix: reject inverted range when comparer returns negative other than -1 RangeValidator's constructor guarded against an inverted range with comparer.Compare(to, from) == -1. The IComparer/IComparable contract only guarantees the sign of the result, not the magnitude, so the guard silently failed for comparers returning other negatives, e.g. char.CompareTo ('a'.CompareTo('c') == -2) or custom comparers that return x - y. This let InclusiveBetween('c', 'a') build a validator that can never pass instead of throwing, unlike the int case which happens to return -1. Compare against < 0 to honor the comparer contract. Add char-bound throw tests to the inclusive and exclusive between suites. --- .../ExclusiveBetweenValidatorTests.cs | 6 ++++++ .../InclusiveBetweenValidatorTests.cs | 6 ++++++ src/FluentValidation/Validators/RangeValidator.cs | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs b/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs index a4ebfa4..75fd95d 100644 --- a/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs +++ b/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs @@ -127,6 +127,12 @@ public class ExclusiveBetweenValidatorTests { Assert.Throws(() => RangeValidatorFactory.CreateExclusiveBetween("ccc", "aaa")); } + [Fact] + public void When_the_to_is_smaller_than_the_from_then_the_validator_should_throw_for_chars() { + // char.CompareTo returns the difference ('a' vs 'c' is -2), not just -1. + Assert.Throws(() => RangeValidatorFactory.CreateExclusiveBetween('c', 'a')); + } + [Fact] public void When_the_validator_fails_the_error_message_should_be_set_for_strings() { var validator = new TestValidator(v => v.RuleFor(x => x.Surname).ExclusiveBetween("bbb", "zzz")); diff --git a/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs b/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs index 8341b18..c10a982 100644 --- a/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs +++ b/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs @@ -122,6 +122,12 @@ public class InclusiveBetweenValidatorTests { Assert.Throws(() => RangeValidatorFactory.CreateInclusiveBetween("ccc", "aaa")); } + [Fact] + public void When_the_to_is_smaller_than_the_from_then_the_validator_should_throw_for_chars() { + // char.CompareTo returns the difference ('a' vs 'c' is -2), not just -1. + Assert.Throws(() => RangeValidatorFactory.CreateInclusiveBetween('c', 'a')); + } + [Fact] public void When_the_validator_fails_the_error_message_should_be_set_for_strings() { var validator = new TestValidator(v => v.RuleFor(x => x.Surname).InclusiveBetween("bbb", "zzz")); diff --git a/src/FluentValidation/Validators/RangeValidator.cs b/src/FluentValidation/Validators/RangeValidator.cs index eafe717..65b00e4 100644 --- a/src/FluentValidation/Validators/RangeValidator.cs +++ b/src/FluentValidation/Validators/RangeValidator.cs @@ -34,7 +34,8 @@ public abstract class RangeValidator : PropertyValidator only guarantees the sign, not the magnitude. + if (comparer.Compare(to, from) < 0) { throw new ArgumentOutOfRangeException(nameof(to), "To should be larger than from."); } }