Skip to content

Commit

Permalink
CDRIVER-1339 good errors from ISO8601 parser
Browse files Browse the repository at this point in the history
  • Loading branch information
ajdavis committed Dec 13, 2016
1 parent 693e32f commit 199ab19
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 54 deletions.
5 changes: 4 additions & 1 deletion src/bson/bson-iso8601-private.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
BSON_BEGIN_DECLS

bool
_bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out);
_bson_iso8601_date_parse (const char *str,
int32_t len,
int64_t *out,
bson_error_t *error);

BSON_END_DECLS

Expand Down
58 changes: 37 additions & 21 deletions src/bson/bson-iso8601.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#ifndef _WIN32
#include "bson-timegm-private.h"
#include "bson-json.h"
#endif


Expand Down Expand Up @@ -104,7 +105,10 @@ parse_num (const char *str,
}

bool
_bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
_bson_iso8601_date_parse (const char *str,
int32_t len,
int64_t *out,
bson_error_t *error)
{
const char *ptr;
int32_t remaining = len;
Expand Down Expand Up @@ -145,40 +149,52 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
struct tm posix_date = {0};
#endif

#define DATE_PARSE_ERR(msg) \
bson_set_error (error, \
BSON_ERROR_JSON, \
BSON_JSON_ERROR_READ_INVALID_PARAM, \
"Could not parse \"%s\" as date: " msg, \
str); \
return false

#define DEFAULT_DATE_PARSE_ERR \
DATE_PARSE_ERR ("use ISO8601 format yyyy-mm-ddThh:mm plus timezone, either" \
" \"Z\" or like \"+0500\"")

ptr = str;

/* we have to match at least yyyy-mm-ddThh:mm[:+-Z] */
/* we have to match at least yyyy-mm-ddThh:mm */
if (!(get_tok ("-", &ptr, &remaining, &year_ptr, &year_len) &&
get_tok ("-", &ptr, &remaining, &month_ptr, &month_len) &&
get_tok ("T", &ptr, &remaining, &day_ptr, &day_len) &&
get_tok (":", &ptr, &remaining, &hour_ptr, &hour_len) &&
get_tok (":+-Z", &ptr, &remaining, &min_ptr, &min_len))) {
return false;
DEFAULT_DATE_PARSE_ERR;
}

/* if the minute has a ':' at the end look for seconds */
if (min_ptr[min_len] == ':') {
if (remaining < 2) {
return false;
DATE_PARSE_ERR ("reached end of date while looking for seconds");
}

get_tok (".+-Z", &ptr, &remaining, &sec_ptr, &sec_len);

if (!sec_len) {
return false;
DATE_PARSE_ERR ("minute ends in \":\" seconds is required");
}
}

/* if we had a second and it is followed by a '.' look for milliseconds */
if (sec_len && sec_ptr[sec_len] == '.') {
if (remaining < 2) {
return false;
DATE_PARSE_ERR ("reached end of date while looking for milliseconds");
}

get_tok ("+-Z", &ptr, &remaining, &millis_ptr, &millis_len);

if (!millis_len) {
return false;
DATE_PARSE_ERR ("seconds ends in \".\", milliseconds is required");
}
}

Expand All @@ -192,33 +208,33 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
* across 1970 GMT. We'll check in timegm later on to make sure we're post
* 1970 */
if (!parse_num (year_ptr, year_len, 4, 1969, 9999, &year)) {
return false;
DATE_PARSE_ERR ("year must be an integer");
}

/* values are as in struct tm */
year -= 1900;

if (!parse_num (month_ptr, month_len, 2, 1, 12, &month)) {
return false;
DATE_PARSE_ERR ("month must be an integer");
}

/* values are as in struct tm */
month -= 1;

if (!parse_num (day_ptr, day_len, 2, 1, 31, &day)) {
return false;
DATE_PARSE_ERR ("day must be an integer");
}

if (!parse_num (hour_ptr, hour_len, 2, 0, 23, &hour)) {
return false;
DATE_PARSE_ERR ("hour must be an integer");
}

if (!parse_num (min_ptr, min_len, 2, 0, 59, &min)) {
return false;
DATE_PARSE_ERR ("minute must be an integer");
}

if (sec_len && !parse_num (sec_ptr, sec_len, 2, 0, 60, &sec)) {
return false;
DATE_PARSE_ERR ("seconds must be an integer");
}

if (tz_len > 0) {
Expand All @@ -229,15 +245,15 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
int32_t tz_min;

if (tz_len != 5 || !digits_only (tz_ptr + 1, 4)) {
return false;
DATE_PARSE_ERR ("could not parse timezone");
}

if (!parse_num (tz_ptr + 1, 2, -1, -23, 23, &tz_hour)) {
return false;
DATE_PARSE_ERR ("timezone hour must be at most 23");
}

if (!parse_num (tz_ptr + 3, 2, -1, 0, 59, &tz_min)) {
return false;
DATE_PARSE_ERR ("timezone minute must be at most 59");
}

/* we inflect the meaning of a 'positive' timezone. Those are hours
Expand All @@ -246,10 +262,10 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
(tz_ptr[0] == '-' ? 1 : -1) * ((tz_min * 60) + (tz_hour * 60 * 60));

if (!(tz_adjustment > -86400 && tz_adjustment < 86400)) {
return false;
DATE_PARSE_ERR ("timezone offset must be less than 24 hours");
}
} else {
return false;
DATE_PARSE_ERR ("timezone is required");
}
}

Expand All @@ -259,7 +275,7 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
millis = 0;

if (millis_len > 3 || !digits_only (millis_ptr, millis_len)) {
return false;
DATE_PARSE_ERR ("milliseconds must be an integer");
}

for (i = 1, magnitude = 1; i <= millis_len; i++, magnitude *= 10) {
Expand All @@ -273,7 +289,7 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
}

if (millis < 0 || millis > 1000) {
return false;
DATE_PARSE_ERR ("milliseconds must be at least 0 and less than 1000");
}
}

Expand Down Expand Up @@ -326,7 +342,7 @@ _bson_iso8601_date_parse (const char *str, int32_t len, int64_t *out)
millis += tz_adjustment * 1000;

if (millis < 0) {
return false;
DATE_PARSE_ERR ("must be after January 1, 1970");
}

*out = millis;
Expand Down
6 changes: 3 additions & 3 deletions src/bson/bson-json.c
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ _bson_json_read_string (bson_json_reader_t *reader, /* IN */
case BSON_JSON_LF_DATE: {
int64_t v64;

if (!_bson_iso8601_date_parse ((char *) val, (int) vlen, &v64)) {
_bson_json_read_set_error (
reader, "Could not parse \"%s\" as a date", val_w_null);
if (!_bson_iso8601_date_parse (
(char *) val, (int) vlen, &v64, reader->error)) {
jsonsl_stop (reader->json);
} else {
bson->bson_type_data.date.has_date = true;
bson->bson_type_data.date.date = v64;
Expand Down
6 changes: 4 additions & 2 deletions tests/test-iso8601.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ static void
test_date (const char *str, int64_t millis)
{
int64_t v;
bson_error_t error;

if (!_bson_iso8601_date_parse (str, strlen (str), &v)) {
if (!_bson_iso8601_date_parse (str, strlen (str), &v, &error)) {
fprintf (stderr, "could not parse (%s)\n", str);
abort ();
}
Expand All @@ -36,8 +37,9 @@ static void
test_date_should_fail (const char *str)
{
int64_t v;
bson_error_t error;

if (_bson_iso8601_date_parse (str, strlen (str), &v)) {
if (_bson_iso8601_date_parse (str, strlen (str), &v, &error)) {
fprintf (stderr, "should not be able to parse (%s)\n", str);
abort ();
}
Expand Down
119 changes: 92 additions & 27 deletions tests/test-json.c
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,9 @@ test_bson_as_json_code (void)
BSON_APPEND_INT32 (&scope, "x", 1);
assert (BSON_APPEND_CODE_WITH_SCOPE (&code, "c", "function () {}", &scope));
str = bson_as_json (&code, NULL);
ASSERT_CMPSTR (str, "{ \"c\" : { \"$code\" : \"function () {}\", \"$scope\" "
": { \"x\" : 1 } } }");
ASSERT_CMPSTR (str,
"{ \"c\" : { \"$code\" : \"function () {}\", \"$scope\" "
": { \"x\" : 1 } } }");

bson_free (str);
bson_destroy (&code);
Expand Down Expand Up @@ -1508,57 +1509,121 @@ test_bson_json_array_subdoc (void)
}

static void
test_bson_json_date_check (bool should_work, const char *json, int64_t value)
test_bson_json_date_check (const char *json, int64_t value)
{
bson_error_t error = {0};
bson_t b, compare;
bool r;

if (should_work) {
bson_init (&compare);
bson_init (&compare);

BSON_APPEND_DATE_TIME (&compare, "dt", value);
BSON_APPEND_DATE_TIME (&compare, "dt", value);

r = bson_init_from_json (&b, json, -1, &error);
r = bson_init_from_json (&b, json, -1, &error);

if (!r) {
fprintf (stderr, "%s\n", error.message);
}
if (!r) {
fprintf (stderr, "%s\n", error.message);
}

assert (r);
assert (r);

bson_eq_bson (&b, &compare);
bson_destroy (&compare);
bson_destroy (&b);
} else {
r = bson_init_from_json (&b, json, -1, &error);
bson_eq_bson (&b, &compare);
bson_destroy (&compare);
bson_destroy (&b);
}

if (r) {
fprintf (stderr, "parsing %s should fail\n", json);
}

assert (!r);
static void
test_bson_json_date_error (const char *json, const char *msg)
{
bson_error_t error = {0};
bson_t b;
bool r;
r = bson_init_from_json (&b, json, -1, &error);
if (r) {
fprintf (stderr, "parsing %s should fail\n", json);
}
assert (!r);
ASSERT_ERROR_CONTAINS (
error, BSON_ERROR_JSON, BSON_JSON_ERROR_READ_INVALID_PARAM, msg);
}

static void
test_bson_json_date (void)
{
/* to make a timestamp, "python3 -m pip install iso8601" and in Python 3:
* iso8601.parse_date("2016-12-13T12:34:56.123Z").timestamp() * 1000
*/
test_bson_json_date_check (
true, "{ \"dt\" : { \"$date\" : \"1970-01-01T00:00:00.000Z\" } }", 0);
"{ \"dt\" : { \"$date\" : \"2016-12-13T12:34:56.123Z\" } }",
1481632496123);
test_bson_json_date_check (
true, "{ \"dt\" : { \"$date\" : \"1969-12-31T16:00:00.000-0800\" } }", 0);
"{ \"dt\" : { \"$date\" : \"1970-01-01T00:00:00.000Z\" } }", 0);
test_bson_json_date_check (
true, "{ \"dt\" : { \"$date\" : -62135593139000 } }", -62135593139000);
"{ \"dt\" : { \"$date\" : \"1969-12-31T16:00:00.000-0800\" } }", 0);
test_bson_json_date_check ("{ \"dt\" : { \"$date\" : -62135593139000 } }",
-62135593139000);
test_bson_json_date_check (
true,
"{ \"dt\" : { \"$date\" : { \"$numberLong\" : \"-62135593139000\" } } }",
-62135593139000);

test_bson_json_date_check (
false,
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000+01:00\" } }",
0);
"Could not parse");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:30:\" } }",
"reached end of date while looking for seconds");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:+01:00\" } }",
"seconds is required");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:30:00.\" } }",
"reached end of date while looking for milliseconds");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.+01:00\" } }",
"milliseconds is required");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"foo-01-01T00:00:00.000Z\" } }",
"year must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-foo-01T00:00:00.000Z\" } }",
"month must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-fooT00:00:00.000Z\" } }",
"day must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01Tfoo:00:00.000Z\" } }",
"hour must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T00:foo:00.000Z\" } }",
"minute must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T00:00:foo.000Z\" } }",
"seconds must be an integer");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000\" } }",
"timezone is required");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000X\" } }",
"timezone is required");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000+1\" } }",
"could not parse timezone");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000+xx00\" } }",
"could not parse timezone");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000+2400\" } }",
"timezone hour must be at most 23");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000-2400\" } }",
"timezone hour must be at most 23");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000+0060\" } }",
"timezone minute must be at most 59");
test_bson_json_date_error (
"{ \"dt\" : { \"$date\" : \"1970-01-01T01:00:00.000-0060\" } }",
"timezone minute must be at most 59");
}

static void
Expand Down

0 comments on commit 199ab19

Please sign in to comment.