/* * Copyright 2014-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 "bson/bson.h" #include #include #include #include #include "php_array_api.h" #include "php_phongo.h" #include "phongo_bson.h" #include "phongo_error.h" #include "BSON/Binary.h" #include "BSON/DBPointer.h" #include "BSON/Decimal128.h" #include "BSON/Document.h" #include "BSON/Int64.h" #include "BSON/Javascript.h" #include "BSON/ObjectId.h" #include "BSON/PackedArray.h" #include "BSON/Regex.h" #include "BSON/Symbol.h" #include "BSON/Timestamp.h" #include "BSON/UTCDateTime.h" #undef MONGOC_LOG_DOMAIN #define MONGOC_LOG_DOMAIN "PHONGO-BSON" #define PHONGO_BSON_STATE_ZCHILD(state) (&((php_phongo_bson_state*) (state))->zchild) #define PHONGO_FIELD_PATH_EXPANSION 8 #define PHONGO_TYPEMAP_FIELD_STR_ARRAY "array" #define PHONGO_TYPEMAP_FIELD_STR_DOCUMENT "document" #define PHONGO_TYPEMAP_FIELD_STR_ROOT "root" #define PHONGO_TYPEMAP_TYPE_STR_ARRAY "array" #define PHONGO_TYPEMAP_TYPE_STR_BSON "bson" #define PHONGO_TYPEMAP_TYPE_STR_OBJECT "object" #define PHONGO_TYPEMAP_TYPE_STR_STDCLASS "stdclass" /* Forward declarations */ static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data); static bool php_phongo_bson_visit_array(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_array, void* data); static inline bool phongo_is_class_instantiatable(const zend_class_entry* ce) { if (ce->ce_flags & (ZEND_ACC_EXPLICIT_ABSTRACT_CLASS | ZEND_ACC_IMPLICIT_ABSTRACT_CLASS | ZEND_ACC_INTERFACE | ZEND_ACC_TRAIT)) { return false; } if (ce->ce_flags & ZEND_ACC_ENUM) { return false; } return true; } /* Path builder */ char* php_phongo_field_path_as_string(php_phongo_field_path* field_path) { size_t length = 1; /* NULL character */ size_t i; char* path; char* ptr; if (!field_path) { return estrdup(""); } if (!field_path->elements) { return estrdup(""); } for (i = 0; i <= field_path->size; i++) { if (!field_path->elements[i]) { continue; } length += (1 + strlen(field_path->elements[i])); } path = emalloc(length); ptr = path; for (i = 0; i <= field_path->size; i++) { if (!field_path->elements[i]) { continue; } strcpy(ptr, field_path->elements[i]); ptr += strlen(field_path->elements[i]); ptr[0] = '.'; ptr++; } ptr[-1] = '\0'; return path; } php_phongo_field_path* php_phongo_field_path_alloc(bool owns_elements) { php_phongo_field_path* tmp = ecalloc(1, sizeof(php_phongo_field_path)); tmp->ref_count = 1; tmp->owns_elements = owns_elements; return tmp; } void php_phongo_field_path_free(php_phongo_field_path* field_path) { if (field_path->owns_elements) { size_t i; for (i = 0; i < field_path->size; i++) { efree(field_path->elements[i]); } } if (field_path->elements) { efree(field_path->elements); } if (field_path->element_types) { efree(field_path->element_types); } efree(field_path); } static void php_phongo_field_path_ensure_allocation(php_phongo_field_path* field_path, size_t level) { if (level >= field_path->allocated_size) { size_t i; field_path->allocated_size = field_path->size + PHONGO_FIELD_PATH_EXPANSION; field_path->elements = erealloc(field_path->elements, sizeof(char*) * field_path->allocated_size); field_path->element_types = erealloc(field_path->element_types, sizeof(php_phongo_bson_field_path_item_types) * field_path->allocated_size); for (i = level; i < field_path->allocated_size; i++) { field_path->elements[i] = NULL; field_path->element_types[i] = PHONGO_FIELD_PATH_ITEM_NONE; } } } void php_phongo_field_path_write_item_at_current_level(php_phongo_field_path* field_path, const char* element) { php_phongo_field_path_ensure_allocation(field_path, field_path->size); if (field_path->owns_elements) { /* Note: owns_elements is only used for field paths parsed from a type * map, so it's unlikely that an element would already have been * allocated here. This is in contrast to BSON encoding/decoding, which * frequently updates the field path and does not own elements. */ if (UNEXPECTED(field_path->elements[field_path->size])) { efree(field_path->elements[field_path->size]); } field_path->elements[field_path->size] = estrdup(element); } else { field_path->elements[field_path->size] = (char*) element; } } void php_phongo_field_path_write_type_at_current_level(php_phongo_field_path* field_path, php_phongo_bson_field_path_item_types element_type) { php_phongo_field_path_ensure_allocation(field_path, field_path->size); field_path->element_types[field_path->size] = element_type; } bool php_phongo_field_path_push(php_phongo_field_path* field_path, const char* element, php_phongo_bson_field_path_item_types element_type) { if (element) { php_phongo_field_path_write_item_at_current_level(field_path, element); } php_phongo_field_path_write_type_at_current_level(field_path, element_type); field_path->size++; return field_path->size <= BSON_MAX_NESTING_LEVEL; } bool php_phongo_field_path_pop(php_phongo_field_path* field_path) { php_phongo_field_path_ensure_allocation(field_path, field_path->size); field_path->elements[field_path->size] = NULL; field_path->element_types[field_path->size] = PHONGO_FIELD_PATH_ITEM_NONE; field_path->size--; field_path->elements[field_path->size] = NULL; field_path->element_types[field_path->size] = PHONGO_FIELD_PATH_ITEM_NONE; return true; } inline static bool php_phongo_bson_state_is_initialized(php_phongo_bson_state* state) { return state->field_path != NULL; } static void php_phongo_bson_state_ctor(php_phongo_bson_state* state) { state->field_path = php_phongo_field_path_alloc(false); } static void php_phongo_bson_state_copy_ctor(php_phongo_bson_state* dst, php_phongo_bson_state* src) { dst->map = src->map; if (src->field_path) { src->field_path->ref_count++; } dst->field_path = src->field_path; } static void php_phongo_bson_state_dtor(php_phongo_bson_state* state) { if (state->field_path) { state->field_path->ref_count--; if (state->field_path->ref_count < 1) { php_phongo_field_path_free(state->field_path); } state->field_path = NULL; } } static void php_phongo_bson_visit_corrupt(const bson_iter_t* iter ARG_UNUSED, void* data ARG_UNUSED) { mongoc_log(MONGOC_LOG_LEVEL_WARNING, MONGOC_LOG_DOMAIN, "Corrupt BSON data detected!"); } static void php_phongo_bson_visit_unsupported_type(const bson_iter_t* iter ARG_UNUSED, const char* key, uint32_t v_type_code, void* data ARG_UNUSED) { php_phongo_bson_state* state = (php_phongo_bson_state*) data; char* path_string; php_phongo_field_path_write_item_at_current_level(state->field_path, key); path_string = php_phongo_field_path_as_string(state->field_path); phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Detected unknown BSON type 0x%02hhx for field path \"%s\". Are you using the latest driver?", (unsigned char) v_type_code, path_string); efree(path_string); } static bool php_phongo_bson_visit_double(const bson_iter_t* iter ARG_UNUSED, const char* key, double v_double, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (state->is_visiting_array) { add_next_index_double(retval, v_double); } else { add_assoc_double(retval, key, v_double); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_utf8(const bson_iter_t* iter ARG_UNUSED, const char* key, size_t v_utf8_len, const char* v_utf8, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (state->is_visiting_array) { ADD_NEXT_INDEX_STRINGL(retval, v_utf8, v_utf8_len); } else { ADD_ASSOC_STRING_EX(retval, key, strlen(key), v_utf8, v_utf8_len); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_binary(const bson_iter_t* iter ARG_UNUSED, const char* key, bson_subtype_t v_subtype, size_t v_binary_len, const uint8_t* v_binary, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (v_subtype == 0x80 && strcmp(key, PHONGO_ODM_FIELD_NAME) == 0) { zend_string* zs_classname = zend_string_init((const char*) v_binary, v_binary_len, 0); zend_class_entry* found_ce = zend_fetch_class(zs_classname, ZEND_FETCH_CLASS_AUTO | ZEND_FETCH_CLASS_SILENT); zend_string_release(zs_classname); if (found_ce && phongo_is_class_instantiatable(found_ce) && instanceof_function(found_ce, php_phongo_persistable_ce)) { ((php_phongo_bson_state*) data)->odm_ce = found_ce; } } { zval zchild; if (!phongo_binary_new(&zchild, (const char*) v_binary, v_binary_len, v_subtype)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_undefined(const bson_iter_t* iter, const char* key, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; object_init_ex(&zchild, php_phongo_undefined_ce); if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_oid(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_oid_t* v_oid, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_objectid_new(&zchild, v_oid)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_bool(const bson_iter_t* iter ARG_UNUSED, const char* key, bool v_bool, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (state->is_visiting_array) { add_next_index_bool(retval, v_bool); } else { add_assoc_bool(retval, key, v_bool); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_date_time(const bson_iter_t* iter ARG_UNUSED, const char* key, int64_t msec_since_epoch, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_utcdatetime_new(&zchild, msec_since_epoch)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_decimal128(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_decimal128_t* decimal, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_decimal128_new(&zchild, decimal)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_null(const bson_iter_t* iter ARG_UNUSED, const char* key, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (state->is_visiting_array) { add_next_index_null(retval); } else { add_assoc_null(retval, key); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_regex(const bson_iter_t* iter ARG_UNUSED, const char* key, const char* v_regex, const char* v_options, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_regex_new(&zchild, v_regex, v_options)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_symbol(const bson_iter_t* iter, const char* key, size_t v_symbol_len, const char* v_symbol, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_symbol_new(&zchild, v_symbol, v_symbol_len)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_code(const bson_iter_t* iter ARG_UNUSED, const char* key, size_t v_code_len, const char* v_code, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_javascript_new(&zchild, v_code, v_code_len, NULL)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_dbpointer(const bson_iter_t* iter, const char* key, size_t namespace_len, const char* namespace, const bson_oid_t* oid, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_dbpointer_new(&zchild, namespace, namespace_len, oid)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_codewscope(const bson_iter_t* iter ARG_UNUSED, const char* key, size_t v_code_len, const char* v_code, const bson_t* v_scope, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_javascript_new(&zchild, v_code, v_code_len, v_scope)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_int32(const bson_iter_t* iter ARG_UNUSED, const char* key, int32_t v_int32, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; if (state->is_visiting_array) { add_next_index_long(retval, v_int32); } else { add_assoc_long(retval, key, v_int32); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_timestamp(const bson_iter_t* iter ARG_UNUSED, const char* key, uint32_t v_timestamp, uint32_t v_increment, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; if (!phongo_timestamp_new(&zchild, v_increment, v_timestamp)) { return true; } if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_int64(const bson_iter_t* iter ARG_UNUSED, const char* key, int64_t v_int64, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; php_phongo_field_path_write_item_at_current_level(state->field_path, key); if (state->is_visiting_array) { if (state->map.int64_as_object) { ADD_NEXT_INDEX_INT64_OBJ(retval, v_int64); } else { ADD_NEXT_INDEX_INT64(retval, v_int64); } } else { if (state->map.int64_as_object) { ADD_ASSOC_INT64_OBJ(retval, key, v_int64); } else { ADD_ASSOC_INT64(retval, key, v_int64); } } return false; } static bool php_phongo_bson_visit_maxkey(const bson_iter_t* iter ARG_UNUSED, const char* key, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; object_init_ex(&zchild, php_phongo_maxkey_ce); if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static bool php_phongo_bson_visit_minkey(const bson_iter_t* iter ARG_UNUSED, const char* key, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); php_phongo_bson_state* state = (php_phongo_bson_state*) data; zval zchild; object_init_ex(&zchild, php_phongo_minkey_ce); if (state->is_visiting_array) { add_next_index_zval(retval, &zchild); } else { ADD_ASSOC_ZVAL(retval, key, &zchild); } php_phongo_field_path_write_item_at_current_level(state->field_path, key); return false; } static const bson_visitor_t php_bson_visitors = { NULL /* php_phongo_bson_visit_before*/, NULL /*php_phongo_bson_visit_after*/, php_phongo_bson_visit_corrupt, php_phongo_bson_visit_double, php_phongo_bson_visit_utf8, php_phongo_bson_visit_document, php_phongo_bson_visit_array, php_phongo_bson_visit_binary, php_phongo_bson_visit_undefined, php_phongo_bson_visit_oid, php_phongo_bson_visit_bool, php_phongo_bson_visit_date_time, php_phongo_bson_visit_null, php_phongo_bson_visit_regex, php_phongo_bson_visit_dbpointer, php_phongo_bson_visit_code, php_phongo_bson_visit_symbol, php_phongo_bson_visit_codewscope, php_phongo_bson_visit_int32, php_phongo_bson_visit_timestamp, php_phongo_bson_visit_int64, php_phongo_bson_visit_maxkey, php_phongo_bson_visit_minkey, php_phongo_bson_visit_unsupported_type, php_phongo_bson_visit_decimal128, { NULL } }; static inline bool map_element_matches_field_path(php_phongo_field_path_map_element* map_element, php_phongo_field_path* current) { size_t i; if (map_element->entry->size != current->size) { return false; } for (i = 0; i < current->size; i++) { if (strcmp(map_element->entry->elements[i], "$") == 0) { continue; } if (strcmp(map_element->entry->elements[i], current->elements[i]) != 0) { return false; } } return true; } static php_phongo_field_path_map_element* map_find_field_path_entry(php_phongo_bson_state* state) { size_t i; /* Loop over all field path mappings, and for each, try to see whether it matches the current path */ for (i = 0; i < state->map.field_paths.size; i++) { if (map_element_matches_field_path(state->map.field_paths.map[i], state->field_path)) { return state->map.field_paths.map[i]; } } return NULL; } static void php_phongo_handle_field_path_entry_for_compound_type(php_phongo_bson_state* state, php_phongo_bson_typemap_element* element) { php_phongo_field_path_map_element* entry = map_find_field_path_entry(state); if (entry) { state->field_type.type = entry->node.type; state->field_type.ce = entry->node.ce; } else { state->field_type.type = element->type; state->field_type.ce = element->ce; } } static bool php_phongo_bson_visit_document(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_document, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); bson_iter_t child; php_phongo_bson_state* parent_state = (php_phongo_bson_state*) data; php_phongo_bson_state state; php_phongo_field_path_push(parent_state->field_path, key, PHONGO_FIELD_PATH_ITEM_DOCUMENT); PHONGO_BSON_INIT_STATE(state); php_phongo_bson_state_copy_ctor(&state, parent_state); /* Check for entries in the fieldPath type map key, and use them to * override the default ones for this type */ php_phongo_handle_field_path_entry_for_compound_type(&state, &state.map.document); /* Only traverse BSON document if we're not returning a raw BSON structure */ if (state.field_type.type != PHONGO_TYPEMAP_BSON) { if (!bson_iter_init(&child, v_document)) { php_phongo_bson_state_dtor(&state); return false; } array_init(&state.zchild); if (bson_iter_visit_all(&child, &php_bson_visitors, &state) || child.err_off) { /* Iteration stopped prematurely due to corruption or a failed * visitor. Free state.zchild, which we just initialized, and return * true to stop iteration for our parent context. */ zval_ptr_dtor(&state.zchild); php_phongo_bson_state_dtor(&state); return true; } } /* If php_phongo_bson_visit_binary() finds an ODM class, it should * supersede a default type map and named document class. */ if (state.odm_ce && state.field_type.type == PHONGO_TYPEMAP_NONE) { state.field_type.type = PHONGO_TYPEMAP_CLASS; } switch (state.field_type.type) { case PHONGO_TYPEMAP_BSON: { php_phongo_document_t* intern; object_init_ex(&state.zchild, php_phongo_document_ce); intern = Z_DOCUMENT_OBJ_P(&state.zchild); intern->bson = bson_copy(v_document); break; } case PHONGO_TYPEMAP_NATIVE_ARRAY: /* Do nothing, item will be added later */ break; case PHONGO_TYPEMAP_CLASS: { zval obj; zend_class_entry* obj_ce = state.odm_ce ? state.odm_ce : state.field_type.ce; object_init_ex(&obj, obj_ce); zend_call_method_with_1_params(Z_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state.zchild); zval_ptr_dtor(&state.zchild); ZVAL_COPY_VALUE(&state.zchild, &obj); break; } case PHONGO_TYPEMAP_NATIVE_OBJECT: default: convert_to_object(&state.zchild); } if (parent_state->is_visiting_array) { add_next_index_zval(retval, &state.zchild); } else { ADD_ASSOC_ZVAL(retval, key, &state.zchild); } php_phongo_bson_state_dtor(&state); php_phongo_field_path_pop(parent_state->field_path); return false; } static bool php_phongo_bson_visit_array(const bson_iter_t* iter ARG_UNUSED, const char* key, const bson_t* v_array, void* data) { zval* retval = PHONGO_BSON_STATE_ZCHILD(data); bson_iter_t child; php_phongo_bson_state* parent_state = (php_phongo_bson_state*) data; php_phongo_bson_state state; php_phongo_field_path_push(parent_state->field_path, key, PHONGO_FIELD_PATH_ITEM_ARRAY); PHONGO_BSON_INIT_STATE(state); php_phongo_bson_state_copy_ctor(&state, parent_state); /* Check for entries in the fieldPath type map key, and use them to * override the default ones for this type */ php_phongo_handle_field_path_entry_for_compound_type(&state, &state.map.array); /* Only traverse BSON array if we're not returning a raw BSON structure */ if (state.field_type.type != PHONGO_TYPEMAP_BSON) { if (!bson_iter_init(&child, v_array)) { php_phongo_bson_state_dtor(&state); return false; } /* Note that we are visiting an array, so element visitors know to use * add_next_index() (i.e. disregard BSON keys) instead of add_assoc() * when building the PHP array. */ state.is_visiting_array = true; array_init(&state.zchild); if (bson_iter_visit_all(&child, &php_bson_visitors, &state) || child.err_off) { /* Iteration stopped prematurely due to corruption or a failed * visitor. Free state.zchild, which we just initialized, and return * true to stop iteration for our parent context. */ zval_ptr_dtor(&state.zchild); php_phongo_bson_state_dtor(&state); return true; } } switch (state.field_type.type) { case PHONGO_TYPEMAP_BSON: { php_phongo_packedarray_t* intern; object_init_ex(&state.zchild, php_phongo_packedarray_ce); intern = Z_PACKEDARRAY_OBJ_P(&state.zchild); intern->bson = bson_copy(v_array); break; } case PHONGO_TYPEMAP_CLASS: { zval obj; object_init_ex(&obj, state.field_type.ce); zend_call_method_with_1_params(Z_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state.zchild); zval_ptr_dtor(&state.zchild); ZVAL_COPY_VALUE(&state.zchild, &obj); break; } case PHONGO_TYPEMAP_NATIVE_OBJECT: convert_to_object(&state.zchild); break; case PHONGO_TYPEMAP_NATIVE_ARRAY: default: /* Do nothing, will be added later */ break; } if (parent_state->is_visiting_array) { add_next_index_zval(retval, &state.zchild); } else { ADD_ASSOC_ZVAL(retval, key, &state.zchild); } php_phongo_bson_state_dtor(&state); php_phongo_field_path_pop(parent_state->field_path); return false; } /* Converts a BSON document to a PHP value using the default typemap. */ bool php_phongo_bson_to_zval(const bson_t* b, zval* zv) { bool retval; php_phongo_bson_state state; PHONGO_BSON_INIT_STATE(state); retval = php_phongo_bson_to_zval_ex(b, &state); ZVAL_ZVAL(zv, &state.zchild, 1, 1); return retval; } /* Converts BSON data to a PHP value using the default typemap. */ bool php_phongo_bson_data_to_zval(const unsigned char* data, int data_len, zval* zv) { bool retval; php_phongo_bson_state state; PHONGO_BSON_INIT_STATE(state); retval = php_phongo_bson_data_to_zval_ex(data, data_len, &state); ZVAL_ZVAL(zv, &state.zchild, 1, 1); return retval; } /* Converts a BSON value to a zval, returning BSON objects and arrays as * standard PHP types instead of Document or PackedArray instances. * * On success, the zval will be populated and true will be returned. On error, * an exception will have been thrown and false will be returned. */ bool phongo_bson_value_to_zval_legacy(const bson_value_t* value, zval* zv) { if (value->value_type == BSON_TYPE_ARRAY || value->value_type == BSON_TYPE_DOCUMENT) { bson_t bson = BSON_INITIALIZER; php_phongo_bson_state state; zval* return_value; bool retval = false; /* Use php_phongo_bson_to_zval_ex internally to convert arrays and documents */ PHONGO_BSON_INIT_STATE(state); state.map.root.type = PHONGO_TYPEMAP_NATIVE_ARRAY; bson_append_value(&bson, "data", 4, value); if (!php_phongo_bson_to_zval_ex(&bson, &state)) { /* Exception already thrown */ goto cleanup; } retval = true; return_value = php_array_fetchc(&state.zchild, "data"); if (return_value) { ZVAL_ZVAL(zv, return_value, 1, 0); } cleanup: zval_ptr_dtor(&state.zchild); return retval; } return phongo_bson_value_to_zval(value, zv); } /* Converts a BSON value to a zval. * * On success, the zval will be populated and true will be returned. On error, * an exception will have been thrown and false will be returned. */ bool phongo_bson_value_to_zval(const bson_value_t* value, zval* zv) { bson_t bson = BSON_INITIALIZER; switch (value->value_type) { case BSON_TYPE_INT32: ZVAL_LONG(zv, value->value.v_int32); return true; case BSON_TYPE_INT64: phongo_int64_new(zv, value->value.v_int64); return true; case BSON_TYPE_DOUBLE: ZVAL_DOUBLE(zv, value->value.v_double); return true; case BSON_TYPE_BOOL: ZVAL_BOOL(zv, value->value.v_bool); return true; case BSON_TYPE_NULL: ZVAL_NULL(zv); return true; case BSON_TYPE_UTF8: ZVAL_STRINGL(zv, value->value.v_utf8.str, value->value.v_utf8.len); return true; case BSON_TYPE_BINARY: return phongo_binary_new(zv, (char*) value->value.v_binary.data, value->value.v_binary.data_len, value->value.v_binary.subtype); case BSON_TYPE_OID: return phongo_objectid_new(zv, &value->value.v_oid); case BSON_TYPE_DATE_TIME: return phongo_utcdatetime_new(zv, value->value.v_datetime); case BSON_TYPE_REGEX: return phongo_regex_new(zv, value->value.v_regex.regex, value->value.v_regex.options); case BSON_TYPE_DBPOINTER: return phongo_dbpointer_new(zv, value->value.v_dbpointer.collection, value->value.v_dbpointer.collection_len, &value->value.v_dbpointer.oid); case BSON_TYPE_CODE: return phongo_javascript_new(zv, value->value.v_code.code, value->value.v_code.code_len, NULL); case BSON_TYPE_CODEWSCOPE: if (!bson_init_static(&bson, value->value.v_codewscope.scope_data, value->value.v_codewscope.scope_len)) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Invalid BSON received for BSON_TYPE_CODEWSCOPE"); return false; } return phongo_javascript_new(zv, value->value.v_codewscope.code, value->value.v_codewscope.code_len, &bson); case BSON_TYPE_SYMBOL: return phongo_symbol_new(zv, value->value.v_symbol.symbol, value->value.v_symbol.len); case BSON_TYPE_TIMESTAMP: return phongo_timestamp_new(zv, value->value.v_timestamp.increment, value->value.v_timestamp.timestamp); case BSON_TYPE_DECIMAL128: return phongo_decimal128_new(zv, &value->value.v_decimal128); case BSON_TYPE_MAXKEY: return object_init_ex(zv, php_phongo_maxkey_ce) == SUCCESS; case BSON_TYPE_MINKEY: return object_init_ex(zv, php_phongo_minkey_ce) == SUCCESS; case BSON_TYPE_EOD: ZVAL_NULL(zv); return true; case BSON_TYPE_UNDEFINED: return object_init_ex(zv, php_phongo_undefined_ce) == SUCCESS; case BSON_TYPE_ARRAY: if (!bson_init_static(&bson, value->value.v_doc.data, value->value.v_doc.data_len)) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Invalid BSON received for BSON_TYPE_ARRAY"); return false; } return phongo_packedarray_new(zv, &bson, true); case BSON_TYPE_DOCUMENT: if (!bson_init_static(&bson, value->value.v_doc.data, value->value.v_doc.data_len)) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Invalid BSON received for BSON_TYPE_DOCUMENT"); return false; } return phongo_document_new(zv, &bson, true); default: ZVAL_UNDEF(zv); phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Detected unsupported BSON type %d", value->value_type); return false; } } /* Converts a BSON document to a PHP value according to the typemap specified in * the state argument. * * On success, the result will be set on the state argument and true will be * returned. On error, an exception will have been thrown and false will be * returned. * * Note: the result zval in the state argument will always be initialized for * PHP 5.x so that the caller may always zval_ptr_dtor() it. The zval is left * as-is on PHP 7; however, it should have the type undefined if the state * was initialized to zero. */ bool php_phongo_bson_to_zval_ex(const bson_t* b, php_phongo_bson_state* state) { bson_iter_t iter; bool retval = false; bool must_dtor_state = false; if (!php_phongo_bson_state_is_initialized(state)) { php_phongo_bson_state_ctor(state); must_dtor_state = true; } // Handle BSON root type early to avoid creating an iterator and visiting elements if (state->map.root.type == PHONGO_TYPEMAP_BSON) { zval obj; bson_t** bson; if (state->is_visiting_array) { object_init_ex(&obj, php_phongo_packedarray_ce); bson = &Z_PACKEDARRAY_OBJ_P(&obj)->bson; } else { object_init_ex(&obj, php_phongo_document_ce); bson = &Z_DOCUMENT_OBJ_P(&obj)->bson; } *bson = bson_copy(b); zval_ptr_dtor(&state->zchild); ZVAL_COPY_VALUE(&state->zchild, &obj); retval = true; goto cleanup; } if (!bson_iter_init(&iter, b)) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not initialize BSON iterator"); goto cleanup; } /* We initialize an array because it will either be returned as-is (native * array in type map), passed to bsonUnserialize() (ODM class), or used to * initialize a stdClass object (native object in type map). */ array_init(&state->zchild); if (bson_iter_visit_all(&iter, &php_bson_visitors, state) || iter.err_off) { /* Iteration stopped prematurely due to corruption or a failed visitor. * While we free the reader, state->zchild should be left as-is, since * the calling code may want to zval_ptr_dtor() it. If an exception has * been thrown already (due to an unsupported BSON type for example, * don't overwrite with a generic exception message. */ if (!EG(exception)) { char* path = php_phongo_field_path_as_string(state->field_path); phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Detected corrupt BSON data for field path '%s' at offset %d", path, iter.err_off); efree(path); } goto cleanup; } /* If the root document is an array, default to a native array type. * If php_phongo_bson_visit_binary() found an ODM class, it should supersede * a default type map and named root class. */ if (state->map.root.type == PHONGO_TYPEMAP_NONE) { if (state->is_visiting_array) { state->map.root.type = PHONGO_TYPEMAP_NATIVE_ARRAY; } else if (state->odm_ce) { state->map.root.type = PHONGO_TYPEMAP_CLASS; } } switch (state->map.root.type) { case PHONGO_TYPEMAP_NATIVE_ARRAY: /* Nothing to do here */ break; case PHONGO_TYPEMAP_CLASS: { zval obj; zend_class_entry* obj_ce = state->odm_ce ? state->odm_ce : state->map.root.ce; object_init_ex(&obj, obj_ce); zend_call_method_with_1_params(Z_OBJ_P(&obj), NULL, NULL, BSON_UNSERIALIZE_FUNC_NAME, NULL, &state->zchild); zval_ptr_dtor(&state->zchild); ZVAL_COPY_VALUE(&state->zchild, &obj); break; } case PHONGO_TYPEMAP_NATIVE_OBJECT: default: convert_to_object(&state->zchild); } retval = true; cleanup: if (must_dtor_state) { php_phongo_bson_state_dtor(state); } return retval; } /* Converts a BSON document to a PHP value according to the typemap specified in * the state argument. * * On success, the result will be set on the state argument and true will be * returned. On error, an exception will have been thrown and false will be * returned. * * Note: the result zval in the state argument will always be initialized for * PHP 5.x so that the caller may always zval_ptr_dtor() it. The zval is left * as-is on PHP 7; however, it should have the type undefined if the state * was initialized to zero. */ bool php_phongo_bson_data_to_zval_ex(const unsigned char* data, int data_len, php_phongo_bson_state* state) { bson_reader_t* reader = NULL; const bson_t* b; bool eof = false; bool retval = false; reader = bson_reader_new_from_data(data, data_len); if (!(b = bson_reader_read(reader, NULL))) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not read document from BSON reader"); goto cleanup; } retval = php_phongo_bson_to_zval_ex(b, state); if (bson_reader_read(reader, &eof) || !eof) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Reading document did not exhaust input buffer"); retval = false; goto cleanup; } cleanup: if (reader) { bson_reader_destroy(reader); } return retval; } /* Fetches a zend_class_entry for the given class name and checks that it is * also instantiatable and implements a specified interface. Returns the class * on success; otherwise, NULL is returned and an exception is thrown. */ static zend_class_entry* php_phongo_bson_state_fetch_class(const char* classname, int classname_len, zend_class_entry* interface_ce) { zend_string* zs_classname = zend_string_init(classname, classname_len, 0); zend_class_entry* found_ce = zend_fetch_class(zs_classname, ZEND_FETCH_CLASS_AUTO | ZEND_FETCH_CLASS_SILENT); zend_string_release(zs_classname); if (!found_ce) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Class %s does not exist", classname); } else if (!phongo_is_class_instantiatable(found_ce)) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "%s %s is not instantiatable", zend_get_object_type_uc(found_ce), classname); } else if (!instanceof_function(found_ce, interface_ce)) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "Class %s does not implement %s", classname, ZSTR_VAL(interface_ce->name)); } else { return found_ce; } return NULL; } /* Parses a BSON type (i.e. array, document, or root). On success, the type and * type_ce output arguments will be assigned and true will be returned; * otherwise, false is returned and an exception is thrown. */ static bool php_phongo_bson_state_parse_type(zval* options, const char* name, php_phongo_bson_typemap_element* element) { char* type; int type_len; zend_bool type_free = 0; bool retval = true; type = php_array_fetch_string(options, name, &type_len, &type_free); if (!type_len) { goto cleanup; } if (!strcasecmp(type, PHONGO_TYPEMAP_TYPE_STR_ARRAY)) { element->type = PHONGO_TYPEMAP_NATIVE_ARRAY; element->ce = NULL; } else if (!strcasecmp(type, PHONGO_TYPEMAP_TYPE_STR_BSON)) { element->type = PHONGO_TYPEMAP_BSON; element->ce = NULL; } else if (!strcasecmp(type, PHONGO_TYPEMAP_TYPE_STR_STDCLASS) || !strcasecmp(type, PHONGO_TYPEMAP_TYPE_STR_OBJECT)) { element->type = PHONGO_TYPEMAP_NATIVE_OBJECT; element->ce = NULL; } else { if ((element->ce = php_phongo_bson_state_fetch_class(type, type_len, php_phongo_unserializable_ce))) { element->type = PHONGO_TYPEMAP_CLASS; } else { /* Exception already thrown */ retval = false; } } cleanup: if (type_free) { efree(type); } return retval; } static void field_path_map_element_set_info(php_phongo_field_path_map_element* element, php_phongo_bson_typemap_element* typemap_element) { element->node.type = typemap_element->type; element->node.ce = typemap_element->ce; } static void map_add_field_path_element(php_phongo_bson_typemap* map, php_phongo_field_path_map_element* element) { /* Make sure we have allocated enough */ if (map->field_paths.allocated_size < map->field_paths.size + 1) { map->field_paths.allocated_size += PHONGO_FIELD_PATH_EXPANSION; map->field_paths.map = erealloc(map->field_paths.map, sizeof(php_phongo_field_path_map_element) * map->field_paths.allocated_size); } map->field_paths.map[map->field_paths.size] = element; map->field_paths.size++; } static php_phongo_field_path_map_element* field_path_map_element_alloc(void) { php_phongo_field_path_map_element* tmp = ecalloc(1, sizeof(php_phongo_field_path_map_element)); tmp->entry = php_phongo_field_path_alloc(true); return tmp; } static void field_path_map_element_dtor(php_phongo_field_path_map_element* element) { php_phongo_field_path_free(element->entry); efree(element); } static bool php_phongo_bson_state_add_field_path(php_phongo_bson_typemap* map, char* field_path_original, php_phongo_bson_typemap_element* typemap_element) { char* ptr = NULL; char* segment_end = NULL; php_phongo_field_path_map_element* field_path_map_element; if (field_path_original[0] == '.') { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "A 'fieldPaths' key may not start with a '.'"); return false; } if (field_path_original[strlen(field_path_original) - 1] == '.') { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "A 'fieldPaths' key may not end with a '.'"); return false; } field_path_map_element = field_path_map_element_alloc(); ptr = field_path_original; /* Loop over all the segments. A segment is delimited by a "." */ while ((segment_end = strchr(ptr, '.')) != NULL) { char* tmp = NULL; /* Bail out if we have an empty segment */ if (ptr == segment_end) { field_path_map_element_dtor(field_path_map_element); phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "A 'fieldPaths' key may not have an empty segment"); return false; } tmp = calloc(1, segment_end - ptr + 1); memcpy(tmp, ptr, segment_end - ptr); php_phongo_field_path_push(field_path_map_element->entry, tmp, PHONGO_FIELD_PATH_ITEM_NONE); free(tmp); ptr = segment_end + 1; } /* Add the last (or single) element */ php_phongo_field_path_push(field_path_map_element->entry, ptr, PHONGO_FIELD_PATH_ITEM_NONE); field_path_map_element_set_info(field_path_map_element, typemap_element); map_add_field_path_element(map, field_path_map_element); return true; } void php_phongo_bson_typemap_dtor(php_phongo_bson_typemap* map) { size_t i; if (map->field_paths.map) { for (i = 0; i < map->field_paths.size; i++) { field_path_map_element_dtor(map->field_paths.map[i]); } efree(map->field_paths.map); } map->field_paths.map = NULL; } /* Loops over each element in the fieldPaths array (if exists, and is an * array), and then checks whether each element is a valid type mapping */ static bool php_phongo_bson_state_parse_fieldpaths(zval* typemap, php_phongo_bson_typemap* map) { zval* fieldpaths = NULL; HashTable* ht_data; if (!php_array_existsc(typemap, "fieldPaths")) { return true; } fieldpaths = php_array_fetchc_array(typemap, "fieldPaths"); if (!fieldpaths) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "The 'fieldPaths' element is not an array"); return false; } ht_data = HASH_OF(fieldpaths); { zend_string* string_key = NULL; zend_ulong num_key = 0; zval* property; ZEND_HASH_FOREACH_KEY_VAL(ht_data, num_key, string_key, property) { php_phongo_bson_typemap_element element; if (!string_key) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "The 'fieldPaths' element is not an associative array"); return false; } if (strcmp(ZSTR_VAL(string_key), "") == 0) { phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT, "The 'fieldPaths' element may not be an empty string"); return false; } if (!php_phongo_bson_state_parse_type(fieldpaths, ZSTR_VAL(string_key), &element)) { return false; } if (!php_phongo_bson_state_add_field_path(map, ZSTR_VAL(string_key), &element)) { return false; } } ZEND_HASH_FOREACH_END(); } return true; } /* Applies the array argument to a typemap struct. Returns true on success; * otherwise, false is returned an an exception is thrown. */ bool php_phongo_bson_typemap_to_state(zval* typemap, php_phongo_bson_typemap* map) { if (!typemap) { return true; } if (!php_phongo_bson_state_parse_type(typemap, PHONGO_TYPEMAP_FIELD_STR_ARRAY, &map->array) || !php_phongo_bson_state_parse_type(typemap, PHONGO_TYPEMAP_FIELD_STR_DOCUMENT, &map->document) || !php_phongo_bson_state_parse_type(typemap, PHONGO_TYPEMAP_FIELD_STR_ROOT, &map->root) || !php_phongo_bson_state_parse_fieldpaths(typemap, map)) { /* Exception should already have been thrown */ return false; } return true; } bool php_phongo_bson_to_json(zval* return_value, const bson_t* bson, php_phongo_json_mode_t mode) { char* json = NULL; size_t json_len; if (mode == PHONGO_JSON_MODE_LEGACY) { json = bson_as_legacy_extended_json(bson, &json_len); } else if (mode == PHONGO_JSON_MODE_CANONICAL) { json = bson_as_canonical_extended_json(bson, &json_len); } else if (mode == PHONGO_JSON_MODE_RELAXED) { json = bson_as_relaxed_extended_json(bson, &json_len); } if (!json) { phongo_throw_exception(PHONGO_ERROR_UNEXPECTED_VALUE, "Could not convert BSON document to a JSON string"); return false; } ZVAL_STRINGL(return_value, json, json_len); bson_free(json); return true; }