Skip to content

PHPC-790: Accept integer and floats in Timestamp and UTCDateTime constructors #467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 25, 2016

Conversation

jmikola
Copy link
Member

@jmikola jmikola commented Nov 22, 2016

bson_ascii_strtoll() will support range checking in libbson 1.5.0 (CDRIVER-1377), so there is no longer any benefit of using _atoi64() on Windows. This will make parsing consistent across both platforms, since _atoi64() does not report errors for non-integer strings (e.g. "1.2").
@jmikola jmikola force-pushed the phpc-790 branch 4 times, most recently from d972e3c to 23ca7ed Compare November 22, 2016 21:10
@@ -91,22 +91,14 @@ static bool php_phongo_timestamp_init_from_string(php_phongo_timestamp_t *intern
* available on all platforms (e.g. HP-UX), and atoll() provides no error
* reporting at all. */

#if defined(PHP_WIN32)
increment = _atoi64(s_increment);
#else
increment = bson_ascii_strtoll(s_increment, &endptr, 10);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_atoi64() would parse "1234.5" as 1234 and raise no error, while bson_ascii_strtoll() is more strict. Also, bson_ascii_strtoll() now supports out-of-range error reporting in libbson 1.5.0 (CDRIVER-1337). That was the only benefit of using _atoi64() for Windows before, so I see no reason to use consistent parsing for all platforms.

}

if (Z_TYPE_P(increment) != IS_STRING) {
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected increment to be integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(increment)));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HHVM will throw "Expected timestamp to be an unsigned 32-bit integer or string" on type errors (see: here). I'm happy to submit a PR to revise that once this is approved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the HHVM warning is not wrong? It has to be an unsigned 32-bit integer. I don't see you test for this here, but it's in php_phongo_timestamp_init I believe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

php_phongo_timestamp_init() performs range checking and will throw the "Expected increment to be an unsigned 32-bit integer, %d given" exceptions. I can change this to "Expected timestamp to be an unsigned 32-bit integer or string" and remove the "%s given" bit. Sharing the unexpected type was for consistency with our other exceptions for type errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that for HHVM, we should add the "%s given" bit, and you change the message to "Expected increment to be an unsigned 32-bit integer or string, %s given" — does that make sense? (note, in your reply you used timestamp and not increment).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me. The timestamp/increment was indeed a typo on my part. I'll take care of the HHVM PR.

}

zend_restore_error_handling(&error_handling TSRMLS_CC);
if (Z_TYPE_P(milliseconds) != IS_STRING) {
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(milliseconds)));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HHVM only throws "Expected instance of DateTimeInterface" and "Error parsing %s as 64-bit integer" exceptions, so it may need a PR for this (see here).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the HHVM message is much clearer as an API point of view. The "error parsing x as 64-bit integer" is what people are interested in. Whether it's an "integer", "float", or "string" is irrelevant. The current message in your code also misses "float".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I omitted "float" from the message since the cast to a long is just for the user's convenience. I wasn't planning on documenting that we accept a float, since it is being truncated. This is mainly to skirt around scalar type hint errors.

In HHVM's case, the invalid argument is cast to a string value for the exception message. That leads to some odd output for null (empty string), boolean ("1" and "0"), and would likely emit some notice on an array. The type error I was using here was consistent with what we already did for ReadPreference, WriteConcern, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - add some tests for these additional types, so I can test HHVM against it as well? (If you haven't done so)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are in bson-utcdatetime_error-004.phpt.

@@ -8,19 +8,19 @@ MongoDB\BSON\Timestamp::__set_state() with broken numeric strings
require_once __DIR__ . '/../utils/tools.php';

