Coverage Report

Created: 2026-10-28 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/date/time_duration.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright © The PHP Group and Contributors.                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to the Modified BSD License that is      |
6
   | bundled with this package in the file LICENSE, and is available      |
7
   | through the World Wide Web at <https://www.php.net/license/>.        |
8
   |                                                                      |
9
   | SPDX-License-Identifier: BSD-3-Clause                                |
10
   +----------------------------------------------------------------------+
11
   | Authors: Derick Rethans <derick@derickrethans.nl>                    |
12
   |          Tim Düsterhus <timwolla@php.net>                            |
13
   +----------------------------------------------------------------------+
14
 */
15
16
#include "php.h"
17
#include "Zend/zend_exceptions.h"
18
19
#include "php_date.h"
20
#include "php_time.h"
21
#include "time_duration.h"
22
23
0
#define NANOS_IN_SEC 1000000000
24
0
#define NANOS_IN_MICRO 1000
25
0
#define MICROS_IN_SEC 1000000
26
0
#define NANOS_IN_MILLI 1000000
27
0
#define MILLIS_IN_SEC 1000
28
29
static_assert(NANOS_IN_MICRO * MICROS_IN_SEC == NANOS_IN_SEC, "");
30
static_assert(NANOS_IN_MILLI * MILLIS_IN_SEC == NANOS_IN_SEC, "");
31
32
0
#define Z_PARAM_ULONG(l) { \
33
0
    zend_long __##l; \
34
0
    Z_PARAM_LONG(__##l); \
35
0
    if (__##l < 0) { \
36
0
      zend_argument_value_error(_i, "must be greater than or equal to 0"); \
37
0
      _error_code = ZPP_ERROR_FAILURE; \
38
0
      break; \
39
0
    } \
40
0
    l = __##l; \
41
0
  }
42
43
ZEND_COLD static void throw_out_of_range_exception(void)
44
0
{
45
0
#if SIZEOF_ZEND_LONG != 4
46
0
  zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 9_223_372_035 seconds (roughly 292 years)", 0);
47
#else
48
  zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 2_147_483_647 seconds (roughly 68 years)", 0);
49
#endif
50
0
}
51
52
ZEND_COLD static void throw_timelib_error(int error)
53
0
{
54
0
  switch (error) {
55
0
    case TIMELIB_ERROR_SECONDS_OUT_OF_RANGE:
56
0
      throw_out_of_range_exception();
57
0
      break;
58
0
    case TIMELIB_ERROR_DIVISION_BY_ZERO:
59
0
      zend_throw_exception_ex(zend_ce_division_by_zero_error, 0, "Division by zero");
60
0
      break;
61
0
    case TIMELIB_ERROR_ISO8601_DURATION_PARSE_FAILURE:
62
0
    case TIMELIB_ERROR_DURATION_MISSING_PERIOD:
63
0
    case TIMELIB_ERROR_DURATION_ONLY_PERIOD_ALLOWED:
64
0
    case TIMELIB_ERROR_DURATION_DAYS_FOUND:
65
0
      zend_throw_exception(php_date_ce_time_timeexception, timelib_get_error_message(error), 0);
66
0
      break;
67
0
    default:
68
      /* This should be unreachable in practice. */
69
0
      zend_throw_exception_ex(php_date_ce_time_timeexception, 0, "Failed to create a Time\\Duration: %s", timelib_get_error_message(error));
70
0
      break;
71
0
  }
72
0
}
73
74
static inline php_date_time_duration *create_duration_shell(zval *target)
75
0
{
76
0
  object_init_ex(target, php_date_ce_time_duration);
77
78
0
  return Z_DATE_TIME_DURATION_P(target);
79
0
}
80
81
static inline bool duration_representable(const timelib_duration *duration)
82
0
{
83
0
  return
84
    /* Check if the duration would overflow the $seconds property. */
85
0
    duration->seconds <= ((uint64_t)ZEND_LONG_MAX)
86
    /* This constraint is an explicit part of PHP's API: It is the maximum $seconds
87
     * value that allows storing the entire duration as a single int64_t counting
88
     * nanoseconds, which might be desirable in the future when userland `int` is
89
     * consistently 64 bits.
90
     *
91
     * While it is currently also enforced by timelib, this might change
92
     * in a future version of timelib, thus we also enforce it manually. */
93
0
    && duration->seconds <= UINT64_C(9223372035);
94
0
}
95
96
ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
97
0
{
98
0
  if (!duration_representable(&object->duration)) {
99
0
    throw_out_of_range_exception();
100
0
    return FAILURE;
101
0
  }
102
103
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 0)));
104
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 1)));
105
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 2)));
106
107
0
  ZVAL_LONG(OBJ_PROP_NUM(&object->std, 0), object->duration.seconds);
