Fix GetPreviousOccurrence returning a day in a disallowed month

When the time-of-day search rolls back past the 1st of the month, dayLimit
(and maxDay) can drop below the first valid day of month. The day-of-month
'*' fast path in TryGetPreviousDay returned actualDay = 0, and
DateTimeToTicks(year, month, 0, ...) underflowed into the last day of the
previous month, which is not re-validated against the month field. Guard
against maxDay below the first day of month so the caller advances to the
previous allowed month instead.
This commit is contained in:
dualfroz committed 2026-09-06 01:19:48 +02:00
1 parent 7d4c970307
commit 1a56b64326
2 files changed
+30

No files matched your search

+9
View File
@@ -995,6 +995,15 @@ namespace Cronos
var lastDayOfMonth = CalendarHelper.GetDaysInMonth(year, month);
var maxDay = dayLimit > lastDayOfMonth ? lastDayOfMonth : dayLimit;
// Rolling back past the 1st can drop maxDay below the first valid day,
// and the "*" fast path would then underflow into the previous month.
if (maxDay < CronField.DaysOfMonth.First)
{
day = default;
actualDay = default;
return false;
}
if (HasFlag(CronExpressionFlag.NearestWeekday))
{
day = HasFlag(CronExpressionFlag.DayOfMonthLast)
@@ -125,6 +125,27 @@ namespace Cronos.Tests
Assert.Equal(GetUtcDateTime(expectedString), previous);
}
[Theory]
// Rolling back past midnight of the 1st must skip to the previous allowed
// month, not land in the preceding disallowed one.
[InlineData("0 17 * 7 *", "2017-07-01 14:00:00", "2016-07-31 17:00:00")]
[InlineData("37-57/3 17-21 * 7 *", "2017-07-01 14:20:00", "2016-07-31 21:55:00")]
[InlineData("0 0 * 3 *", "2020-03-01 00:00:00", "2019-03-31 00:00:00")]
// Control: an unrestricted month field must still roll back normally.
[InlineData("0 17 * * *", "2017-07-01 14:00:00", "2017-06-30 17:00:00")]
public void GetPreviousOccurrence_DoesNotReturnDayInDisallowedMonth_WhenTimeRollsBackPastFirstOfMonth(
string cronExpression, string fromString, string expectedString)
{
var expression = CronExpression.Parse(cronExpression, CronFormat.Standard);
var from = GetUtcDateTime(fromString);
var previous = expression.GetPreviousOccurrence(from, inclusive: false);
Assert.Equal(GetUtcDateTime(expectedString), previous);
}
[Fact]
public void GetPreviousOccurrence_ReturnsCorrectDate_WhenMacroExpressionHasJitterSeed()
{