Skip to content

Commit e425685

Browse files
authored
Merge pull request #499 from cakephp/fix-zero-interval-validation
Prevent infinite loops from zero-interval DatePeriods
2 parents 90fbb59 + 1f24dec commit e425685

File tree

4 files changed

+109
-3
lines changed

4 files changed

+109
-3
lines changed

src/ChronosDatePeriod.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
namespace Cake\Chronos;
1616

17+
use DateInterval;
1718
use DatePeriod;
19+
use InvalidArgumentException;
1820
use Iterator;
1921

2022
/**
@@ -32,15 +34,39 @@ class ChronosDatePeriod implements Iterator
3234
protected Iterator $iterator;
3335

3436
/**
35-
* @param \DatePeriod $period
37+
* @param \DatePeriod $period The DatePeriod to wrap.
38+
* @throws \InvalidArgumentException If the period has a zero interval which would cause an infinite loop.
3639
*/
3740
public function __construct(DatePeriod $period)
3841
{
42+
if (static::isZeroInterval($period->getDateInterval())) {
43+
throw new InvalidArgumentException(
44+
'Cannot create a period with a zero interval. This would cause an infinite loop when iterating.',
45+
);
46+
}
47+
3948
/** @var \Iterator<int, \DateTimeInterface> $iterator */
4049
$iterator = $period->getIterator();
4150
$this->iterator = $iterator;
4251
}
4352

53+
/**
54+
* Check if a DateInterval is effectively zero.
55+
*
56+
* @param \DateInterval $interval The interval to check.
57+
* @return bool True if the interval is zero.
58+
*/
59+
protected static function isZeroInterval(DateInterval $interval): bool
60+
{
61+
return $interval->y === 0
62+
&& $interval->m === 0
63+
&& $interval->d === 0
64+
&& $interval->h === 0
65+
&& $interval->i === 0
66+
&& $interval->s === 0
67+
&& (int)($interval->f * 1_000_000) === 0;
68+
}
69+
4470
/**
4571
* @return \Cake\Chronos\ChronosDate
4672
*/

src/ChronosPeriod.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
namespace Cake\Chronos;
1616

17+
use DateInterval;
1718
use DatePeriod;
19+
use InvalidArgumentException;
1820
use Iterator;
1921

2022
/**
@@ -32,15 +34,39 @@ class ChronosPeriod implements Iterator
3234
protected Iterator $iterator;
3335

3436
/**
35-
* @param \DatePeriod $period
37+
* @param \DatePeriod $period The DatePeriod to wrap.
38+
* @throws \InvalidArgumentException If the period has a zero interval which would cause an infinite loop.
3639
*/
3740
public function __construct(DatePeriod $period)
3841
{
42+
if (static::isZeroInterval($period->getDateInterval())) {
43+
throw new InvalidArgumentException(
44+
'Cannot create a period with a zero interval. This would cause an infinite loop when iterating.',
45+
);
46+
}
47+
3948
/** @var \Iterator<int, \DateTimeInterface> $iterator */
4049
$iterator = $period->getIterator();
4150
$this->iterator = $iterator;
4251
}
4352

53+
/**
54+
* Check if a DateInterval is effectively zero.
55+
*
56+
* @param \DateInterval $interval The interval to check.
57+
* @return bool True if the interval is zero.
58+
*/
59+
protected static function isZeroInterval(DateInterval $interval): bool
60+
{
61+
return $interval->y === 0
62+
&& $interval->m === 0
63+
&& $interval->d === 0
64+
&& $interval->h === 0
65+
&& $interval->i === 0
66+
&& $interval->s === 0
67+
&& (int)($interval->f * 1_000_000) === 0;
68+
}
69+
4470
/**
4571
* @return \Cake\Chronos\Chronos
4672
*/

tests/TestCase/ChronosDatePeriodTest.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use DateInterval;
2020
use DatePeriod;
2121
use DateTime;
22+
use InvalidArgumentException;
2223

2324
class ChronosDatePeriodTest extends TestCase
2425
{
@@ -30,11 +31,37 @@ public function testChronosPeriod(): void
3031
$output[$key] = $value;
3132
}
3233
$this->assertCount(4, $output);
33-
$this->assertInstanceOf(ChronosDAte::class, $output[0]);
34+
$this->assertInstanceOf(ChronosDate::class, $output[0]);
3435
$this->assertSame('2025-01-01 00:00:00', $output[0]->format('Y-m-d H:i:s'));
3536
$this->assertInstanceOf(ChronosDate::class, $output[1]);
3637
$this->assertSame('2025-01-02 00:00:00', $output[1]->format('Y-m-d H:i:s'));
3738
$this->assertInstanceOf(ChronosDate::class, $output[3]);
3839
$this->assertSame('2025-01-04 00:00:00', $output[3]->format('Y-m-d H:i:s'));
3940
}
41+
42+
public function testZeroIntervalThrowsException(): void
43+
{
44+
$this->expectException(InvalidArgumentException::class);
45+
$this->expectExceptionMessage('Cannot create a period with a zero interval');
46+
47+
$period = new DatePeriod(
48+
new DateTime('2025-01-01'),
49+
new DateInterval('PT0S'),
50+
new DateTime('2025-01-02'),
51+
);
52+
new ChronosDatePeriod($period);
53+
}
54+
55+
public function testZeroIntervalAllZeroComponents(): void
56+
{
57+
$this->expectException(InvalidArgumentException::class);
58+
59+
$interval = new DateInterval('P0D');
60+
$period = new DatePeriod(
61+
new DateTime('2025-01-01'),
62+
$interval,
63+
new DateTime('2025-01-02'),
64+
);
65+
new ChronosDatePeriod($period);
66+
}
4067
}

tests/TestCase/ChronosPeriodTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use DateInterval;
2020
use DatePeriod;
2121
use DateTime;
22+
use InvalidArgumentException;
2223

2324
class ChronosPeriodTest extends TestCase
2425
{
@@ -35,4 +36,30 @@ public function testChronosPeriod(): void
3536
$this->assertInstanceOf(Chronos::class, $output[1]);
3637
$this->assertSame('2025-01-01 01:00:00', $output[1]->format('Y-m-d H:i:s'));
3738
}
39+
40+
public function testZeroIntervalThrowsException(): void
41+
{
42+
$this->expectException(InvalidArgumentException::class);
43+
$this->expectExceptionMessage('Cannot create a period with a zero interval');
44+
45+
$period = new DatePeriod(
46+
new DateTime('2025-01-01'),
47+
new DateInterval('PT0S'),
48+
new DateTime('2025-01-02'),
49+
);
50+
new ChronosPeriod($period);
51+
}
52+
53+
public function testZeroIntervalAllZeroComponents(): void
54+
{
55+
$this->expectException(InvalidArgumentException::class);
56+
57+
$interval = new DateInterval('P0D');
58+
$period = new DatePeriod(
59+
new DateTime('2025-01-01'),
60+
$interval,
61+
new DateTime('2025-01-02'),
62+
);
63+
new ChronosPeriod($period);
64+
}
3865
}

0 commit comments

Comments
 (0)