108
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 0)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
109
0
  ZVAL_LONG(OBJ_PROP_NUM(&object->std, 1), object->duration.nanoseconds);
110
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 1)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
111
0
  ZVAL_BOOL(OBJ_PROP_NUM(&object->std, 2), object->duration.negative);
112
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 2)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
113
114
0
  return SUCCESS;
115
0
}
116
117
ZEND_ATTRIBUTE_NODISCARD PHPAPI zend_result php_date_time_duration_create(zval *target, zend_ulong seconds, zend_ulong nanoseconds)
118
0
{
119
0
  ZEND_ASSERT(nanoseconds < NANOS_IN_SEC);
120
121
0
  if (EXPECTED(DATEG(duration_cache))) {
122
0
    php_date_time_duration *cached = php_date_time_duration_from_obj(DATEG(duration_cache));
123
0
    ZEND_ASSERT(!cached->duration.negative);
124
125
0
    if (cached->duration.seconds == seconds && cached->duration.nanoseconds == nanoseconds) {
126
0
      ZVAL_OBJ_COPY(target, &cached->std);
127
0
      return SUCCESS;
128
0
    }
129
0
  }
130
131
0
  php_date_time_duration *obj = create_duration_shell(target);
132
133
0
  int error = timelib_duration_ctor_static(&obj->duration, seconds, nanoseconds, /* negative */ false);
134
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
135
0
    throw_timelib_error(error);
136
0
    return FAILURE;
137
0
  }
138
139
0
  if (sync_properties(obj) == FAILURE) {
140
0
    return FAILURE;
141
0
  }
142
143
0
  if (DATEG(duration_cache)) {
144
0
    zend_object_release(DATEG(duration_cache));
145
0
  }
146
0
  DATEG(duration_cache) = zend_object_copy(&obj->std);
147
148
0
  return SUCCESS;
149
0
}
150
151
PHP_METHOD(Time_Duration, __construct)
152
0
{
153
0
  zend_throw_error(NULL, "Cannot directly construct Time\\Duration, use Time\\Duration::from*() methods instead");
154
0
}
155
156
PHP_METHOD(Time_Duration, __unserialize)
157
15
{
158
15
  php_date_time_duration *duration = Z_DATE_TIME_DURATION_P(ZEND_THIS);
159
160
15
  HashTable *data;
161
162
45
  ZEND_PARSE_PARAMETERS_START(1, 1)
163
60
    Z_PARAM_ARRAY_HT(data);
164
15
  ZEND_PARSE_PARAMETERS_END();
165
166
15
  object_properties_load(&duration->std, data);
167
15
  if (EG(exception)) {
168
11
    goto fail;
169
11
  }
170
171
4
  zval *seconds = OBJ_PROP_NUM(&duration->std, 0);
172
4
  zval *nanoseconds = OBJ_PROP_NUM(&duration->std, 1);
173
4
  zval *negative = OBJ_PROP_NUM(&duration->std, 2);
174
4
  if (Z_ISUNDEF_P(seconds) || Z_ISUNDEF_P(nanoseconds) || Z_ISUNDEF_P(negative)) {
175
4
    goto fail;
176
4
  }
177
178
  /* Verify that both properties are non-negative, since the timelib_duration_ctor_static() takes
179
   * unsigned. */
180
0
  if (Z_LVAL_P(seconds) < 0 || Z_LVAL_P(nanoseconds) < 0) {
181
0
    goto fail;
182
0
  }
183
184
  /* timelib_duration_ctor_static() takes a uint32_t for nanoseconds, so larger values would
185
   * implicitly wrap around instead of being rejected. */
186
0
  if (Z_LVAL_P(nanoseconds) >= NANOS_IN_SEC) {
187
0
    goto fail;
188
0
  }
189
190
0
  int error = timelib_duration_ctor_static(&duration->duration, Z_LVAL_P(seconds), Z_LVAL_P(nanoseconds), Z_TYPE_P(negative) == IS_TRUE);
191
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
192
0
    throw_timelib_error(error);
193
0
    goto fail;
194
0
  }
195
196
0
  if (!duration_representable(&duration->duration)) {
197
0
    throw_out_of_range_exception();
198
0
    goto fail;
199
0
  }
200
201
0
  if ((Z_TYPE_P(negative) == IS_TRUE) != duration->duration.negative) {
202
0
    goto fail;
203
0
  }
204
205
0
  return;
206
207
15
 fail:
208
209
  /* If an exception is already active (e.g. for unrepresentable durations) it will be wrapped for
210
   * uniform exceptions thrown from unserialization handlers, but to still provide additional
211
   * context for a human reader. */
212
15
  zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(duration->std.ce->name));
213
15
  RETURN_THROWS();
