Coverage Report

Created: 2026-09-14 06:25

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
22
0
#define NANOS_IN_SEC 1000000000
23
0
#define NANOS_IN_MICRO 1000
24
0
#define MICROS_IN_SEC 1000000
25
0
#define NANOS_IN_MILLI 1000000
26
0
#define MILLIS_IN_SEC 1000
27
28
ZEND_STATIC_ASSERT(NANOS_IN_MICRO * MICROS_IN_SEC == NANOS_IN_SEC, "");
29
ZEND_STATIC_ASSERT(NANOS_IN_MILLI * MILLIS_IN_SEC == NANOS_IN_SEC, "");
30
31
0
#define Z_PARAM_ULONG(l) { \
32
0
    zend_long __##l; \
33
0
    Z_PARAM_LONG(__##l); \
34
0
    if (__##l < 0) { \
35
0
      zend_argument_value_error(_i, "must be greater than or equal to 0"); \
36
0
      _error_code = ZPP_ERROR_FAILURE; \
37
0
      break; \
38
0
    } \
39
0
    l = __##l; \
40
0
  }
41
42
ZEND_COLD static void throw_out_of_range_exception(void)
43
0
{
44
0
#if SIZEOF_ZEND_LONG != 4
45
0
  zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 9_223_372_035 seconds (roughly 292 years)", 0);
46
#else
47
  zend_throw_exception(php_date_ce_time_timeexception, "The maximum representable range is 2_147_483_647 seconds (roughly 68 years)", 0);
48
#endif
49
0
}
50
51
ZEND_COLD static void throw_timelib_error(int error)
52
0
{
53
0
  switch (error) {
54
0
    case TIMELIB_ERROR_SECONDS_OUT_OF_RANGE:
55
0
      throw_out_of_range_exception();
56
0
      break;
57
0
    case TIMELIB_ERROR_DIVISION_BY_ZERO:
58
0
      zend_throw_exception_ex(zend_ce_division_by_zero_error, 0, "Division by zero");
59
0
      break;
60
0
    case TIMELIB_ERROR_ISO8601_DURATION_PARSE_FAILURE:
61
0
    case TIMELIB_ERROR_DURATION_MISSING_PERIOD:
62
0
    case TIMELIB_ERROR_DURATION_ONLY_PERIOD_ALLOWED:
63
0
    case TIMELIB_ERROR_DURATION_DAYS_FOUND:
64
0
      zend_throw_exception(php_date_ce_time_timeexception, timelib_get_error_message(error), 0);
65
0
      break;
66
0
    default:
67
      /* This should be unreachable in practice. */
68
0
      zend_throw_exception_ex(php_date_ce_time_timeexception, 0, "Failed to create a Time\\Duration: %s", timelib_get_error_message(error));
69
0
      break;
70
0
  }
71
0
}
72
73
static inline php_date_time_duration *create_duration_shell(zval *target)
74
0
{
75
0
  object_init_ex(target, php_date_ce_time_duration);
76
77
0
  return Z_DATE_TIME_DURATION_P(target);
78
0
}
79
80
ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
81
0
{
82
0
  if (
83
    /* Check if the duration would overflow the $seconds property. */
84
0
    object->duration.seconds > ((uint64_t)ZEND_LONG_MAX)
85
    /* This constraint is an explicit part of PHP's API: It is the maximum $seconds
86
     * value that allows storing the entire duration as a single int64_t counting
87
     * nanoseconds, which might be desirable in the future when userland `int` is
88
     * consistently 64 bits.
89
     *
90
     * While it is currently also enforced by timelib, this might change
91
     * in a future version of timelib, thus we also enforce it manually. */
92
0
    || object->duration.seconds > UINT64_C(9223372035)
93
0
  ) {
94
0
    throw_out_of_range_exception();
95
0
    return FAILURE;
96
0
  }
97
98
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 0)));
99
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 1)));
100
0
  ZEND_ASSERT(Z_ISUNDEF_P(OBJ_PROP_NUM(&object->std, 2)));
101
102
0
  ZVAL_LONG(OBJ_PROP_NUM(&object->std, 0), object->duration.seconds);
103
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 0)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
104
0
  ZVAL_LONG(OBJ_PROP_NUM(&object->std, 1), object->duration.nanoseconds);
105
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 1)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
106
0
  ZVAL_BOOL(OBJ_PROP_NUM(&object->std, 2), object->duration.negative);
107
0
  Z_PROP_FLAG_P(OBJ_PROP_NUM(&object->std, 2)) &= ~(IS_PROP_UNINIT|IS_PROP_REINITABLE);
108
109
0
  return SUCCESS;
