Skip to content

Commit

Permalink
Fixed bug #80022: Support ISO 8601 years outside 0000-9999 range better
Browse files Browse the repository at this point in the history
  • Loading branch information
derickr committed Jul 22, 2022
1 parent 8ea587a commit 6ae86c2
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 39 deletions.
47 changes: 42 additions & 5 deletions ext/date/php_date.c
Expand Up @@ -134,6 +134,7 @@ PHPAPI time_t php_time(void)
* zone = (( "+" / "-" ) 4DIGIT)
*/
#define DATE_FORMAT_RFC2822 "D, d M Y H:i:s O"

/*
* RFC3339, Section 5.6: http://www.ietf.org/rfc/rfc3339.txt
* date-fullyear = 4DIGIT
Expand All @@ -156,8 +157,42 @@ PHPAPI time_t php_time(void)
*/
#define DATE_FORMAT_RFC3339 "Y-m-d\\TH:i:sP"

/*
* This format does not technically match the ISO 8601 standard, as it does not
* use : in the UTC offset format specifier. This is kept for BC reasons. The
* DATE_FORMAT_ISO8601_EXPANDED format does correct this, as well as adding
* support for years out side of the traditional 0000-9999 range.
*/
#define DATE_FORMAT_ISO8601 "Y-m-d\\TH:i:sO"

/* ISO 8601:2004(E)
*
* Section 3.5 Expansion:
* By mutual agreement of the partners in information interchange, it is
* permitted to expand the component identifying the calendar year, which is
* otherwise limited to four digits. This enables reference to dates and times
* in calendar years outside the range supported by complete representations,
* i.e. before the start of the year [0000] or after the end of the year
* [9999]."
*
* Section 4.1.2.4 Expanded representations:
* If, by agreement, expanded representations are used, the formats shall be as
* specified below. The interchange parties shall agree the additional number of
* digits in the time element year. In the examples below it has been agreed to
* expand the time element year with two digits.
* Extended format: ±YYYYY-MM-DD
* Example: +001985-04-12
*
* PHP's year expansion digits are variable.
*/
#define DATE_FORMAT_ISO8601_EXPANDED "X-m-d\\TH:i:sP"

/* Internal Only
* This format only extends the year when needed, keeping the 'P' format with
* colon for UTC offsets
*/
#define DATE_FORMAT_ISO8601_LARGE_YEAR "x-m-d\\TH:i:sP"

/*
* RFC3339, Appendix A: http://www.ietf.org/rfc/rfc3339.txt
* ISO 8601 also requires (in section 5.3.1.3) that a decimal fraction
Expand Down Expand Up @@ -659,6 +694,8 @@ static zend_string *date_format(const char *format, size_t format_len, timelib_t
case 'L': length = slprintf(buffer, sizeof(buffer), "%d", timelib_is_leap((int) t->y)); break;
case 'y': length = slprintf(buffer, sizeof(buffer), "%02d", (int) (t->y % 100)); break;
case 'Y': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "", php_date_llabs((timelib_sll) t->y)); break;
case 'x': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : (t->y >= 10000 ? "+" : ""), php_date_llabs((timelib_sll) t->y)); break;
case 'X': length = slprintf(buffer, sizeof(buffer), "%s%04lld", t->y < 0 ? "-" : "+", php_date_llabs((timelib_sll) t->y)); break;

/* time */
case 'a': length = slprintf(buffer, sizeof(buffer), "%s", t->h >= 12 ? "pm" : "am"); break;
Expand Down Expand Up @@ -1810,7 +1847,7 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
zval zv;

/* first we add the date and time in ISO format */
ZVAL_STR(&zv, date_format("Y-m-d H:i:s.u", sizeof("Y-m-d H:i:s.u")-1, dateobj->time, 1));
ZVAL_STR(&zv, date_format("x-m-d H:i:s.u", sizeof("x-m-d H:i:s.u")-1, dateobj->time, 1));
zend_hash_str_update(props, "date", sizeof("date")-1, &zv);