214
15
}
215
216
PHP_METHOD(Time_Duration, fromSeconds)
217
0
{
218
0
  zend_ulong seconds;
219
0
  zend_ulong nanoseconds = 0;
220
221
0
  ZEND_PARSE_PARAMETERS_START(1, 2)
222
0
    Z_PARAM_ULONG(seconds);
223
0
    Z_PARAM_OPTIONAL;
224
0
    Z_PARAM_ULONG(nanoseconds);
225
0
  ZEND_PARSE_PARAMETERS_END();
226
227
0
  if (nanoseconds >= NANOS_IN_SEC) {
228
0
    zend_argument_value_error(2, "must be less than 1_000_000_000");
229
0
    RETURN_THROWS();
230
0
  }
231
232
0
  if (php_date_time_duration_create(return_value, seconds, nanoseconds) == FAILURE) {
233
0
    RETURN_THROWS();
234
0
  }
235
0
}
236
237
PHP_METHOD(Time_Duration, fromNanoseconds)
238
0
{
239
0
  zend_ulong nanoseconds;
240
241
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
242
0
    Z_PARAM_ULONG(nanoseconds);
243
0
  ZEND_PARSE_PARAMETERS_END();
244
245
0
  zend_ulong seconds = nanoseconds / NANOS_IN_SEC;
246
0
  nanoseconds %= NANOS_IN_SEC;
247
248
0
  if (php_date_time_duration_create(return_value, seconds, nanoseconds) == FAILURE) {
249
0
    RETURN_THROWS();
250
0
  }
251
0
}
252
253
PHP_METHOD(Time_Duration, fromMicroseconds)
254
0
{
255
0
  zend_ulong microseconds;
256
257
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
258
0
    Z_PARAM_ULONG(microseconds);
259
0
  ZEND_PARSE_PARAMETERS_END();
260
261
0
  zend_ulong seconds = microseconds / MICROS_IN_SEC;
262
0
  zend_ulong nanoseconds = (microseconds % MICROS_IN_SEC) * NANOS_IN_MICRO;
263
264
0
  if (php_date_time_duration_create(return_value, seconds, nanoseconds) == FAILURE) {
265
0
    RETURN_THROWS();
266
0
  }
267
0
}
268
269
PHP_METHOD(Time_Duration, fromMilliseconds)
270
0
{
271
0
  zend_ulong milliseconds;
272
273
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
274
0
    Z_PARAM_ULONG(milliseconds);
275
0
  ZEND_PARSE_PARAMETERS_END();
276
277
0
  zend_ulong seconds = milliseconds / MILLIS_IN_SEC;
278
0
  zend_ulong nanoseconds = (milliseconds % MILLIS_IN_SEC) * NANOS_IN_MILLI;
279
280
0
  if (php_date_time_duration_create(return_value, seconds, nanoseconds) == FAILURE) {
281
0
    RETURN_THROWS();
282
0
  }
283
0
}
284
285
PHP_METHOD(Time_Duration, fromMinutes)
286
0
{
287
0
  zend_ulong minutes;
288
289
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
290
0
    Z_PARAM_ULONG(minutes);
291
0
  ZEND_PARSE_PARAMETERS_END();
292
293
0
  if (minutes > (ZEND_ULONG_MAX / 60)) {
294
0
    throw_out_of_range_exception();
295
0
    RETURN_THROWS();
296
0
  }
297
298
0
  if (php_date_time_duration_create(return_value, minutes * 60, /* nanoseconds */ 0) == FAILURE) {
299
0
    RETURN_THROWS();
300
0
  }
301
0
}
302
303
PHP_METHOD(Time_Duration, fromHours)
304
0
{
305
0
  zend_ulong hours;
306
307
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
308
0
    Z_PARAM_ULONG(hours);
309
0
  ZEND_PARSE_PARAMETERS_END();
310
311
0
  if (hours > (ZEND_ULONG_MAX / 3600)) {
312
0
    throw_out_of_range_exception();
313
0
    RETURN_THROWS();
314
0
  }
315
316
0
  if (php_date_time_duration_create(return_value, hours * 3600, /* nanoseconds */ 0) == FAILURE) {
317
0
    RETURN_THROWS();
318
0
  }
319
0
}
320
321
PHP_METHOD(Time_Duration, fromIso8601DurationString)
322
0
{
323
0
  zend_string *specification;
324
325
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
326
0
    Z_PARAM_STR(specification);
327
0
  ZEND_PARSE_PARAMETERS_END();
328
329
0
  if (zend_str_has_nul_byte(specification)) {
330
0
    throw_timelib_error(TIMELIB_ERROR_ISO8601_DURATION_PARSE_FAILURE);
331
0
    RETURN_THROWS();
332
0
  }
333
334
0
  int error;
335
0
  timelib_duration *d = timelib_duration_create_from_iso8601string(ZSTR_VAL(specification), &error);
336
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
337
0
    throw_timelib_error(error);
338
0
    RETURN_THROWS();
339
0
  }