110
0
}
111
112
ZEND_ATTRIBUTE_NODISCARD static zend_result create_duration(zval *target, zend_ulong seconds, zend_ulong nanoseconds)
113
0
{
114
0
  ZEND_ASSERT(nanoseconds < NANOS_IN_SEC);
115
116
0
  if (EXPECTED(DATEG(duration_cache))) {
117
0
    php_date_time_duration *cached = php_date_time_duration_from_obj(DATEG(duration_cache));
118
0
    ZEND_ASSERT(!cached->duration.negative);
119
120
0
    if (cached->duration.seconds == seconds && cached->duration.nanoseconds == nanoseconds) {
121
0
      ZVAL_OBJ_COPY(target, &cached->std);
122
0
      return SUCCESS;
123
0
    }
124
0
  }
125
126
0
  php_date_time_duration *obj = create_duration_shell(target);
127
128
0
  int error = timelib_duration_ctor_static(&obj->duration, seconds, nanoseconds, /* negative */ false);
129
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
130
0
    throw_timelib_error(error);
131
0
    return FAILURE;
132
0
  }
133
134
0
  if (sync_properties(obj) == FAILURE) {
135
0
    return FAILURE;
136
0
  }
137
138
0
  if (DATEG(duration_cache)) {
139
0
    zend_object_release(DATEG(duration_cache));
140
0
  }
141
0
  GC_ADDREF(&obj->std);
142
0
  DATEG(duration_cache) = &obj->std;
143
144
0
  return SUCCESS;
145
0
}
146
147
PHP_METHOD(Time_Duration, __construct)
148
0
{
149
0
  zend_throw_error(NULL, "Cannot directly construct Time\\Duration, use Time\\Duration::from*() methods instead");
150
0
}
151
152
PHP_METHOD(Time_Duration, fromSeconds)
153
0
{
154
0
  zend_ulong seconds;
155
0
  zend_ulong nanoseconds = 0;
156
157
0
  ZEND_PARSE_PARAMETERS_START(1, 2)
158
0
    Z_PARAM_ULONG(seconds);
159
0
    Z_PARAM_OPTIONAL;
160
0
    Z_PARAM_ULONG(nanoseconds);
161
0
  ZEND_PARSE_PARAMETERS_END();
162
163
0
  if (nanoseconds >= NANOS_IN_SEC) {
164
0
    zend_argument_value_error(2, "must be less than 1_000_000_000");
165
0
    RETURN_THROWS();
166
0
  }
167
168
0
  if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
169
0
    RETURN_THROWS();
170
0
  }
171
0
}
172
173
PHP_METHOD(Time_Duration, fromNanoseconds)
174
0
{
175
0
  zend_ulong nanoseconds;
176
177
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
178
0
    Z_PARAM_ULONG(nanoseconds);
179
0
  ZEND_PARSE_PARAMETERS_END();
180
181
0
  zend_ulong seconds = nanoseconds / NANOS_IN_SEC;
182
0
  nanoseconds %= NANOS_IN_SEC;
183
184
0
  if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
185
0
    RETURN_THROWS();
186
0
  }
187
0
}
188
189
PHP_METHOD(Time_Duration, fromMicroseconds)
190
0
{
191
0
  zend_ulong microseconds;
192
193
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
194
0
    Z_PARAM_ULONG(microseconds);
195
0
  ZEND_PARSE_PARAMETERS_END();
196
197
0
  zend_ulong seconds = microseconds / MICROS_IN_SEC;
198
0
  zend_ulong nanoseconds = (microseconds % MICROS_IN_SEC) * NANOS_IN_MICRO;
199
200
0
  if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
201
0
    RETURN_THROWS();
202
0
  }
203
0
}
204
205
PHP_METHOD(Time_Duration, fromMilliseconds)
206
0
{
207
0
  zend_ulong milliseconds;
208
209
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
210
0
    Z_PARAM_ULONG(milliseconds);
211
0
  ZEND_PARSE_PARAMETERS_END();
212
213
0
  zend_ulong seconds = milliseconds / MILLIS_IN_SEC;
214
0
  zend_ulong nanoseconds = (milliseconds % MILLIS_IN_SEC) * NANOS_IN_MILLI;
215
216
0
  if (create_duration(return_value, seconds, nanoseconds) == FAILURE) {
217
0
    RETURN_THROWS();
218
0
  }
219
0
}
220
221
PHP_METHOD(Time_Duration, fromMinutes)
222
0
{
223
0
  zend_ulong minutes;
224
225
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
226
0
    Z_PARAM_ULONG(minutes);
227
0
  ZEND_PARSE_PARAMETERS_END();
228
229
0
  if (minutes > (ZEND_ULONG_MAX / 60)) {
230
0
    throw_out_of_range_exception();
231
0
    RETURN_THROWS();
232
0
  }
233
234
0
  if (create_duration(return_value, minutes * 60, /* nanoseconds */ 0) == FAILURE) {
235
0
    RETURN_THROWS();
236
0
  }
237
0
}
238
239
PHP_METHOD(Time_Duration, fromHours)
240
0
{
241
0
  zend_ulong hours;
242
243
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
244
0
    Z_PARAM_ULONG(hours);
245
0
  ZEND_PARSE_PARAMETERS_END();
246
247
0
  if (hours > (ZEND_ULONG_MAX / 3600)) {
248
0
    throw_out_of_range_exception();
249
0
    RETURN_THROWS();
250
0
  }
251
252
0
  if (create_duration(return_value, hours * 3600, /* nanoseconds */ 0) == FAILURE) {
253
0
    RETURN_THROWS();
254
0
  }
255
0
}
256
257
PHP_METHOD(Time_Duration, fromIso8601DurationString)
258
0
{
259
0
  zend_string *specification;
260
261
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
262
0
    Z_PARAM_STR(specification);
263
0
  ZEND_PARSE_PARAMETERS_END();
264
265
0
  int error;
266
0
  timelib_duration *d = timelib_duration_create_from_iso8601string(ZSTR_VAL(specification), &error);
267
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
268
0
    throw_timelib_error(error);
269
0
    RETURN_THROWS();
270
0
  }
