/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 | } |