340
341
0
  php_date_time_duration *new = create_duration_shell(return_value);
342
0
  new->duration = *d;
343
0
  timelib_duration_dtor(d);
344
345
0
  if (sync_properties(new) == FAILURE) {
346
0
    RETURN_THROWS();
347
0
  }
348
0
}
349
350
PHP_METHOD(Time_Duration, negate)
351
0
{
352
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
353
354
0
  ZEND_PARSE_PARAMETERS_NONE();
355
356
0
  php_date_time_duration *new = create_duration_shell(return_value);
357
358
0
  int error = timelib_duration_negate_static(&new->duration, &original->duration);
359
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
360
0
    throw_timelib_error(error);
361
0
    RETURN_THROWS();
362
0
  }
363
364
0
  if (sync_properties(new) == FAILURE) {
365
0
    RETURN_THROWS();
366
0
  }
367
0
}
368
369
PHP_METHOD(Time_Duration, absolute)
370
0
{
371
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
372
373
0
  ZEND_PARSE_PARAMETERS_NONE();
374
375
0
  if (!original->duration.negative) {
376
0
    RETURN_COPY(ZEND_THIS);
377
0
  }
378
379
0
  if (php_date_time_duration_create(return_value, original->duration.seconds, original->duration.nanoseconds) == FAILURE) {
380
0
    RETURN_THROWS();
381
0
  }
382
0
}
383
384
PHP_METHOD(Time_Duration, add)
385
0
{
386
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
387
388
0
  const php_date_time_duration *additional;
389
390
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
391
0
    Z_PARAM_DATE_TIME_DURATION(additional);
392
0
  ZEND_PARSE_PARAMETERS_END();
393
394
0
  php_date_time_duration *new = create_duration_shell(return_value);
395
396
0
  int error = timelib_duration_add_static(&new->duration, &original->duration, &additional->duration);
397
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
398
0
    throw_timelib_error(error);
399
0
    RETURN_THROWS();
400
0
  }
401
402
0
  if (sync_properties(new) == FAILURE) {
403
0
    RETURN_THROWS();
404
0
  }
405
0
}
406
407
PHP_METHOD(Time_Duration, sub)
408
0
{
409
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
410
411
0
  const php_date_time_duration *minus;
412
413
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
414
0
    Z_PARAM_DATE_TIME_DURATION(minus);
415
0
  ZEND_PARSE_PARAMETERS_END();
416
417
0
  php_date_time_duration *new = create_duration_shell(return_value);
418
419
0
  int error = timelib_duration_sub_static(&new->duration, &original->duration, &minus->duration);
420
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
421
0
    throw_timelib_error(error);
422
0
    RETURN_THROWS();
423
0
  }
424
425
0
  if (sync_properties(new) == FAILURE) {
426
0
    RETURN_THROWS();
427
0
  }
428
0
}
429
430
PHP_METHOD(Time_Duration, multiplyBy)
431
0
{
432
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
433
434
0
  zend_ulong factor;
435
436
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
437
0
    Z_PARAM_ULONG(factor);
438
0
  ZEND_PARSE_PARAMETERS_END();
439
440
0
  php_date_time_duration *new = create_duration_shell(return_value);
441
442
0
  int error = timelib_duration_mul_static(&new->duration, &original->duration, factor);
443
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
444
0
    throw_timelib_error(error);
445
0
    RETURN_THROWS();
446
0
  }
447
448
0
  if (sync_properties(new) == FAILURE) {
449
0
    RETURN_THROWS();
450
0
  }
451
0
}
452
453
PHP_METHOD(Time_Duration, divideBy)
454
0
{
455
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
456
457
0
  zend_ulong divisor;
458
459
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
460
0
    Z_PARAM_ULONG(divisor);
461
0
  ZEND_PARSE_PARAMETERS_END();
462
463
0
  php_date_time_duration *new = create_duration_shell(return_value);
464
465
0
  int error = timelib_duration_div_static(&new->duration, &original->duration, divisor);
466
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
467
0
    throw_timelib_error(error);
468
0
    RETURN_THROWS();
469
0
  }
470
471
0
  if (sync_properties(new) == FAILURE) {
472
0
    RETURN_THROWS();
473
0
  }
474
0
}
475
476
PHP_METHOD(Time_Duration, compare)
477
0
{
478
0
  const php_date_time_duration *a;
479
0
  const php_date_time_duration *b;
480
481
0
  ZEND_PARSE_PARAMETERS_START(2, 2)
482
0
    Z_PARAM_DATE_TIME_DURATION(a);
483
0
    Z_PARAM_DATE_TIME_DURATION(b);
484
0
  ZEND_PARSE_PARAMETERS_END();
485
486
0
  RETURN_LONG(timelib_duration_compare(&a->duration, &b->duration));
487
0
}