/* then we add the timezone name (or similar) */
Expand Down Expand Up @@ -3800,7 +3837,7 @@ PHP_FUNCTION(timezone_transitions_get)
#define add_nominal() \
array_init(&element); \
add_assoc_long(&element, "ts", timestamp_begin); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, timestamp_begin, 0)); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, timestamp_begin, 0)); \
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[0].offset); \
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[0].isdst); \
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[0].abbr_idx]); \
Expand All @@ -3809,7 +3846,7 @@ PHP_FUNCTION(timezone_transitions_get)
#define add(i,ts) \
array_init(&element); \
add_assoc_long(&element, "ts", ts); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].offset); \
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].isdst); \
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[tzobj->tzi.tz->trans_idx[i]].abbr_idx]); \
Expand All @@ -3818,7 +3855,7 @@ PHP_FUNCTION(timezone_transitions_get)
#define add_by_index(i,ts) \
array_init(&element); \
add_assoc_long(&element, "ts", ts); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
add_assoc_long(&element, "offset", tzobj->tzi.tz->type[i].offset); \
add_assoc_bool(&element, "isdst", tzobj->tzi.tz->type[i].isdst); \
add_assoc_string(&element, "abbr", &tzobj->tzi.tz->timezone_abbr[tzobj->tzi.tz->type[i].abbr_idx]); \
Expand All @@ -3827,7 +3864,7 @@ PHP_FUNCTION(timezone_transitions_get)
#define add_from_tto(to,ts) \
array_init(&element); \
add_assoc_long(&element, "ts", ts); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601, 13, ts, 0)); \
add_assoc_str(&element, "time", php_format_date(DATE_FORMAT_ISO8601_LARGE_YEAR, 13, ts, 0)); \
add_assoc_long(&element, "offset", (to)->offset); \
add_assoc_bool(&element, "isdst", (to)->is_dst); \
add_assoc_string(&element, "abbr", (to)->abbr); \
Expand Down
8 changes: 8 additions & 0 deletions ext/date/php_date.stub.php
Expand Up @@ -20,6 +20,12 @@
*/
const DATE_ISO8601 = "Y-m-d\\TH:i:sO";

/**
* @var string
* @cvalue DATE_FORMAT_ISO8601_EXPANDED
*/
const DATE_ISO8601_EXPANDED = "X-m-d\\TH:i:sP";

