From 81309656cb8a7441fac6a9142e5872808dbdbbc5 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 | 7 +++++++ .../InclusiveBetweenValidatorTests.cs | 7 +++++++ src/FluentValidation/Validators/RangeValidator.cs | 6 +++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs b/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs index a4ebfa4..6cb332f 100644 --- a/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs +++ b/src/FluentValidation.Tests/ExclusiveBetweenValidatorTests.cs @@ -127,6 +127,13 @@ 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 arithmetic difference (for example 'a'.CompareTo('c') == -2), + // so the range guard must treat any negative result as "less than", not only -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..3f1cc76 100644 --- a/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs +++ b/src/FluentValidation.Tests/InclusiveBetweenValidatorTests.cs @@ -122,6 +122,13 @@ 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 arithmetic difference (for example 'a'.CompareTo('c') == -2), + // so the range guard must treat any negative result as "less than", not only -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..28acc8f 100644 --- a/src/FluentValidation/Validators/RangeValidator.cs +++ b/src/FluentValidation/Validators/RangeValidator.cs @@ -34,7 +34,11 @@ public abstract class RangeValidator : PropertyValidator.Compare only guarantees the sign of the result, not a specific + // magnitude, so any negative value means "to" is less than "from". Comparing against + // -1 misses comparers that return other negatives (for example char.CompareTo, which + // returns the arithmetic difference, or custom comparers). + if (comparer.Compare(to, from) < 0) { throw new ArgumentOutOfRangeException(nameof(to), "To should be larger than from."); } }