/* * Copyright 2018-present MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "php_phongo.h" #include "phongo_error.h" #include "phongo_util.h" #include "Int64_arginfo.h" zend_class_entry* php_phongo_int64_ce; /* Initialize the object and return whether it was successful. */ static bool php_phongo_int64_init(php_phongo_int64_t* intern, int64_t integer) { intern->integer = integer; intern->initialized = true; return true; } /* Initialize the object from a numeric string and return whether it was * successful. An exception will be thrown on error. */ static bool php_phongo_int64_init_from_string(php_phongo_int64_t* intern, const char* s_integer, size_t s_integer_len) { int64_t integer; if (!php_phongo_parse_int64(&integer, s_integer, s_integer_len)) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Error parsing \"%s\" as 64-bit integer for %s initialization", s_integer, ZSTR_VAL(php_phongo_int64_ce->name)); return false; } return php_phongo_int64_init(intern, integer); } /* Initialize the object from a HashTable and return whether it was successful. * An exception will be thrown on error. */ static bool php_phongo_int64_init_from_hash(php_phongo_int64_t* intern, HashTable* props) { zval* value; if ((value = zend_hash_str_find(props, "integer", sizeof("integer") - 1)) && Z_TYPE_P(value) == IS_STRING) { return php_phongo_int64_init_from_string(intern, Z_STRVAL_P(value), Z_STRLEN_P(value)); } phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "%s initialization requires \"integer\" string field", ZSTR_VAL(php_phongo_int64_ce->name)); return false; } HashTable* php_phongo_int64_get_properties_hash(zend_object* object, bool is_temp) { php_phongo_int64_t* intern; HashTable* props; intern = Z_OBJ_INT64(object); PHONGO_GET_PROPERTY_HASH_INIT_PROPS(is_temp, intern, props, 2); if (!intern->initialized) { return props; } { zval value; ZVAL_INT64_STRING(&value, intern->integer); zend_hash_str_update(props, "integer", sizeof("integer") - 1, &value); } return props; } static PHP_METHOD(MongoDB_BSON_Int64, __construct) { php_phongo_int64_t* intern; zval* value; intern = Z_INT64_OBJ_P(getThis()); PHONGO_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ZVAL(value); PHONGO_PARSE_PARAMETERS_END(); if (Z_TYPE_P(value) == IS_STRING) { php_phongo_int64_init_from_string(intern, Z_STRVAL_P(value), Z_STRLEN_P(value)); } else if (Z_TYPE_P(value) == IS_LONG) { php_phongo_int64_init(intern, Z_LVAL_P(value)); } else { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Expected value to be integer or string, %s given", zend_zval_type_name(value)); } } /* Return the Int64's value as a string. */ static PHP_METHOD(MongoDB_BSON_Int64, __toString) { php_phongo_int64_t* intern; PHONGO_PARSE_PARAMETERS_NONE(); intern = Z_INT64_OBJ_P(getThis()); ZVAL_INT64_STRING(return_value, intern->integer); } static PHP_METHOD(MongoDB_BSON_Int64, __set_state) { php_phongo_int64_t* intern; HashTable* props; zval* array; PHONGO_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ARRAY(array) PHONGO_PARSE_PARAMETERS_END(); object_init_ex(return_value, php_phongo_int64_ce); intern = Z_INT64_OBJ_P(return_value); props = Z_ARRVAL_P(array); php_phongo_int64_init_from_hash(intern, props); } static PHP_METHOD(MongoDB_BSON_Int64, jsonSerialize) { php_phongo_int64_t* intern; PHONGO_PARSE_PARAMETERS_NONE(); intern = Z_INT64_OBJ_P(getThis()); array_init_size(return_value, 1); ADD_ASSOC_INT64_AS_STRING(return_value, "$numberLong", intern->integer); } static PHP_METHOD(MongoDB_BSON_Int64, __serialize) { PHONGO_PARSE_PARAMETERS_NONE(); RETURN_ARR(php_phongo_int64_get_properties_hash(Z_OBJ_P(getThis()), true)); } static PHP_METHOD(MongoDB_BSON_Int64, __unserialize) { zval* data; PHONGO_PARSE_PARAMETERS_START(1, 1) Z_PARAM_ARRAY(data) PHONGO_PARSE_PARAMETERS_END(); php_phongo_int64_init_from_hash(Z_INT64_OBJ_P(getThis()), Z_ARRVAL_P(data)); } /* MongoDB\BSON\Int64 object handlers */ static zend_object_handlers php_phongo_handler_int64; static void php_phongo_int64_free_object(zend_object* object) { php_phongo_int64_t* intern = Z_OBJ_INT64(object); zend_object_std_dtor(&intern->std); if (intern->properties) { zend_hash_destroy(intern->properties); FREE_HASHTABLE(intern->properties); } } zend_object* php_phongo_int64_create_object(zend_class_entry* class_type) { php_phongo_int64_t* intern = zend_object_alloc(sizeof(php_phongo_int64_t), class_type); zend_object_std_init(&intern->std, class_type); object_properties_init(&intern->std, class_type); intern->std.handlers = &php_phongo_handler_int64; return &intern->std; } static zend_object* php_phongo_int64_clone_object(zend_object* object) { php_phongo_int64_t* intern; php_phongo_int64_t* new_intern; zend_object* new_object; intern = Z_OBJ_INT64(object); new_object = php_phongo_int64_create_object(object->ce); new_intern = Z_OBJ_INT64(new_object); zend_objects_clone_members(&new_intern->std, &intern->std); php_phongo_int64_init(new_intern, intern->integer); return new_object; } static bool php_phongo_int64_is_int64_object(zval* object) { if (Z_TYPE_P(object) != IS_OBJECT) { return false; } return Z_OBJ_P(object)->ce == php_phongo_int64_ce; } static bool php_phongo_int64_is_long_or_double(zval* value) { return Z_TYPE_P(value) == IS_LONG || Z_TYPE_P(value) == IS_DOUBLE; } static int php_phongo_int64_compare_int64_objects(zval* o1, zval* o2) { php_phongo_int64_t *intern1, *intern2; intern1 = Z_INT64_OBJ_P(o1); intern2 = Z_INT64_OBJ_P(o2); if (intern1->integer != intern2->integer) { return intern1->integer < intern2->integer ? -1 : 1; } return 0; } static int php_phongo_int64_compare_with_long_or_float(zval* object, zval* value) { php_phongo_int64_t* intern; int64_t long_value; double double_value; intern = Z_INT64_OBJ_P(object); assert(php_phongo_int64_is_long_or_double(value)); switch (Z_TYPE_P(value)) { case IS_LONG: long_value = Z_LVAL_P(value); if (intern->integer != long_value) { return intern->integer < long_value ? -1 : 1; } break; case IS_DOUBLE: double_value = Z_DVAL_P(value); if (intern->integer != double_value) { return intern->integer < double_value ? -1 : 1; } break; default: return 0; } return 0; } static int php_phongo_int64_compare_objects(zval* o1, zval* o2) { if (php_phongo_int64_is_int64_object(o1) && php_phongo_int64_is_int64_object(o2)) { return php_phongo_int64_compare_int64_objects(o1, o2); } if (php_phongo_int64_is_int64_object(o1) && php_phongo_int64_is_long_or_double(o2)) { return php_phongo_int64_compare_with_long_or_float(o1, o2); } if (php_phongo_int64_is_long_or_double(o1) && php_phongo_int64_is_int64_object(o2)) { // Invert the result as we're flipping the values used for comparison return -1 * php_phongo_int64_compare_with_long_or_float(o2, o1); } ZEND_COMPARE_OBJECTS_FALLBACK(o1, o2); return 0; } static zend_result php_phongo_int64_cast_object(zend_object* readobj, zval* retval, int type) { php_phongo_int64_t* intern; intern = Z_OBJ_INT64(readobj); switch (type) { case IS_DOUBLE: ZVAL_DOUBLE(retval, (double) intern->integer); return SUCCESS; case IS_LONG: case _IS_NUMBER: #if SIZEOF_ZEND_LONG == 4 if (intern->integer > INT32_MAX || intern->integer < INT32_MIN) { zend_error(E_WARNING, "Truncating 64-bit integer value %" PRId64 " to 32 bits", intern->integer); } #endif ZVAL_LONG(retval, intern->integer); return SUCCESS; case _IS_BOOL: ZVAL_BOOL(retval, intern->integer != 0); return SUCCESS; default: return zend_std_cast_object_tostring(readobj, retval, type); } } /* Computes the power of two int64_t values by using the exponentiation by * squaring algorithm. This is necessary because in case the result exceeds * the range of a int64_t, we want PHP to return a float as it would when * using 64-bit values directly. We can't use anything involving zend_long * here as this would limit us to 32 bits on a 32-bit platform. This also * prohibits us from falling back to PHP's default functions after unwrapping * the int64_t from the php_phongo_int64_t instance. */ static int64_t phongo_pow_int64(int64_t base, int64_t exp) { if (exp == 0) { return 1; } if (exp % 2) { return base * phongo_pow_int64(base * base, (exp - 1) / 2); } return phongo_pow_int64(base * base, exp / 2); } #define OPERATION_RESULT_INT64(value) phongo_int64_new(result, (value)); #define PHONGO_GET_INT64(int64, zval) \ if (Z_TYPE_P((zval)) == IS_LONG) { \ (int64) = Z_LVAL_P((zval)); \ } else if (Z_TYPE_P((zval)) == IS_OBJECT && Z_OBJCE_P((zval)) == php_phongo_int64_ce) { \ (int64) = Z_INT64_OBJ_P((zval))->integer; \ } else { \ return FAILURE; \ } #define INT64_SIGN_MASK INT64_MIN /* Overload arithmetic operators for computation on int64_t values. * This ensures that any computation involving at least one php_phongo_int64_t * results in a php_phongo_int64_t value, regardless of whether the result * would fit in an int32_t or not. Results that exceed the 64-bit integer * range are returned as float as PHP would do when using 64-bit integers. * Note that ZEND_(PRE|POST)_(INC|DEC) are not handled here: when checking for * a do_operation handler for inc/dec, PHP calls the handler with a ZEND_ADD * or ZEND_SUB opcode and the same pointer for result and op1, and a ZVAL_LONG * of 1 for op2. */ static zend_result php_phongo_int64_do_operation_ex(zend_uchar opcode, zval* result, zval* op1, zval* op2) { int64_t value1, value2, lresult; PHONGO_GET_INT64(value1, op1); switch (opcode) { case ZEND_ADD: PHONGO_GET_INT64(value2, op2); lresult = value1 + value2; /* The following is based on the logic in fast_long_add_function() in PHP. * If the result sign differs from the first operand sign, we have an overflow if: * - adding a positive to a positive yields a negative, or * - adding a negative to a negative (i.e. subtraction) yields a positive */ if ((value1 & INT64_SIGN_MASK) != (lresult & INT64_SIGN_MASK) && (value1 & INT64_SIGN_MASK) == (value2 & INT64_SIGN_MASK)) { ZVAL_DOUBLE(result, (double) value1 + (double) value2); } else { OPERATION_RESULT_INT64(lresult); } return SUCCESS; case ZEND_SUB: PHONGO_GET_INT64(value2, op2); lresult = value1 - value2; /* The following is based on the logic in fast_long_sub_function() in PHP. * If the result sign differs from the first operand sign, we have an overflow if: * - subtracting a positive from a negative yields a positive, or * - subtracting a negative from a positive (i.e. addition) yields a negative */ if ((value1 & INT64_SIGN_MASK) != (lresult & INT64_SIGN_MASK) && (value1 & INT64_SIGN_MASK) != (value2 & INT64_SIGN_MASK)) { ZVAL_DOUBLE(result, (double) value1 - (double) value2); } else { OPERATION_RESULT_INT64(lresult); } return SUCCESS; case ZEND_MUL: PHONGO_GET_INT64(value2, op2); /* The following is based on the C-native implementation of * ZEND_SIGNED_MULTIPLY_LONG() in PHP if no other methods (e.g. ASM * or _builtin_smull_overflow) can be used. */ { int64_t lres = value1 * value2; long double dres = (long double) value1 * (long double) value2; long double delta = (long double) lres - dres; if ((dres + delta) != dres) { ZVAL_DOUBLE(result, dres); } else { OPERATION_RESULT_INT64(lres); } } return SUCCESS; case ZEND_DIV: PHONGO_GET_INT64(value2, op2); if (value2 == 0) { zend_throw_exception(zend_ce_division_by_zero_error, "Division by zero", 0); return FAILURE; } /* The following is based on div_function_base() in PHP. * - INT64_MIN / 1 exceeds the int64 range -> return double * - if division has a remainder, return double as result can't be * an int */ if ((value1 == INT64_MIN && value2 == -1) || (value1 % value2 != 0)) { ZVAL_DOUBLE(result, (double) value1 / value2); } else { OPERATION_RESULT_INT64(value1 / value2); } return SUCCESS; case ZEND_MOD: PHONGO_GET_INT64(value2, op2); if (value2 == 0) { zend_throw_exception(zend_ce_division_by_zero_error, "Division by zero", 0); return FAILURE; } OPERATION_RESULT_INT64(value1 % value2); return SUCCESS; case ZEND_SL: PHONGO_GET_INT64(value2, op2); OPERATION_RESULT_INT64(value1 << value2); return SUCCESS; case ZEND_SR: PHONGO_GET_INT64(value2, op2); OPERATION_RESULT_INT64(value1 >> value2); return SUCCESS; case ZEND_POW: PHONGO_GET_INT64(value2, op2); // Negative exponents always yield floats, leave them for PHP to handle if (value2 < 0) { return FAILURE; } // Handle 0 separately to distinguish between base 0 and // phongo_pow_int64 overflowing if (value1 == 0) { OPERATION_RESULT_INT64(0); return SUCCESS; } { int64_t pow_result = phongo_pow_int64(value1, value2); // If the result would overflow an int64_t, we let PHP fall back // to its default pow() implementation which returns a float. if (pow_result == 0) { return FAILURE; } OPERATION_RESULT_INT64(pow_result); } return SUCCESS; case ZEND_BW_AND: PHONGO_GET_INT64(value2, op2); OPERATION_RESULT_INT64(value1 & value2); return SUCCESS; case ZEND_BW_OR: PHONGO_GET_INT64(value2, op2); OPERATION_RESULT_INT64(value1 | value2); return SUCCESS; case ZEND_BW_XOR: PHONGO_GET_INT64(value2, op2); OPERATION_RESULT_INT64(value1 ^ value2); return SUCCESS; case ZEND_BW_NOT: OPERATION_RESULT_INT64(~value1); return SUCCESS; default: return FAILURE; } } static zend_result php_phongo_int64_do_operation(zend_uchar opcode, zval* result, zval* op1, zval* op2) { zval op1_copy; int retval; // Copy op1 for unary operations (e.g. $int64++) to ensure correct return values if (result == op1) { ZVAL_COPY_VALUE(&op1_copy, op1); op1 = &op1_copy; } retval = php_phongo_int64_do_operation_ex(opcode, result, op1, op2); if (retval == SUCCESS && op1 == &op1_copy) { zval_ptr_dtor(op1); } return retval; } #undef OPERATION_RESULT_INT64 #undef PHONGO_GET_INT64 #undef INT64_SIGN_MASK static HashTable* php_phongo_int64_get_debug_info(zend_object* object, int* is_temp) { *is_temp = 1; return php_phongo_int64_get_properties_hash(object, true); } static HashTable* php_phongo_int64_get_properties(zend_object* object) { return php_phongo_int64_get_properties_hash(object, false); } void php_phongo_int64_init_ce(INIT_FUNC_ARGS) { php_phongo_int64_ce = register_class_MongoDB_BSON_Int64(php_phongo_json_serializable_ce, php_phongo_type_ce, zend_ce_stringable); php_phongo_int64_ce->create_object = php_phongo_int64_create_object; memcpy(&php_phongo_handler_int64, phongo_get_std_object_handlers(), sizeof(zend_object_handlers)); php_phongo_handler_int64.compare = php_phongo_int64_compare_objects; php_phongo_handler_int64.clone_obj = php_phongo_int64_clone_object; php_phongo_handler_int64.get_debug_info = php_phongo_int64_get_debug_info; php_phongo_handler_int64.get_properties = php_phongo_int64_get_properties; php_phongo_handler_int64.free_obj = php_phongo_int64_free_object; php_phongo_handler_int64.offset = XtOffsetOf(php_phongo_int64_t, std); php_phongo_handler_int64.cast_object = php_phongo_int64_cast_object; php_phongo_handler_int64.do_operation = php_phongo_int64_do_operation; } bool phongo_int64_new(zval* object, int64_t integer) { php_phongo_int64_t* intern; object_init_ex(object, php_phongo_int64_ce); intern = Z_INT64_OBJ_P(object); intern->integer = integer; intern->initialized = true; return true; }