/**
* @var string
* @cvalue DATE_FORMAT_RFC822
Expand Down Expand Up @@ -285,6 +291,8 @@ interface DateTimeInterface
/** @var string */
public const ISO8601 = DATE_ISO8601;
/** @var string */
public const ISO8601_EXPANDED = DATE_ISO8601_EXPANDED;
/** @var string */
public const RFC822 = DATE_RFC822;
/** @var string */
public const RFC850 = DATE_RFC850;
Expand Down
11 changes: 10 additions & 1 deletion ext/date/php_date_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ext/date/tests/DateTimeZone_getTransitions_basic1.phpt
Expand Up @@ -32,7 +32,7 @@ array(5) {
["ts"]=>
int(-213228000)
["time"]=>
string(24) "1963-03-31T02:00:00+0000"
string(25) "1963-03-31T02:00:00+00:00"
["offset"]=>
int(3600)
["isdst"]=>
Expand Down
46 changes: 23 additions & 23 deletions ext/date/tests/DateTimeZone_getTransitions_bug1.phpt
Expand Up @@ -24,35 +24,35 @@ showTransitions('Europe/Paris', 1645095600); // GH Issue 8108
--EXPECT--
Europe/London from @1648342200-@1727398200:

1648342200 2022-03-27T00:50:00+0000 0 x GMT
1648342800 2022-03-27T01:00:00+0000 3600 DST BST
1667091600 2022-10-30T01:00:00+0000 0 x GMT
1679792400 2023-03-26T01:00:00+0000 3600 DST BST
1698541200 2023-10-29T01:00:00+0000 0 x GMT
1711846800 2024-03-31T01:00:00+0000 3600 DST BST
1648342200 2022-03-27T00:50:00+00:00 0 x GMT
1648342800 2022-03-27T01:00:00+00:00 3600 DST BST
1667091600 2022-10-30T01:00:00+00:00 0 x GMT
1679792400 2023-03-26T01:00:00+00:00 3600 DST BST
1698541200 2023-10-29T01:00:00+00:00 0 x GMT
1711846800 2024-03-31T01:00:00+00:00 3600 DST BST

America/Los_Angeles from @1648557596-@1727613596:

1648557596 2022-03-29T12:39:56+0000 -25200 DST PDT
1667725200 2022-11-06T09:00:00+0000 -28800 x PST
1678615200 2023-03-12T10:00:00+0000 -25200 DST PDT
1699174800 2023-11-05T09:00:00+0000 -28800 x PST
1710064800 2024-03-10T10:00:00+0000 -25200 DST PDT
1648557596 2022-03-29T12:39:56+00:00 -25200 DST PDT
1667725200 2022-11-06T09:00:00+00:00 -28800 x PST
1678615200 2023-03-12T10:00:00+00:00 -25200 DST PDT
1699174800 2023-11-05T09:00:00+00:00 -28800 x PST
1710064800 2024-03-10T10:00:00+00:00 -25200 DST PDT

America/Chicago from @1293861600-@1372917600:

1293861600 2011-01-01T06:00:00+0000 -21600 x CST
1300003200 2011-03-13T08:00:00+0000 -18000 DST CDT
1320562800 2011-11-06T07:00:00+0000 -21600 x CST
1331452800 2012-03-11T08:00:00+0000 -18000 DST CDT
1352012400 2012-11-04T07:00:00+0000 -21600 x CST
1362902400 2013-03-10T08:00:00+0000 -18000 DST CDT
1293861600 2011-01-01T06:00:00+00:00 -21600 x CST
1300003200 2011-03-13T08:00:00+00:00 -18000 DST CDT
1320562800 2011-11-06T07:00:00+00:00 -21600 x CST
1331452800 2012-03-11T08:00:00+00:00 -18000 DST CDT
1352012400 2012-11-04T07:00:00+00:00 -21600 x CST
1362902400 2013-03-10T08:00:00+00:00 -18000 DST CDT

Europe/Paris from @1645095600-@1724151600:

1645095600 2022-02-17T11:00:00+0000 3600 x CET
1648342800 2022-03-27T01:00:00+0000 7200 DST CEST
1667091600 2022-10-30T01:00:00+0000 3600 x CET
1679792400 2023-03-26T01:00:00+0000 7200 DST CEST
1698541200 2023-10-29T01:00:00+0000 3600 x CET
1711846800 2024-03-31T01:00:00+0000 7200 DST CEST
1645095600 2022-02-17T11:00:00+00:00 3600 x CET
1648342800 2022-03-27T01:00:00+00:00 7200 DST CEST
1667091600 2022-10-30T01:00:00+00:00 3600 x CET
1679792400 2023-03-26T01:00:00+00:00 7200 DST CEST
1698541200 2023-10-29T01:00:00+00:00 3600 x CET
1711846800 2024-03-31T01:00:00+00:00 7200 DST CEST
29 changes: 29 additions & 0 deletions ext/date/tests/bug75035.phpt
@@ -0,0 +1,29 @@
--TEST--
Bug #75035 (Datetime fails to unserialize "extreme" dates)
--INI--
date.timezone=UTC
--FILE--
<?php
var_dump('PHP version', PHP_VERSION);

foreach ([PHP_INT_MIN, PHP_INT_MAX] as $extreme) {
$i = 64;
while ($i --> 0) {
$d = new DateTime('@' . ($extreme >> $i));
$s = serialize($d);
try {
$u = unserialize($s);
} catch (Error $e) {
$u = "failed unserialization: " . $e->getMessage() . ' : ' . $s;
}
$original = $d->format('Y-m-d H:i:s');
$serializedUnserialized = is_string($u) ? $u : $u->format('Y-m-d H:i:s');
if ($original !== $serializedUnserialized) {
var_dump('[' . ($extreme >> $i) . '] ' . $original . ' => ' . $serializedUnserialized);
}
}
}
?>
--EXPECTF--
string(11) "PHP version"
string(%d) "%s"
6 changes: 3 additions & 3 deletions ext/date/tests/bug80963.phpt
Expand Up @@ -19,7 +19,7 @@ array(5) {
["ts"]=>
int(2140045200)
["time"]=>
string(24) "2037-10-25T01:00:00+0000"
string(25) "2037-10-25T01:00:00+00:00"
["offset"]=>
int(0)
["isdst"]=>
Expand All @@ -32,7 +32,7 @@ array(5) {
["ts"]=>
int(2140668000)
["time"]=>
string(24) "2037-11-01T06:00:00+0000"
string(25) "2037-11-01T06:00:00+00:00"
["offset"]=>
int(-18000)
["isdst"]=>
Expand All @@ -45,7 +45,7 @@ array(5) {
["ts"]=>
int(2140045200)
["time"]=>
string(24) "2037-10-25T01:00:00+0000"
string(25) "2037-10-25T01:00:00+00:00"
["offset"]=>
int(3600)
["isdst"]=>
Expand Down
10 changes: 5 additions & 5 deletions ext/date/tests/bug81504.phpt
Expand Up @@ -11,8 +11,8 @@ foreach ($tz->getTransitions(strtotime("1996-01-01"), strtotime("1997-12-31")) a
}
?>
--EXPECT--
1996-01-01T00:00:00+0000 3600 CET
1996-03-31T01:00:00+0000 7200 CEST
1996-10-27T01:00:00+0000 3600 CET
1997-03-30T01:00:00+0000 7200 CEST
1997-10-26T01:00:00+0000 3600 CET
1996-01-01T00:00:00+00:00 3600 CET
1996-03-31T01:00:00+00:00 7200 CEST
1996-10-27T01:00:00+00:00 3600 CET
1997-03-30T01:00:00+00:00 7200 CEST
1997-10-26T01:00:00+00:00 3600 CET
2 changes: 1 addition & 1 deletion ext/date/tests/timezone_transitions_get_basic1.phpt
Expand Up @@ -35,7 +35,7 @@ array(5) {
["ts"]=>
int(-213228000)
["time"]=>
string(24) "1963-03-31T02:00:00+0000"
string(25) "1963-03-31T02:00:00+00:00"
["offset"]=>
int(3600)
["isdst"]=>
Expand Down

0 comments on commit 6ae86c2

Please sign in to comment.