Skip to content

Commit fbbdafb

Browse files
committed
Fixed bug GH-21616: DateTime::modify() wrong across DST boundary
do_adjust_relative() adds h/i/s to wall-clock fields before converting to a Unix timestamp. Near a spring-forward gap that produces a nonexistent time, which resolves to the wrong offset. Fix: zero out relative h/i/s/us before timelib_update_ts(), apply them to sse directly. Same technique as timelib_add_wall(). y/m/d, weekday relatives, and first_last_day_of still go through timelib_update_ts. Also fixes GH-15880.
1 parent 11318af commit fbbdafb

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

ext/date/php_date.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,6 +3288,7 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{
32883288
php_date_obj *dateobj;
32893289
timelib_time *tmp_time;
32903290
timelib_error_container *err = NULL;
3291+
timelib_sll rel_h, rel_i, rel_s, rel_us;
32913292

32923293
dateobj = Z_PHPDATE_P(object);
32933294

@@ -3356,8 +3357,51 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{
33563357

33573358
timelib_time_dtor(tmp_time);
33583359

3360+
/* Strip relative h/i/s/us before timelib_update_ts() so that
3361+
* do_adjust_relative() does not add them to wall-clock fields.
3362+
* Wall-clock addition breaks at DST boundaries; SSE arithmetic
3363+
* (applied below) is always correct. */
3364+
rel_h = dateobj->time->relative.h;
3365+
rel_i = dateobj->time->relative.i;
3366+
rel_s = dateobj->time->relative.s;
3367+
rel_us = dateobj->time->relative.us;
3368+
dateobj->time->relative.h = 0;
3369+
dateobj->time->relative.i = 0;
3370+
dateobj->time->relative.s = 0;
3371+
dateobj->time->relative.us = 0;
3372+
33593373
timelib_update_ts(dateobj->time, NULL);
33603374
timelib_update_from_sse(dateobj->time);
3375+
3376+
/* Fold microsecond overflow into seconds so that the seconds
3377+
* component goes through SSE arithmetic, not wall-clock.
3378+
* Matches timelib_add_wall()'s do_range_limit() call
3379+
* (interval.c:317). */
3380+
if (rel_us >= 1000000 || rel_us <= -1000000) {
3381+
rel_s += rel_us / 1000000;
3382+
rel_us = rel_us % 1000000;
3383+
}
3384+
3385+
/* Apply h/i/s via SSE (Unix timestamp) arithmetic, matching
3386+
* the approach in timelib_add_wall() (interval.c:312). */
3387+
if (rel_h || rel_i || rel_s) {
3388+
dateobj->time->sse +=
3389+
timelib_hms_to_seconds(rel_h, rel_i, rel_s);
3390+
timelib_update_from_sse(dateobj->time);
3391+
}
3392+
3393+
/* Apply remaining sub-second microseconds. After the above
3394+
* normalization rel_us is in (-1000000, 1000000), so the
3395+
* cascade into seconds is at most +/-1s -- safe to go through
3396+
* wall-clock normalize + update_ts (have_relative is already
3397+
* cleared by the first timelib_update_ts call). */
3398+
if (rel_us) {
3399+
dateobj->time->us += rel_us;
3400+
timelib_do_normalize(dateobj->time);
3401+
timelib_update_ts(dateobj->time, NULL);
3402+
timelib_update_from_sse(dateobj->time);
3403+
}
3404+
33613405
dateobj->time->have_relative = 0;
33623406
memset(&dateobj->time->relative, 0, sizeof(dateobj->time->relative));
33633407

ext/date/tests/date_modify-1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ Sun, 22 Aug 1993 00:00:00 +12
2525
Sun, 27 Mar 2005 01:59:59 CET
2626
Sun, 27 Mar 2005 03:00:00 CEST
2727
Sun, 30 Oct 2005 01:59:59 CEST
28-
Sun, 30 Oct 2005 03:00:00 CET
28+
Sun, 30 Oct 2005 02:00:00 CET

0 commit comments

Comments
 (0)