271
272
0
  php_date_time_duration *new = create_duration_shell(return_value);
273
0
  new->duration = *d;
274
0
  timelib_duration_dtor(d);
275
276
0
  if (sync_properties(new) == FAILURE) {
277
0
    RETURN_THROWS();
278
0
  }
279
0
}
280
281
PHP_METHOD(Time_Duration, negate)
282
0
{
283
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
284
285
0
  ZEND_PARSE_PARAMETERS_NONE();
286
287
0
  php_date_time_duration *new = create_duration_shell(return_value);
288
289
0
  int error = timelib_duration_negate_static(&new->duration, &original->duration);
290
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
291
0
    throw_timelib_error(error);
292
0
    RETURN_THROWS();
293
0
  }
294
295
0
  if (sync_properties(new) == FAILURE) {
296
0
    RETURN_THROWS();
297
0
  }
298
0
}
299
300
PHP_METHOD(Time_Duration, absolute)
301
0
{
302
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
303
304
0
  ZEND_PARSE_PARAMETERS_NONE();
305
306
0
  if (!original->duration.negative) {
307
0
    RETURN_COPY(ZEND_THIS);
308
0
  }
309
310
0
  if (create_duration(return_value, original->duration.seconds, original->duration.nanoseconds) == FAILURE) {
311
0
    RETURN_THROWS();
312
0
  }
313
0
}
314
315
PHP_METHOD(Time_Duration, add)
316
0
{
317
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
318
319
0
  const php_date_time_duration *additional;
320
321
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
322
0
    Z_PARAM_DATE_TIME_DURATION(additional);
323
0
  ZEND_PARSE_PARAMETERS_END();
324
325
0
  php_date_time_duration *new = create_duration_shell(return_value);
326
327
0
  int error = timelib_duration_add_static(&new->duration, &original->duration, &additional->duration);
328
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
329
0
    throw_timelib_error(error);
330
0
    RETURN_THROWS();
331
0
  }
332
333
0
  if (sync_properties(new) == FAILURE) {
334
0
    RETURN_THROWS();
335
0
  }
336
0
}
337
338
PHP_METHOD(Time_Duration, sub)
339
0
{
340
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
341
342
0
  const php_date_time_duration *minus;
343
344
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
345
0
    Z_PARAM_DATE_TIME_DURATION(minus);
346
0
  ZEND_PARSE_PARAMETERS_END();
347
348
0
  php_date_time_duration *new = create_duration_shell(return_value);
349
350
0
  int error = timelib_duration_sub_static(&new->duration, &original->duration, &minus->duration);
351
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
352
0
    throw_timelib_error(error);
353
0
    RETURN_THROWS();
354
0
  }
355
356
0
  if (sync_properties(new) == FAILURE) {
357
0
    RETURN_THROWS();
358
0
  }
359
0
}
360
361
PHP_METHOD(Time_Duration, multiplyBy)
362
0
{
363
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
364
365
0
  zend_ulong factor;
366
367
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
368
0
    Z_PARAM_ULONG(factor);
369
0
  ZEND_PARSE_PARAMETERS_END();
370
371
0
  php_date_time_duration *new = create_duration_shell(return_value);
372
373
0
  int error = timelib_duration_mul_static(&new->duration, &original->duration, factor);
374
0
  if (error != TIMELIB_ERROR_NO_ERROR) {
375
0
    throw_timelib_error(error);
376
0
    RETURN_THROWS();
377
0
  }
378
379
0
  if (sync_properties(new) == FAILURE) {
380
0
    RETURN_THROWS();
381
0
  }
382
0
}
383
384
PHP_METHOD(Time_Duration, divideBy)
385
0
{
386
0
  const php_date_time_duration *original = Z_DATE_TIME_DURATION_P(ZEND_THIS);
387
388
0
  zend_ulong divisor;
389
390
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
391
0
    Z_PARAM_ULONG(divisor);
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_div_static(&new->duration, &original->duration, divisor);
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, compare)
408
0
{
409
0
  const php_date_time_duration *a;
410
0
  const php_date_time_duration *b;
411
412
0
  ZEND_PARSE_PARAMETERS_START(2, 2)
413
0
    Z_PARAM_DATE_TIME_DURATION(a);
414
0
    Z_PARAM_DATE_TIME_DURATION(b);
415
0
  ZEND_PARSE_PARAMETERS_END();
416
417
0
  RETURN_LONG(timelib_duration_compare(&a->duration, &b->duration));
418
0
}