echo throws(function() {
MongoDB\BSON\Timestamp::__set_state(['increment' => 'broken', 'timestamp' => '5678']);
MongoDB\BSON\Timestamp::__set_state(['increment' => '1.23', 'timestamp' => '5678']);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test would have been problematic for Windows since _atoi64() stops parsing at the first invalid character without raising an error; however, bson_ascii_strtoll() is more strict.

OK: Got MongoDB\Driver\Exception\InvalidArgumentException
Expected increment to be integer or string, %r(null|NULL)%r given
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
Expected increment to be integer or string, %r(double|float)%r given
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pattern is necessary for PHP 5 and 7.

<?php exit(0); ?>
--EXPECTF--
OK: Got MongoDB\Driver\Exception\InvalidArgumentException
Expected increment to be integer or string, %r(null|NULL)%r given
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pattern is necessary for PHP and HHVM.

@@ -1,26 +1,24 @@
--TEST--
MongoDB\BSON\Timestamp::__set_state() with broken numeric strings
--SKIPIF--
<?php if (8 !== PHP_INT_SIZE) { die('skip Only for 64-bit platform'); } ?>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was erroneously copy-pasted when the test was created in d25e565.

Copy link
Contributor

@derickr derickr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly fine, but some queries about error messages.

}

if (Z_TYPE_P(increment) != IS_STRING) {
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected increment to be integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(increment)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the HHVM warning is not wrong? It has to be an unsigned 32-bit integer. I don't see you test for this here, but it's in php_phongo_timestamp_init I believe.

zend_restore_error_handling(&error_handling TSRMLS_CC);
return;
}
if (Z_TYPE_P(milliseconds) == IS_DOUBLE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could cause data loss, and I'm not keen about that :-/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admittedly has some trouble reading through all of the zend_dval_to_lval() cases to see if this would be an issue. Would you agree that the best solution is to convert this to a string via "%f.0"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think that's needed. It's just that we ignore a user's sub-milliseconds, because they passed in a float, without notifying them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this isn't a matter of stuffing a double into a 32-bit integer, I don't understand the concern. UTCDateTime is documented as only representing milliseconds and the purpose of this change was because users previously had to use microtime(true) * 1000 to create a UTCDateTime with the current time. This is no different than a userland method using an (int) cast to ensure it's working with an integer (before scalar type hinting was available).

That said, I think the 32-bit issue is still a concern worth addressing. microtime(true) * 1000 will easily result in values that exceed INT32_MAX.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. That should be handled.

}

zend_restore_error_handling(&error_handling TSRMLS_CC);
if (Z_TYPE_P(milliseconds) != IS_STRING) {
phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected integer or string, %s given", zend_get_type_by_const(Z_TYPE_P(milliseconds)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the HHVM message is much clearer as an API point of view. The "error parsing x as 64-bit integer" is what people are interested in. Whether it's an "integer", "float", or "string" is irrelevant. The current message in your code also misses "float".

@@ -6,7 +6,7 @@ MongoDB\BSON\UTCDateTime unserialization requires "milliseconds" string to parse
require_once __DIR__ . '/../utils/tools.php';

echo throws(function() {
unserialize('C:24:"MongoDB\BSON\UTCDateTime":40:{a:1:{s:12:"milliseconds";s:7:"INVALID";}}');
unserialize('C:24:"MongoDB\BSON\UTCDateTime":42:{a:1:{s:12:"milliseconds";s:9:"1234.5678";}}');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have kept the length of the argument the same, as to not have to mess with lengths in hardcoded serialized strings. But that's done now.

zend_restore_error_handling(&error_handling TSRMLS_CC);
return;
}
if (Z_TYPE_P(milliseconds) == IS_DOUBLE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. That should be handled.

Changing "s" to "z" in zend_parse_parameters() allows integers to be accepted in strict types mode. If all necessary parameters are integers, we can also skip conversion to a string and back to a 64-bit integer.
zend_restore_error_handling(&error_handling TSRMLS_CC);
return;
}
tmp_len = snprintf(tmp, sizeof(tmp), "%.0f", Z_DVAL_P(milliseconds) > 0 ? floor(Z_DVAL_P(milliseconds)) : ceil(Z_DVAL_P(milliseconds)));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trunc() is available in C99, but I didn't want to risk any Windows build issues for older PHP issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to convert to a long straight away here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally used convert_to_long(), but you mentioned it would lose data. Although you were referring to precision after milliseconds, which I later explained we don't care about, that solution did have issues on 32-bit platforms. This method ensures that values larger than INT32_MAX in a float will convert properly on 32-bit.

@jmikola jmikola merged commit a916713 into mongodb:master Nov 25, 2016
jmikola added a commit that referenced this pull request Nov 25, 2016
@jmikola jmikola deleted the phpc-790 branch November 25, 2016 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants