From b4ebce9546665ffa28bafc85ab57b8c55f0395f8 Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Mon, 29 Jan 2024 12:00:12 +0100 Subject: [PATCH] OCF Bridging feature --- .github/workflows/cmake-linux.yml | 14 +- .github/workflows/cmake-windows.yml | 2 + .github/workflows/coverity.yml | 1 + .github/workflows/sonar-cloud-analysis.yml | 10 +- .github/workflows/static-analysis.yml | 2 +- CMakeLists.txt | 7 + api/oc_bridge.c | 621 ++++++++++++++ api/oc_collection.c | 21 + api/oc_collection_internal.h | 3 + api/oc_core_res.c | 309 ++++++- api/oc_core_res_internal.h | 22 + api/oc_push.c | 9 +- api/oc_ri.c | 73 ++ api/oc_swupdate.c | 31 + api/oc_swupdate_internal.h | 8 + api/oc_vod_map.c | 499 +++++++++++ api/unittest/RITest.cpp | 169 +++- api/unittest/bridgetest.cpp | 298 +++++++ api/unittest/collectiontest.cpp | 57 ++ api/unittest/coreresourcetest.cpp | 113 ++- api/unittest/uuidtest.cpp | 2 +- api/unittest/vodmaptest.cpp | 529 ++++++++++++ apps/CMakeLists.txt | 8 + apps/Readme.md | 6 +- apps/docs/bridging.md | 260 ++++++ .../resources/bridging_implementation.png | Bin 0 -> 75651 bytes .../resources/bridging_vod_statemachine.png | Bin 0 -> 82730 bytes apps/dummy_bridge_bridge_device_IDD.cbor | Bin 0 -> 3392 bytes apps/dummy_bridge_bridge_device_IDD.json | 303 +++++++ apps/dummy_bridge_linux.c | 800 ++++++++++++++++++ apps/dummy_bridge_virtual_light_IDD.cbor | Bin 0 -> 2885 bytes apps/dummy_bridge_virtual_light_IDD.json | 299 +++++++ include/oc_api.h | 1 + include/oc_bridge.h | 271 ++++++ include/oc_core_res.h | 5 + include/oc_ri.h | 26 +- include/oc_vod_map.h | 145 ++++ port/linux/Makefile | 10 + security/oc_acl.c | 50 +- security/oc_acl_internal.h | 14 + security/oc_ael.c | 57 +- security/oc_ael_internal.h | 6 + security/oc_cred.c | 50 +- security/oc_cred_internal.h | 14 + security/oc_doxm.c | 21 + security/oc_doxm_internal.h | 12 + security/oc_pstat.c | 25 + security/oc_pstat_internal.h | 13 + security/oc_sdi.c | 30 +- security/oc_sdi_internal.h | 4 + security/oc_sp.c | 39 + security/oc_sp_internal.h | 4 + security/oc_store.c | 2 +- security/oc_svr.c | 65 ++ security/oc_svr_internal.h | 25 + security/unittest/acltest.cpp | 41 + security/unittest/credtest.cpp | 42 + security/unittest/doxmtest.cpp | 41 + security/unittest/pstattest.cpp | 35 + security/unittest/sditest.cpp | 35 + security/unittest/sptest.cpp | 40 + sonar-project.properties | 2 +- util/oc_features.h | 5 + 63 files changed, 5564 insertions(+), 42 deletions(-) create mode 100644 api/oc_bridge.c create mode 100644 api/oc_vod_map.c create mode 100644 api/unittest/bridgetest.cpp create mode 100644 api/unittest/vodmaptest.cpp create mode 100644 apps/docs/bridging.md create mode 100644 apps/docs/resources/bridging_implementation.png create mode 100644 apps/docs/resources/bridging_vod_statemachine.png create mode 100644 apps/dummy_bridge_bridge_device_IDD.cbor create mode 100644 apps/dummy_bridge_bridge_device_IDD.json create mode 100644 apps/dummy_bridge_linux.c create mode 100644 apps/dummy_bridge_virtual_light_IDD.cbor create mode 100644 apps/dummy_bridge_virtual_light_IDD.json create mode 100644 include/oc_bridge.h create mode 100644 include/oc_vod_map.h diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 03a04dee23..236006ff12 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -70,11 +70,11 @@ jobs: - args: "-DOC_SECURITY_ENABLED=OFF -DOC_TCP_ENABLED=ON -DOC_IPV4_ENABLED=ON" # rep realloc on, ocf 1.1 on - args: "-DOC_REPRESENTATION_REALLOC_ENCODING_ENABLED=ON -DOC_VERSION_1_1_0_ENABLED=ON" - # everything off (dynamic allocation off, secure off, pki off, idd off, oscore off, well-known core resource off, software update off, maintenance resource off, /oic/res observable off, push notifications off, plgd-time off, introspection off, etag off) - - args: "-DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_SECURITY_ENABLED=OFF -DOC_PKI_ENABLED=OFF -DOC_IDD_API_ENABLED=OFF -DOC_OSCORE_ENABLED=OFF -DOC_WKCORE_ENABLED=OFF -DOC_SOFTWARE_UPDATE_ENABLED=OFF -DOC_MNT_ENABLED=OFF -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=OFF -DOC_PUSH_ENABLED=OFF -DPLGD_DEV_TIME_ENABLED=OFF -DOC_INTROSPECTION_ENABLED=OFF -DOC_ETAG_ENABLED=OFF" + # everything off (dynamic allocation off, secure off, pki off, idd off, oscore off, well-known core resource off, software update off, maintenance resource off, /oic/res observable off, push notifications off, plgd-time off, introspection off, etag off, bridging off) + - args: "-DOC_DYNAMIC_ALLOCATION_ENABLED=OFF -DOC_SECURITY_ENABLED=OFF -DOC_PKI_ENABLED=OFF -DOC_IDD_API_ENABLED=OFF -DOC_OSCORE_ENABLED=OFF -DOC_WKCORE_ENABLED=OFF -DOC_SOFTWARE_UPDATE_ENABLED=OFF -DOC_MNT_ENABLED=OFF -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=OFF -DOC_PUSH_ENABLED=OFF -DPLGD_DEV_TIME_ENABLED=OFF -DOC_INTROSPECTION_ENABLED=OFF -DOC_ETAG_ENABLED=OFF -DOC_BRIDGE_ENABLED=OFF" uses: ./.github/workflows/unit-test-with-cfg.yml with: - build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON ${{ matrix.args }} + build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_BRIDGE_ENABLED=ON ${{ matrix.args }} build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} clang: ${{ github.event_name == 'workflow_dispatch' && inputs.clang }} coverage: false @@ -90,7 +90,7 @@ jobs: - args: "-DOC_DEBUG_ENABLED=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON" uses: ./.github/workflows/unit-test-with-cfg.yml with: - build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DBUILD_MBEDTLS_FORCE_3_5_0=ON ${{ matrix.args }} + build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_BRIDGE_ENABLED=ON -DBUILD_MBEDTLS_FORCE_3_5_0=ON ${{ matrix.args }} build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} clang: ${{ github.event_name == 'workflow_dispatch' && inputs.clang }} coverage: false @@ -100,8 +100,8 @@ jobs: cmake_linux_preinstalled: uses: ./.github/workflows/unit-test-with-cfg.yml with: - # cloud on (ipv4+tcp on), collections create on, maintenance resource on, well-known core resource on, software update on, /oic/res observable on, push notification on, plgd-time on, etag on - build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON + # cloud on (ipv4+tcp on), collections create on, maintenance resource on, well-known core resource on, software update on, /oic/res observable on, push notification on, plgd-time on, etag on, bridging on + build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_BRIDGE_ENABLED=ON build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} clang: ${{ github.event_name == 'workflow_dispatch' && inputs.clang }} coverage: false @@ -138,7 +138,7 @@ jobs: # install_faketime: true uses: ./.github/workflows/unit-test-with-cfg.yml with: - build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON ${{ matrix.args }} + build_args: -DOC_LOG_MAXIMUM_LOG_LEVEL=INFO -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_BRIDGE_ENABLED=ON ${{ matrix.args }} build_type: ${{ (github.event_name == 'workflow_dispatch' && inputs.build_type) || 'Debug' }} clang: ${{ ((github.event_name == 'workflow_dispatch' && inputs.clang) || matrix.clang) || false }} coverage: false diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 6a6cce2da1..79df5c53e6 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -92,6 +92,7 @@ jobs: -D OC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -D PLGD_DEV_TIME_ENABLED=ON -D OC_ETAG_ENABLED=ON + -D OC_BRIDGE_ENABLED=ON -D OC_DEBUG_ENABLED=ON COMMAND_ERROR_IS_FATAL ANY ) @@ -184,6 +185,7 @@ jobs: -D OC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON \ -D PLGD_DEV_TIME_ENABLED=ON \ -D OC_ETAG_ENABLED=ON \ + -D OC_BRIDGE_ENABLED=ON \ -D OC_DEBUG_ENABLED=ON \ ${{ matrix.build_args }} diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 6e47acbf6c..065346766e 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -29,6 +29,7 @@ jobs: -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_JSON_ENCODER_ENABLED=ON + -DOC_BRIDGE_ENABLED=ON -B ${{github.workspace}}/build - uses: vapier/coverity-scan-action@v1 diff --git a/.github/workflows/sonar-cloud-analysis.yml b/.github/workflows/sonar-cloud-analysis.yml index aa9a30ac30..3985faca7c 100644 --- a/.github/workflows/sonar-cloud-analysis.yml +++ b/.github/workflows/sonar-cloud-analysis.yml @@ -25,10 +25,10 @@ jobs: fail-fast: false matrix: include: - # cloud (ipv4+tcp) on, collection create on, push on, rfotm on - - build_args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON" - # security off, ipv4 on, collection create on, push on, rfotm on - - build_args: "-DOC_SECURITY_ENABLED=OFF -DOC_IPV4_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON" + # cloud (ipv4+tcp) on, collection create on, push on, rfotm on, bridging on + - build_args: "-DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DOC_BRIDGE_ENABLED=ON" + # security off, ipv4 on, collection create on, push on, rfotm on, bridging on + - build_args: "-DOC_SECURITY_ENABLED=OFF -DOC_IPV4_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_BRIDGE_ENABLED=ON " # ipv6 dns on, oscore off, rep realloc on, json encoder on - build_args: "-DOC_DNS_LOOKUP_IPV6_ENABLED=ON -DOC_OSCORE_ENABLED=OFF -DOC_REPRESENTATION_REALLOC_ENCODING_ENABLED=ON -DOC_JSON_ENCODER_ENABLED=ON" # ipv4 on, tcp on, dynamic allocation off, rfotm on, push off (because it forces dynamic allocation) @@ -106,7 +106,7 @@ jobs: mkdir build && cd build # sonar-scanner currently cannot handle multi configuration configuration (ie. compilation of the same file with different defines), # so we enable as many features as possible so we get max. amount of code analysis - cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_JSON_ENCODER_ENABLED=ON -DBUILD_TESTING=ON .. + cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_VERBOSE_MAKEFILE=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_JSON_ENCODER_ENABLED=ON -DOC_BRIDGE_ENABLED=ON -DBUILD_TESTING=ON .. cd .. # for files defined in multiple cmake targets, sonar-scanner seems to take the configuration from the first compilation of the file, # so we force client-server target to be compiled first so we get analysis of code with both OC_CLIENT and OC_SERVER enabled diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 5c2b689018..45335470ab 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -37,5 +37,5 @@ jobs: - name: Build with clang and analyze with clang-tidy run: | mkdir build && cd build - cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DOC_CLANG_TIDY_ENABLED=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DBUILD_TESTING=OFF .. + cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DOC_CLANG_TIDY_ENABLED=ON -DOC_CLOUD_ENABLED=ON -DOC_COLLECTIONS_IF_CREATE_ENABLED=ON -DOC_MNT_ENABLED=ON -DOC_WKCORE_ENABLED=ON -DOC_SOFTWARE_UPDATE_ENABLED=ON -DOC_DISCOVERY_RESOURCE_OBSERVABLE_ENABLED=ON -DOC_PUSH_ENABLED=ON -DOC_RESOURCE_ACCESS_IN_RFOTM_ENABLED=ON -DPLGD_DEV_TIME_ENABLED=ON -DOC_ETAG_ENABLED=ON -DOC_BRIDGE_ENABLED=ON -DBUILD_TESTING=OFF .. cmake --build . diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d74677016..38b13ef806 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ set(OC_IPV4_ENABLED OFF CACHE BOOL "Enable IPv4 support.") set(OC_DNS_LOOKUP_IPV6_ENABLED OFF CACHE BOOL "Enable IPv6 DNS lookup.") set(OC_PUSH_ENABLED OFF CACHE BOOL "Enable Push Notification.") set(OC_PUSHDEBUG_ENABLED OFF CACHE BOOL "Enable debug messages for Push Notification.") +set(OC_BRIDGE_ENABLED OFF CACHE BOOL "Enable Bridge Support.") set(OC_RESOURCE_ACCESS_IN_RFOTM_ENABLED OFF CACHE BOOL "Enable resource access in RFOTM.") set(OC_MEMORY_TRACE_ENABLED OFF CACHE BOOL "Enable memory tracing.") if (OC_DEBUG_ENABLED) @@ -311,6 +312,12 @@ if(OC_PUSH_ENABLED) set(OC_DYNAMIC_ALLOCATION_ENABLED ON) set(OC_COLLECTIONS_IF_CREATE_ENABLED ON) endif() + +if(OC_BRIDGE_ENABLED) + list(APPEND PUBLIC_COMPILE_DEFINITIONS "OC_BRIDGE") + set(OC_DYNAMIC_ALLOCATION_ENABLED ON) +endif() + if(OC_RESOURCE_ACCESS_IN_RFOTM_ENABLED) if(NOT OC_SECURITY_ENABLED) message(FATAL_ERROR "Cannot enable resource access in RFOTM without security") diff --git a/api/oc_bridge.c b/api/oc_bridge.c new file mode 100644 index 0000000000..b8321ff7e7 --- /dev/null +++ b/api/oc_bridge.c @@ -0,0 +1,621 @@ +/****************************************************************** + * + * Copyright 2020 Intel Corporation + * Copyright 2023 ETRI Joo-Chul Kevin Lee (rune@etri.re.kr) + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_BRIDGE + +#include "oc_bridge.h" +#include "oc_api.h" +#include "oc_core_res.h" +#include "oc_core_res_internal.h" +#include +#include "port/oc_log_internal.h" +#include "port/oc_assert.h" + +#ifdef OC_SECURITY +#include "oc_store.h" +#endif // OC_SECURITY + +OC_LIST(g_vods); +static oc_resource_t *g_vodlist_res; + +static void +print_vodlist(void) +{ +#if OC_DBG_IS_ENABLED + OC_PRINTF("\"vods\": ["); + oc_vods_t *print_vod_item = (oc_vods_t *)oc_list_head(g_vods); + while (print_vod_item) { + OC_PRINTF(" {"); + OC_PRINTF(" \"n\": \"%s\"", oc_string(print_vod_item->name)); + char di_uuid[OC_UUID_LEN]; + oc_uuid_to_str(&print_vod_item->di, di_uuid, OC_UUID_LEN); + OC_PRINTF(" \"di\": \"%s\"", di_uuid); + OC_PRINTF(" \"econame\": \"%s\"", oc_string(print_vod_item->econame)); + if (print_vod_item->next) { + OC_PRINTF(" },"); + } else { + OC_PRINTF(" }"); + } + print_vod_item = print_vod_item->next; + } +#endif +} + +static bool +oc_bridge_is_virtual_device(size_t device_index) +{ + oc_resource_t *r = oc_core_get_resource_by_index(OCF_D, device_index); + for (size_t i = 0; i < oc_string_array_get_allocated_size(r->types); ++i) { + if (strncmp(oc_string_array_get_item(r->types, i), "oic.d.virtual", + strlen("oic.d.virtual")) == 0) { + return true; + } + } + return false; +} + +#ifdef OC_SECURITY +static void +add_virtual_device_to_vods_list(const char *name, const oc_uuid_t *di, + const char *econame) +{ + oc_vods_t *vod = (oc_vods_t *)malloc(sizeof(oc_vods_t)); + oc_new_string(&vod->name, name, strlen(name)); + memcpy(&vod->di, di, sizeof(oc_uuid_t)); + oc_new_string(&vod->econame, econame, strlen(econame)); + + /* mark this vod is online... */ + oc_virtual_device_t *vod_mapping_item = oc_bridge_get_vod_mapping_info2(vod); + if (vod_mapping_item) { + vod_mapping_item->is_vod_online = true; + } else { + char uuid[OC_UUID_LEN]; + oc_uuid_to_str(&vod->di, uuid, OC_UUID_LEN); + OC_DBG("oc_bridge: failed to find Device whose ID is (%s)", uuid); + } + + oc_list_add(g_vods, vod); + + OC_DBG("=====> oc_bridge: adding %s [%s] from oic.r.vodlist", name, econame); + print_vodlist(); +} +#endif // OC_SECURITY + +/* + * remove VOD from `oic.r.vodlist` Resource + */ +static void +remove_virtual_device_from_vods_list(const oc_uuid_t *di) +{ + oc_vods_t *vod_item = (oc_vods_t *)oc_list_head(g_vods); + while (vod_item) { + if (memcmp(&vod_item->di, di, 16) == 0) { + + /* mark this vod is offline */ + oc_virtual_device_t *vod_mapping_item = + oc_bridge_get_vod_mapping_info2(vod_item); + if (vod_mapping_item) { + vod_mapping_item->is_vod_online = false; + } else { + char uuid[OC_UUID_LEN]; + oc_uuid_to_str(&vod_item->di, uuid, OC_UUID_LEN); + OC_ERR("oc_bridge: failed to find Device whose ID is (%s)", uuid); + } + + oc_list_remove(g_vods, vod_item); + OC_DBG("=====> oc_bridge: removing %s [%s] from oic.r.vodlist", + oc_string(vod_item->name), oc_string(vod_item->econame)); + oc_free_string(&vod_item->name); + oc_free_string(&vod_item->econame); + + free(vod_item); + break; + } + vod_item = vod_item->next; + } + print_vodlist(); +} + +static void +get_bridge(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + (void)user_data; + oc_rep_start_root_object(); + switch (iface_mask) { + case OC_IF_BASELINE: + oc_process_baseline_interface(request->resource); + /* fall through */ + case OC_IF_R: + oc_rep_set_array(root, vods); + char di_str[OC_UUID_LEN]; + oc_vods_t *vod_item = (oc_vods_t *)oc_list_head(g_vods); + while (vod_item) { + oc_rep_object_array_begin_item(vods); + oc_rep_set_text_string(vods, n, oc_string(vod_item->name)); + oc_uuid_to_str(&vod_item->di, di_str, OC_UUID_LEN); + oc_rep_set_text_string(vods, di, di_str); + oc_rep_set_text_string(vods, econame, oc_string(vod_item->econame)); + oc_rep_object_array_end_item(vods); + vod_item = vod_item->next; + } + oc_rep_close_array(root, vods); + break; + default: + break; + } + oc_rep_end_root_object(); + oc_send_response(request, OC_STATUS_OK); +} + +#ifdef OC_SECURITY +static void +_handle_owned_bridge(size_t device_index) +{ + /* + * walk all devices + * if device is unowned and a virtual device then call connection_init + * assumption all virtual devices have a higher device index than bridge + */ + for (size_t device = device_index + 1; device < oc_core_get_num_devices(); + ++device) { + if (oc_uuid_is_empty(oc_core_get_device_info(device)->di)) { + continue; + } + if (!oc_is_owned_device(device) && oc_bridge_is_virtual_device(device)) { + oc_connectivity_ports_t ports; + memset(&ports, 0, sizeof(ports)); + + OC_DBG( + "=====> Bridge is owned, VOD %zu connection is being initialized!!", + device); + + if (oc_connectivity_init(device, ports) < 0) { + oc_abort("error initializing connectivity for device"); + } + OC_DBG("======> oc_bridge: init connectivity for virtual device %zu", + device); + } + } +} + +static void +_handle_unowned_bridge(size_t device_index) +{ + /* + * Reset all virtual device information. + * walk all devices + * if device is a virtual device call reset and connection_shutdown + * reset the vod_map + * assumption all virtual devices have a higher device index than bridge + */ + for (size_t device = device_index + 1; device < oc_core_get_num_devices(); + ++device) { + if (oc_bridge_is_virtual_device(device)) { + oc_virtual_device_t *vod_mapping_item = + oc_bridge_get_vod_mapping_info(device); + if (vod_mapping_item) { + vod_mapping_item->is_vod_online = false; + } + + oc_reset_device(device); + oc_connectivity_shutdown(device); + } + } +#if 0 + /* TODO4ME: add way to remove virtual device before reseting the vod_map */ + oc_vod_map_reset(); + OC_DBG("oc_bridge: bridge reset, reseting all connected virtual devices"); +#endif +} + +static void +_handle_owned_vod(const oc_uuid_t *device_uuid, size_t device_index) +{ + /* + * if corresponding non-OCF device is still in paired + * while this VOD is offboard and onboard again. + * + * the device ID of corresponding OCF Device stored in non-OCF + * device cache could point wrong OCF Device.. + * + * => NOP!!!. onboard/offboard DON"T delete oc_device_info_t + * from g_oc_device_info[] array!! + */ + if (oc_bridge_is_virtual_device(device_index)) { + oc_device_info_t *device_info = oc_core_get_device_info(device_index); + oc_string_t econame; + oc_vod_map_get_econame(&econame, device_index); + add_virtual_device_to_vods_list(oc_string(device_info->name), device_uuid, + oc_string(econame)); + OC_DBG("======> oc_bridge: adding %s [%s] to oic.r.vodslist", + oc_string(device_info->name), oc_string(econame)); + } +} + +/* + * For bridging the doxm_owned_changed callback is responsible for two tasks: + * 1. Making sure unowned VODs connect or disconnect from the network based + * on the doxm status of the bridge device + * 2. Updating the oic.r.vodslist when ownership status of the virtual devices + * is change + */ +static void +doxm_owned_changed(const oc_uuid_t *device_uuid, size_t device_index, + bool owned, void *user_data) +{ + (void)user_data; + /* Bridge Device */ + if (g_vodlist_res->device == device_index) { + if (owned) { + _handle_owned_bridge(device_index); + } else { + /* Bridge device is unowned */ + /* + * Reset all virtual device information. + * walk all devices + * if device is a virtual device call reset and connection_shutdown + * reset the vod_map + * assumption all virtual devices have a higher device index than bridge + */ + _handle_unowned_bridge(device_index); + } + } else { + /* Device other than Bridge Device */ + if (owned) { + /* + * if corresponding non-OCF device is still in paired + * while this VOD is offboard and onboard again. + * + * the device ID of corresponding OCF Device stored in non-OCF + * device cache could point wrong OCF Device.. + * + * => NOP!!!. onboard/offboard DON"T delete oc_device_info_t + * from g_oc_device_info[] array!! + */ + _handle_owned_vod(device_uuid, device_index); + } else { + /* + * attempt to remove the unowned device from the vods_list if the uuid + * does not exist the on the vods list nothing will happen. + */ + remove_virtual_device_from_vods_list(device_uuid); + } + /* notify any observers that the vodslist has been updated */ + if (oc_is_owned_device(g_vodlist_res->device)) { + oc_notify_observers(g_vodlist_res); + } + } +} + +#ifdef OC_TEST +void bridge_owned_changed(const oc_uuid_t *device_uuid, size_t device_index, + bool owned, void *user_data); + +void +bridge_owned_changed(const oc_uuid_t *device_uuid, size_t device_index, + bool owned, void *user_data) +{ + doxm_owned_changed(device_uuid, device_index, owned, user_data); +} +#endif // OC_TEST +#endif // OC_SECURITY + +int +oc_bridge_add_bridge_device(const char *name, const char *spec_version, + const char *data_model_version, + oc_add_device_cb_t add_device_cb, void *data) +{ + int ret_value = oc_add_device("/oic/d", "oic.d.bridge", name, spec_version, + data_model_version, add_device_cb, data); + if (ret_value != 0) { + return ret_value; + } + + size_t bridge_device_index = oc_core_get_num_devices() - 1; + + g_vodlist_res = + oc_new_resource(name, "/bridge/vodlist", 1, bridge_device_index); + oc_resource_bind_resource_type(g_vodlist_res, "oic.r.vodlist"); + oc_resource_bind_resource_interface(g_vodlist_res, OC_IF_R); + oc_resource_set_default_interface(g_vodlist_res, OC_IF_R); + oc_resource_set_discoverable(g_vodlist_res, true); + /* + * TODO4ME <2023/7/24> do we need to make the oic.r.vodlist periodic + * observable? oc_resource_set_periodic_observable(g_vodlist_res, 30); + */ + oc_resource_set_request_handler(g_vodlist_res, OC_GET, get_bridge, NULL); + if (!oc_add_resource(g_vodlist_res)) { + return -1; + } + + /* + * - initialize VOD mapping list : `g_vod_mapping_list.vods` + * - initialize `g_vod_mapping_list.next_index` with `g_device_count` + * - load existing `g_vod_mapping_list` from disk + */ + oc_vod_map_init(); + +#ifdef OC_SECURITY + oc_add_ownership_status_cb(&doxm_owned_changed, NULL); +#endif // OC_SECURITY + return 0; +} + +size_t +oc_bridge_add_virtual_device(const uint8_t *virtual_device_id, + size_t virtual_device_id_size, const char *econame, + const char *uri, const char *rt, const char *name, + const char *spec_version, + const char *data_model_version, + oc_add_device_cb_t add_device_cb, void *data) +{ + /* + * add new VOD mapping entry (identified by vod_id) to the proper position of + * `oc_vod_mapping_list_t.vods` list, and update + * `g_vod_mapping_list.next_index` + * + * vd_index : index of `g_oc_device_info[]` which new Device for the VOD + * will be stored. + */ + size_t vd_index = oc_vod_map_add_mapping_entry( + virtual_device_id, virtual_device_id_size, econame); + + oc_add_new_device_t cfg = { + .uri = uri, + .rt = rt, + .name = name, + .spec_version = spec_version, + .data_model_version = data_model_version, + .add_device_cb = add_device_cb, + .add_device_cb_data = data, + }; + + /* + * add corresponding new Device (`oc_device_info_t`) to + * `g_oc_device_info[vd_index]` + */ + oc_device_info_t *device = oc_core_add_new_device_at_index(cfg, vd_index); + + if (!device) { + return 0; + } + + /* + * FIXME4ME <2023/12/11> oc_bridge_add_virtual_device() : do we need this + * code? + */ + if (oc_uuid_is_empty(device->piid)) { + oc_gen_uuid(&device->piid); +#ifdef OC_SECURITY + oc_sec_dump_unique_ids(vd_index); +#endif /* OC_SECURITY */ + } + /* + * According to the security specification: + * An Unowned VOD shall not accept DTLS connection attempts nor TLS connection + * attempts nor any other requests, including discovery requests, while the + * Bridge (that created that VOD) is Unowned. + * + * For that reason only init connectivity if the bridge device is owned or + * if the virtual device is already owned. + * + * The `doxm_owned_changed` callback is responsible for calling + * oc_connectivity_init and oc_connectivity_shutdown for virtual devices + * when the ownership of the bridge device changes. + */ +#ifdef OC_SECURITY + if (oc_is_owned_device(g_vodlist_res->device) || + oc_is_owned_device(vd_index)) { + oc_connectivity_ports_t ports; + memset(&ports, 0, sizeof(ports)); + if (oc_connectivity_init(vd_index, ports) < 0) { + oc_abort("error initializing connectivity for device"); + } + OC_DBG("=====> oc_bridge: init connectivity for virtual device %zu", + vd_index); + } +#else + oc_connectivity_ports_t ports; + memset(&ports, 0, sizeof(ports)); + if (oc_connectivity_init(vd_index, ports) < 0) { + oc_abort("error initializing connectivity for device"); + } +#endif /* OC_SECURITY */ + + oc_device_bind_resource_type(vd_index, "oic.d.virtual"); + +#ifdef OC_SECURITY + if (oc_is_owned_device(vd_index)) { + add_virtual_device_to_vods_list(name, oc_core_get_device_id(vd_index), + econame); + oc_notify_observers(g_vodlist_res); + } +#endif // OC_SECURITY + return vd_index; +} + +/* + * @brief add new vodentry for an existing VOD to "oic.r.vodlist:vods". + * This function is usually called after + * `oc_bridge_remove_virtual_device()` is called. This function DOES NOT add new + * Device to `g_oc_device_info[]`, but just re-registre existing VOD to + * "oic.r.vodlist:vods" list. + * + * @param device_index + * @return 0: success, -1: failure + */ +int +oc_bridge_add_vod(size_t device_index) +{ + oc_device_info_t *device; + oc_virtual_device_t *vod_mapping_item; + + vod_mapping_item = oc_bridge_get_vod_mapping_info(device_index); + if (!vod_mapping_item) { + OC_ERR("oc_bridge: failed to find VOD mapping entry which is corresponding " + "to the Device (device index: %zu)", + device_index); + return -1; + } + + device = oc_core_get_device_info(device_index); + if (!device) { + OC_ERR("oc_bridge: failed to find Device whose index is %zu", device_index); + return -1; + } + +#ifdef OC_SECURITY + if (oc_is_owned_device(g_vodlist_res->device) || + oc_is_owned_device(device_index)) { + oc_connectivity_ports_t ports; + memset(&ports, 0, sizeof(ports)); + if (oc_connectivity_init(device_index, ports) < 0) { + oc_abort("error initializing connectivity for device"); + } + OC_DBG("oc_bridge: init connectivity for virtual device %zu", device_index); + } +#else + oc_connectivity_ports_t ports; + memset(&ports, 0, sizeof(ports)); + if (oc_connectivity_init(device_index, ports) < 0) { + oc_abort("error initializing connectivity for device"); + } +#endif /* OC_SECURITY */ + +#ifdef OC_SECURITY + if (oc_is_owned_device(device_index)) { + add_virtual_device_to_vods_list(oc_string(device->name), + oc_core_get_device_id(device_index), + oc_string(vod_mapping_item->econame)); + oc_notify_observers(g_vodlist_res); + } +#endif // OC_SECURITY + + return 0; +} + +int +oc_bridge_remove_virtual_device(size_t device_index) +{ + if (oc_bridge_is_virtual_device(device_index)) { + remove_virtual_device_from_vods_list(oc_core_get_device_id(device_index)); + oc_connectivity_shutdown(device_index); + return 0; + } + return -1; +} + +int +oc_bridge_delete_virtual_device(size_t device_index) +{ + /* 1. remove this from oic.r.vodlist:vods */ + oc_bridge_remove_virtual_device(device_index); + + /* 2. destroy Device and remove from VOD mapping list */ + if (oc_bridge_is_virtual_device(device_index)) { + oc_uuid_t nil_uuid = { { 0 } }; + oc_set_immutable_device_identifier(device_index, &nil_uuid); + oc_core_remove_device_at_index(device_index); + oc_vod_map_remove_mapping_entry(device_index); + return 0; + } + return -1; +} + +size_t +oc_bridge_get_virtual_device_index(const uint8_t *virtual_device_id, + size_t virtual_device_id_size, + const char *econame) +{ + return oc_vod_map_get_vod_index(virtual_device_id, virtual_device_id_size, + econame); +} + +oc_virtual_device_t * +oc_bridge_get_vod_mapping_info(size_t virtual_device_index) +{ + return oc_vod_map_get_mapping_entry(virtual_device_index); +} + +oc_virtual_device_t * +oc_bridge_get_vod_mapping_info2(const oc_vods_t *vod) +{ + /* find corresponding VOD mapping entry */ + size_t device_index; + if (!oc_core_get_device_index(vod->di, &device_index)) { + char uuid[OC_UUID_LEN]; + oc_uuid_to_str(&vod->di, uuid, OC_UUID_LEN); + OC_ERR("oc_bridge: failed to find Device whose ID is (%s)", uuid); + return NULL; + } + + return oc_vod_map_get_mapping_entry(device_index); +} + +/* + * @brief return entry of "oic.r.vodlist:vods" list + * @param di Device id of the VOD to be returned + * @return VOD entry [oc_vods_t] + */ +oc_vods_t * +oc_bridge_get_vod(oc_uuid_t di) +{ + oc_vods_t *item; + + item = (oc_vods_t *)oc_list_head(g_vods); + + while (item) { + if (oc_uuid_is_equal(item->di, di)) { + return item; + } + item = item->next; + } + + return NULL; +} + +oc_vods_t * +oc_bridge_get_vod_list(void) +{ + return oc_list_head(g_vods); +} + +void +oc_bridge_print_device_list(void) +{ + size_t device_count = oc_core_get_num_devices(); + char di[OC_UUID_LEN]; + char piid[OC_UUID_LEN]; + + for (size_t i = 0; i < device_count; i++) { + oc_uuid_to_str(&oc_core_get_device_info(i)->di, di, OC_UUID_LEN); + oc_uuid_to_str(&oc_core_get_device_info(i)->piid, piid, OC_UUID_LEN); + printf("[ Device Index : %zu ]\n |_ Device ID: %s\n |_ PIID: %s\n |_ " + "Name: %s\n |_ ICV: %s\n |_ DMV: %s\n |_ Enable: %d\n", + i, di, piid, oc_string(oc_core_get_device_info(i)->name), + oc_string(oc_core_get_device_info(i)->icv), + oc_string(oc_core_get_device_info(i)->dmv), + !oc_core_get_device_info(i)->is_removed); + } +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ diff --git a/api/oc_collection.c b/api/oc_collection.c index 6d78c16460..547f47d6e6 100644 --- a/api/oc_collection.c +++ b/api/oc_collection.c @@ -158,6 +158,27 @@ oc_collections_free_all(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_collections_free_per_device(size_t device) +{ + oc_collection_t *col = (oc_collection_t *)oc_list_head(g_collections); + oc_collection_t *t; + + while (col) { + if (col->res.device == device) { + OC_DBG("found collection (\"%s\") for %zu", oc_string(col->res.name), + device); + t = col; + col = (oc_collection_t *)(col->res.next); + collection_free(t, false); + continue; + } + col = (oc_collection_t *)(col->res.next); + } +} +#endif + void oc_collection_notify_resource_changed(oc_collection_t *collection, bool discoveryBatchDispatch) diff --git a/api/oc_collection_internal.h b/api/oc_collection_internal.h index e380c75e9a..a8326036dd 100644 --- a/api/oc_collection_internal.h +++ b/api/oc_collection_internal.h @@ -79,6 +79,9 @@ oc_collection_t *oc_collection_get_all(void); /** @brief Free all collections from the global list */ void oc_collections_free_all(void); +/** @brief Free all collections bound to a device */ +void oc_collections_free_per_device(size_t device); + /** @brief Iterate the global list of colletions and return the next collection * linked with the given resource */ oc_collection_t *oc_get_next_collection_with_link(const oc_resource_t *resource, diff --git a/api/oc_core_res.c b/api/oc_core_res.c index d59d8ee243..d81064dd83 100644 --- a/api/oc_core_res.c +++ b/api/oc_core_res.c @@ -39,6 +39,15 @@ #include "util/oc_macros_internal.h" #include "util/oc_secure_string_internal.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_acl.h" +#include "oc_cred.h" +#ifdef OC_SECURITY +#include "security/oc_ael_internal.h" +#include "security/oc_svr_internal.h" +#endif /* OC_SECURITY*/ +#endif /* OC_HAS_FEATURE_BRIDGE */ + #ifdef OC_CLOUD #include "api/cloud/oc_cloud_resource_internal.h" #endif /* OC_CLOUD */ @@ -105,6 +114,7 @@ oc_core_init(void) #endif /* OC_DYNAMIC_ALLOCATION */ } +#ifdef OC_DYNAMIC_ALLOCATION static void oc_core_free_device_info_properties(oc_device_info_t *oc_device_info_item) { @@ -114,6 +124,7 @@ oc_core_free_device_info_properties(oc_device_info_t *oc_device_info_item) oc_free_string(&(oc_device_info_item->dmv)); } } +#endif /* OC_DYNAMIC_ALLOCATION */ void oc_core_shutdown(void) @@ -121,19 +132,10 @@ oc_core_shutdown(void) oc_platform_deinit(); uint32_t device_count = OC_ATOMIC_LOAD32(g_device_count); -#ifdef OC_DYNAMIC_ALLOCATION - if (g_oc_device_info != NULL) { -#endif /* OC_DYNAMIC_ALLOCATION */ - for (uint32_t i = 0; i < device_count; ++i) { - oc_device_info_t *oc_device_info_item = &g_oc_device_info[i]; - oc_core_free_device_info_properties(oc_device_info_item); - } -#ifdef OC_DYNAMIC_ALLOCATION - free(g_oc_device_info); - g_oc_device_info = NULL; - } -#endif /* OC_DYNAMIC_ALLOCATION */ + /* + * 1. Removed All Core Resources + */ #ifdef OC_DYNAMIC_ALLOCATION if (g_core_resources != NULL) { #endif /* OC_DYNAMIC_ALLOCATION */ @@ -142,12 +144,39 @@ oc_core_shutdown(void) (OC_NUM_CORE_LOGICAL_DEVICE_RESOURCES * device_count); ++i) { oc_resource_t *core_resource = &g_core_resources[i]; - oc_ri_free_resource_properties(core_resource); + +#ifdef OC_HAS_FEATURE_BRIDGE + if ((i < OC_NUM_CORE_PLATFORM_RESOURCES) || + (oc_core_get_device_info((i - OC_NUM_CORE_PLATFORM_RESOURCES) / + OC_NUM_CORE_LOGICAL_DEVICE_RESOURCES) + ->is_removed == false)) { +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_ri_free_resource_properties(core_resource); +#ifdef OC_HAS_FEATURE_BRIDGE + } +#endif /* OC_HAS_FEATURE_BRIDGE */ } #ifdef OC_DYNAMIC_ALLOCATION free(g_core_resources); g_core_resources = NULL; } + + /* + * 2. Removed All Devices + */ +#ifdef OC_DYNAMIC_ALLOCATION + if (g_oc_device_info != NULL) { +#endif /* OC_DYNAMIC_ALLOCATION */ + for (uint32_t i = 0; i < device_count; ++i) { + oc_device_info_t *oc_device_info_item = &g_oc_device_info[i]; + oc_core_free_device_info_properties(oc_device_info_item); + } +#ifdef OC_DYNAMIC_ALLOCATION + free(g_oc_device_info); + g_oc_device_info = NULL; + } +#endif /* OC_DYNAMIC_ALLOCATION */ + #endif /* OC_DYNAMIC_ALLOCATION */ OC_ATOMIC_STORE32(g_device_count, 0); } @@ -328,6 +357,32 @@ oc_create_device_resource(size_t device_count, const char *uri, const char *rt) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +core_update_existing_device_data(size_t device_count, oc_add_new_device_t cfg) +{ + oc_gen_uuid(&g_oc_device_info[device_count].di); + oc_gen_uuid(&g_oc_device_info[device_count].piid); + + oc_new_string(&g_oc_device_info[device_count].name, cfg.name, + strlen(cfg.name)); + oc_new_string(&g_oc_device_info[device_count].icv, cfg.spec_version, + strlen(cfg.spec_version)); + oc_new_string(&g_oc_device_info[device_count].dmv, cfg.data_model_version, + strlen(cfg.data_model_version)); + g_oc_device_info[device_count].add_device_cb = cfg.add_device_cb; + g_oc_device_info[device_count].data = cfg.add_device_cb_data; +} + +static void +core_set_device_removed(size_t index, bool is_removed) +{ + g_oc_device_info[index].is_removed = is_removed; + + return; +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_device_info_t * oc_core_add_new_device(oc_add_new_device_t cfg) { @@ -390,6 +445,10 @@ oc_core_add_new_device(oc_add_new_device_t cfg) oc_abort("error initializing connectivity for device"); } +#ifdef OC_HAS_FEATURE_BRIDGE + core_set_device_removed(device_count, false); +#endif + return &g_oc_device_info[device_count]; } @@ -408,6 +467,230 @@ oc_core_get_device_index(oc_uuid_t di, size_t *device) return false; } +#ifdef OC_HAS_FEATURE_BRIDGE +oc_device_info_t * +oc_core_add_new_device_at_index(oc_add_new_device_t cfg, size_t index) +{ + assert(cfg.uri != NULL); + assert(cfg.rt != NULL); + assert(cfg.name != NULL); + assert(cfg.spec_version != NULL); + assert(cfg.data_model_version != NULL); + + uint32_t device_count = OC_ATOMIC_LOAD32(g_device_count); + +#if defined(OC_SECURITY) || defined(OC_SOFTWARE_UPDATE) + bool is_realloc = false; +#endif + + if (index > device_count) { + OC_ERR("designated device index (%d) is bigger than current number of all " + "Devices", + device_count); + return NULL; + } else if (index < device_count) { + /* + * If an existing Device is being replaced with new Device.. + * - check if the Device on designated `index` is still alive or removed + * before. + */ + if (g_oc_device_info[index].is_removed == false) { + OC_ERR("Trying to replace existing normal Device with new one...! \ + To insert new Device in the middle of the g_oc_device_info[], \ + remove the existing one first"); + return NULL; + } + + /* store new `oc_device_info_t` entry to existing memory slot */ + core_update_existing_device_data(index, cfg); + device_count = (uint32_t)index; + } else if (index == device_count) { + /* + * if `index` is same as the next normal index of Device, + * follow normal procedure. + */ + bool exchanged = false; + while (!exchanged) { +#ifndef OC_DYNAMIC_ALLOCATION + if (device_count == OC_MAX_NUM_DEVICES) { + OC_ERR("device limit reached"); + return NULL; + } +#endif /* !OC_DYNAMIC_ALLOCATION */ + if ((uint64_t)device_count == (uint64_t)MIN(SIZE_MAX, UINT32_MAX)) { + OC_ERR("limit of value type of g_device_count reached"); + return NULL; + } + /* store (device_count+1) to g_device_count */ + OC_ATOMIC_COMPARE_AND_SWAP32(g_device_count, device_count, + device_count + 1, exchanged); + } + + /* extend memory allocated to `g_oc_device_info` to add new Device + * and add new `oc_device_info_t` entry */ + core_update_device_data(device_count, cfg); + +#if defined(OC_SECURITY) || defined(OC_SOFTWARE_UPDATE) + is_realloc = true; +#endif + } + + /* Construct device resource */ + oc_create_device_resource(device_count, cfg.uri, cfg.rt); + + if (oc_get_con_res_announced()) { + /* Construct oic.wk.con resource for this device. */ + oc_create_con_resource(device_count); + } + + oc_create_discovery_resource(device_count); + +#ifdef OC_WKCORE + oc_create_wkcore_resource(device_count); +#endif /* OC_WKCORE */ + +#ifdef OC_INTROSPECTION + oc_create_introspection_resource(device_count); +#endif /* OC_INTROSPECTION */ + +#ifdef OC_MNT + oc_create_maintenance_resource(device_count); +#endif /* OC_MNT */ +#if defined(OC_CLIENT) && defined(OC_SERVER) && defined(OC_CLOUD) + oc_create_cloudconf_resource(device_count); +#endif /* OC_CLIENT && OC_SERVER && OC_CLOUD */ + +#ifdef OC_HAS_FEATURE_PUSH + oc_create_pushconf_resource(device_count); + oc_create_pushreceiver_resource(device_count); +#endif /* OC_HAS_FEATURE_PUSH */ + +#ifdef OC_SECURITY + /* + * Do what "main_init_resources()" does for all Devices here... + * refer to "main_init_resources()" + */ + if ((OC_ATOMIC_LOAD32(g_device_count) == (device_count + 1)) && is_realloc) { + /* realloc memory and populate SVR Resources + * only if new Device is attached to the end of `g_oc_device_info[]` */ + oc_sec_svr_create_new_device(device_count, true); + } else { + oc_sec_svr_create_new_device(device_count, false); + } +#endif /* OC_SECURITY */ + +#ifdef OC_SOFTWARE_UPDATE + /* + * Do what "main_init_resources()" does for all Devices here... + * refer to "main_init_resources()" + */ + if ((OC_ATOMIC_LOAD32(g_device_count) == (device_count + 1)) && is_realloc) { + /* realloc memory and populate SVR Resources + * only if new Device is attached to the end of `g_oc_device_info[]` */ + oc_swupdate_create_new_device(device_count, true); + } else { + oc_swupdate_create_new_device(device_count, false); + } +#endif /* OC_SOFTWARE_UPDATE */ + +#ifdef OC_SECURITY + /* + * Do what "main_load_resources()" does for all Devices here... + * refer to "main_load_resources()" + */ + oc_sec_svr_init_new_device(device_count); +#endif /* OC_SECURITY */ + +#ifdef OC_SOFTWARE_UPDATE + /* + * Do what "main_load_resources()" does for all Devices here... + * refer to "main_load_resources()" + */ + OC_DBG("oc_core_add_new_device_at_index(): loading swupdate(%d)", + device_count); + oc_swupdate_load(device_count); +#endif /* OC_SOFTWARE_UPDATE */ + + core_set_device_removed(device_count, false); + return &g_oc_device_info[device_count]; +} + +#if 0 +static void +core_delete_app_resources_per_device(size_t index) +{ + oc_ri_delete_app_resources_per_device(index); + + return; +} +#endif + +bool +oc_core_remove_device_at_index(size_t index) +{ + if (!oc_core_device_is_valid(index)) { + OC_ERR("Device index value is out of valid range! : \ + Device index %zu, current Device count %d", + index, g_device_count); + return false; + } + +#ifdef OC_SECURITY + oc_reset_device(index); + /* + * oc_sec_sdi_clear(oc_sec_sdi_get(index)); => already done in + * oc_reset_device() + * oc_sec_ael_free_device(index); => already done in + * oc_reset_device() + * oc_sec_cred_clear(index, NULL, NULL); => already done in + * oc_reset_device() + * oc_sec_acl_clear(index, NULL, NULL); => already done in + * oc_reset_device() + */ +#endif /* OC_SECURITY */ + + /* 1. remove core Resources mapped to this Device */ + for (size_t i = OC_NUM_CORE_PLATFORM_RESOURCES + + (OC_NUM_CORE_LOGICAL_DEVICE_RESOURCES * index); + i < OC_NUM_CORE_PLATFORM_RESOURCES + + (OC_NUM_CORE_LOGICAL_DEVICE_RESOURCES * (index + 1)); + ++i) { + oc_resource_t *core_resource = &g_core_resources[i]; + oc_ri_free_resource_properties(core_resource); + memset(core_resource, 0, sizeof(oc_resource_t)); + } + +#ifdef OC_HAS_FEATURE_PUSH + /* + * TODO4ME <2024/01/23> oc_core_remove_device_at_index() : + * - make function to delete receivers object list per VOD + */ +#if 0 + oc_push_free(); +#endif +#endif /* OC_HAS_FEATURE_PUSH */ + + /* 2. remove all application Resources (including collections) mapped to this + * Device */ + /* + * TODO4ME <2023/12/11> oc_core_remove_device_at_index() : do we need to + * delete observer too? (e.g. oc_ri_reset()) + */ + // core_delete_app_resources_per_device(index); + oc_ri_delete_app_resources_per_device(index); + + /* 3. clean all Properties of this Device */ + oc_core_free_device_info_properties(&g_oc_device_info[index]); + memset(&g_oc_device_info[index], 0, sizeof(oc_device_info_t)); + + /* 4. mark this Device is removed */ + core_set_device_removed(index, true); + + return true; +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ + static void oc_device_bind_rt(size_t device_index, const char *rt) { diff --git a/api/oc_core_res_internal.h b/api/oc_core_res_internal.h index 84aad6bf70..d6d37ebc00 100644 --- a/api/oc_core_res_internal.h +++ b/api/oc_core_res_internal.h @@ -55,6 +55,28 @@ void oc_core_shutdown(void); */ oc_device_info_t *oc_core_add_new_device(oc_add_new_device_t cfg); +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief Add new device to the position designeted by `index` of Device array + * (g_oc_device_info[]) + * + * @param cfg device configuration + * @param index index of `g_oc_device_info[]` + * @return oc_device_info_t* the device information + */ +oc_device_info_t *oc_core_add_new_device_at_index(oc_add_new_device_t cfg, + size_t index); + +/** + * @brief Remove existing device at "index" position + * + * @param index Device index + * @return true success + * @return false failure + */ +bool oc_core_remove_device_at_index(size_t index); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** * @brief encode the interfaces with the cbor (payload) encoder * diff --git a/api/oc_push.c b/api/oc_push.c index ea0cc6a4bc..6543eb09b1 100644 --- a/api/oc_push.c +++ b/api/oc_push.c @@ -2286,9 +2286,10 @@ oc_push_init(void) /* * clean up push related data structure - * - for push configuration Resource: they are cleaned when all app Resources - * are removed (see oc_main_shutdown()) - * - for push receivers Resource: free in this function + * - Push configuration Resource, Push Receiver Resource: + * they are cleaned when all app Resources are removed + * (see oc_ri_shutdown()) + * - for push receivers Resource: free receiver object list here */ void oc_push_free(void) @@ -2300,7 +2301,9 @@ oc_push_free(void) _purge_recv_obj_list(recvs_instance); OC_PUSH_DBG("free push receiver Resource (device: %zu)... ", recvs_instance->resource->device); +#if 0 oc_delete_resource(recvs_instance->resource); +#endif oc_memb_free(&g_recvs_instance_memb, recvs_instance); recvs_instance = (oc_recvs_t *)oc_list_pop(g_recvs_list); } diff --git a/api/oc_ri.c b/api/oc_ri.c index 502374259c..fd46b55170 100644 --- a/api/oc_ri.c +++ b/api/oc_ri.c @@ -473,8 +473,81 @@ oc_ri_get_app_resource_by_uri(const char *uri, size_t uri_len, size_t device) #endif /* OC_COLLECTIONS */ return NULL; } + #endif /* OC_SERVER */ +#ifdef OC_HAS_FEATURE_BRIDGE +oc_resource_t * +oc_ri_get_app_resource_by_device(size_t device, bool reset) +{ + static oc_resource_t *rsc; +#ifdef OC_COLLECTIONS + static oc_resource_t *coll_rsc; +#endif + oc_resource_t *found; + + if (reset) { + rsc = oc_ri_get_app_resources(); +#ifdef OC_COLLECTIONS + coll_rsc = oc_collection_get_collections(); +#endif + } + +#ifdef OC_COLLECTIONS + while (rsc || coll_rsc) { + if (rsc && (rsc->device == device)) { + found = rsc; + rsc = rsc->next; + return found; + } else { + if (rsc) + rsc = rsc->next; + if (coll_rsc && (coll_rsc->device == device)) { + found = coll_rsc; + coll_rsc = coll_rsc->next; + return found; + } else if (coll_rsc) { + coll_rsc = coll_rsc->next; + } + } + } +#else + while (rsc) { + if (rsc->device == device) { + found = rsc; + rsc = rsc->next; + return found; + } + + rsc = rsc->next; + } +#endif + + return NULL; +} + +void +oc_ri_delete_app_resources_per_device(size_t index) +{ + oc_resource_t *res = oc_ri_get_app_resources(); + oc_resource_t *t; + + while (res) { + if (res->device == index) { + t = res; + res = res->next; + oc_ri_delete_resource(t); + continue; + } + res = res->next; + } + +#ifdef OC_COLLECTIONS + oc_collections_free_per_device(index); +#endif /* OC_COLLECTIONS */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_ri_init(void) { diff --git a/api/oc_swupdate.c b/api/oc_swupdate.c index 0b9948895a..cef7f91b6f 100644 --- a/api/oc_swupdate.c +++ b/api/oc_swupdate.c @@ -353,6 +353,37 @@ oc_swupdate_create(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +_init_swupdate(size_t device_index) +{ + memset(&g_sw[device_index], 0, sizeof(oc_swupdate_t)); + swupdate_create_resource(device_index); +} + +void +oc_swupdate_create_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + g_sw = (oc_swupdate_t *)realloc(g_sw, oc_core_get_num_devices() * + sizeof(oc_swupdate_t)); + if (g_sw == NULL) { + oc_abort("Insufficient memory"); + } + _init_swupdate(device_index); + } else if (device_index < oc_core_get_num_devices()) { + oc_free_string(&g_sw[device_index].purl); + oc_free_string(&g_sw[device_index].nv); + oc_free_string(&g_sw[device_index].signage); + _init_swupdate(device_index); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + const char * oc_swupdate_action_to_str(oc_swupdate_action_t action) { diff --git a/api/oc_swupdate_internal.h b/api/oc_swupdate_internal.h index c84bab359c..b6a3101dae 100644 --- a/api/oc_swupdate_internal.h +++ b/api/oc_swupdate_internal.h @@ -68,6 +68,14 @@ typedef struct oc_swupdate_t */ void oc_swupdate_create(void); +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief Allocate and initialize Software Update (SWU) resources and data for + * specific Device. + */ +void oc_swupdate_create_new_device(size_t device_index, bool need_realloc); +#endif + /** * @brief Deallocate all SWU resource data. */ diff --git a/api/oc_vod_map.c b/api/oc_vod_map.c new file mode 100644 index 0000000000..d9464af619 --- /dev/null +++ b/api/oc_vod_map.c @@ -0,0 +1,499 @@ +/****************************************************************** + * + * Copyright 2020 Intel Corporation + * Copyright 2023 ETRI Joo-Chul Kevin Lee (rune@etri.re.kr) + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_BRIDGE + +#include "oc_vod_map.h" +#include "oc_rep.h" +#include "oc_rep_internal.h" +#include "oc_core_res.h" +#include "port/oc_connectivity.h" +#include "port/oc_log_internal.h" +#include "port/oc_storage.h" + +/* + * g_reset_index : + * - it stores the index of g_oc_device_info[] where the first VOD was added + */ +static size_t g_reset_index; + +/* + * g_vod_mapping_list : + * - vods : list of VOD (oc_virtual_device_t) + * - next_index : index of g_oc_device_info[]. new VOD will be added to + * g_oc_device_info[next_index] + */ +static oc_vod_mapping_list_t g_vod_mapping_list; + +#define SVR_TAG_MAX (32) + +static bool +_decode_vod_list(const oc_rep_t *rep) +{ + oc_rep_t *v; + if (!oc_rep_get_object_array(rep, "vods", &v)) { + OC_DBG("oc_vod_map: decode 'vods' object array not found."); + return false; + } + + while (NULL != v) { + oc_virtual_device_t *vod = + (oc_virtual_device_t *)malloc(sizeof(oc_virtual_device_t)); + char *v_id = NULL; + if (!oc_rep_get_byte_string(v->value.object, "vod_id", &v_id, + &vod->v_id_size)) { + OC_DBG("oc_vod_map: decode 'vod_id' not found."); + return false; + } + if (NULL != v_id) { + vod->v_id = (uint8_t *)malloc(vod->v_id_size * sizeof(uint8_t)); + memcpy(vod->v_id, v_id, vod->v_id_size); + } else { + OC_DBG("oc_vod_map: decode failed to find 'vod_id'"); + return false; + } + char *en = NULL; + size_t en_size = 0; + if (!oc_rep_get_string(v->value.object, "econame", &en, &en_size)) { + OC_DBG("oc_vod_map: decode 'econame' not found."); + return false; + } + if (NULL != en) { + oc_new_string(&vod->econame, en, en_size); + } else { + return false; + } + int64_t temp = 0; + if (!oc_rep_get_int(v->value.object, "index", &temp)) { + OC_DBG("oc_vod_map: decode 'index' not found."); + return false; + } + vod->index = (size_t)temp; + /* + * TODO4ME oc_vod_map_decode() : insert codes to restore + * `is_removed` value + */ +#if 0 + vod->is_removed = true; +#endif + oc_list_add(g_vod_mapping_list.vods, vod); + v = v->next; + } + + return true; +} + +static bool +_decode_rep(const oc_rep_t *rep) +{ + size_t len = 0; + + len = oc_string_len(rep->name); + switch (rep->type) { + case OC_REP_INT: + if (len == 10 && memcmp(oc_string(rep->name), "next_index", 10) == 0) { + g_vod_mapping_list.next_index = (size_t)rep->value.integer; + } + break; + case OC_REP_OBJECT_ARRAY: { + if (_decode_vod_list(rep) == false) + return false; + } break; + default: + break; + } + + return true; +} + +static bool +oc_vod_map_decode(oc_rep_t *rep, bool from_storage) +{ + /* + * TODO4ME use the from_storage param or drop it from the map_decode + */ + (void)from_storage; + + /* + * TODO4ME <2023/8/14> This could make a bug because Devices array + * (g_oc_device_info[]) can not be loaded now. so currently disable loading of + * `g_vod_mapping_list` + */ + if (from_storage == false) + return true; + + while (rep != NULL) { + if (_decode_rep(rep) == false) + return false; + rep = rep->next; + } + return true; +} + +/* + * load vod_map file and pass bytes to decode to populate oc_vod_list_t + * + * reference oc_sec_load_acl(size_t device) in oc_store.c + */ +static void +oc_vod_map_load(void) +{ + long ret = 0; + oc_rep_t *rep; + +#ifdef OC_DYNAMIC_ALLOCATION + uint8_t *buf = malloc(OC_MAX_APP_DATA_SIZE); + if (!buf) { + return; + } +#else /* OC_DYNAMIC_ALLOCATION */ + uint8_t buf[OC_MAX_APP_DATA_SIZE]; +#endif /* !OC_DYNAMIC_ALLOCATION */ + + ret = oc_storage_read("vod_map", buf, OC_MAX_APP_DATA_SIZE); + if (ret > 0) { +#ifndef OC_DYNAMIC_ALLOCATION + char rep_objects_alloc[OC_MAX_NUM_REP_OBJECTS]; + oc_rep_t rep_objects_pool[OC_MAX_NUM_REP_OBJECTS]; + memset(rep_objects_alloc, 0, OC_MAX_NUM_REP_OBJECTS * sizeof(char)); + memset(rep_objects_pool, 0, OC_MAX_NUM_REP_OBJECTS * sizeof(oc_rep_t)); + struct oc_memb rep_objects = { sizeof(oc_rep_t), OC_MAX_NUM_REP_OBJECTS, + rep_objects_alloc, (void *)rep_objects_pool, + 0 }; +#else /* !OC_DYNAMIC_ALLOCATION */ + struct oc_memb rep_objects = { sizeof(oc_rep_t), 0, 0, 0, 0 }; +#endif /* OC_DYNAMIC_ALLOCATION */ + oc_rep_set_pool(&rep_objects); + rep = oc_parse_rep(buf, (size_t)ret); + /* + * TODO4ME <2023/8/14> This could make a bug because Devices + * (g_oc_device_info[]) can not be loaded now. so, currently disable loading + * of `g_vod_mapping_list` + */ +#if 0 + oc_vod_map_decode(rep, true); +#endif + oc_vod_map_decode(rep, false); + oc_free_rep(rep); + } +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ +} + +/* + * responsible for encoding the oc_vod_list_t to cbor + * function will be used by dump_vod_map() + */ +static void +oc_vod_map_encode(void) +{ + oc_rep_begin_root_object(); + oc_rep_set_int(root, next_index, g_vod_mapping_list.next_index); + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + + oc_rep_open_array(root, vods); + while (v != NULL) { + oc_rep_object_array_begin_item(vods); + oc_rep_set_byte_string(vods, vod_id, v->v_id, v->v_id_size); + oc_rep_set_text_string(vods, econame, oc_string(v->econame)); + oc_rep_set_int(vods, index, v->index); + oc_rep_object_array_end_item(vods); + v = v->next; + } + oc_rep_close_array(root, vods); + oc_rep_end_root_object(); +} + +/* + * convert the oc_vod_list_t to cbor + * dump cbor bytes to vod_map file + * + * reference oc_sec_dump_acl(size_t device) in oc_store.c + */ +static void +oc_vod_map_dump(void) +{ +#ifdef OC_DYNAMIC_ALLOCATION + uint8_t *buf = malloc(OC_MAX_APP_DATA_SIZE); + if (!buf) + return; +#else /* OC_DYNAMIC_ALLOCATION */ + uint8_t buf[OC_MAX_APP_DATA_SIZE]; +#endif /* !OC_DYNAMIC_ALLOCATION */ + + oc_rep_new_v1(buf, OC_MAX_APP_DATA_SIZE); + oc_vod_map_encode(); + int size = oc_rep_get_encoded_payload_size(); + if (size > 0) { + OC_DBG("oc_vod_map: encoded vod_map size %d", size); + oc_storage_write("vod_map", buf, size); + } + +#ifdef OC_DYNAMIC_ALLOCATION + free(buf); +#endif /* OC_DYNAMIC_ALLOCATION */ +} + +void +oc_vod_map_init(void) +{ + OC_LIST_STRUCT_INIT(&g_vod_mapping_list, vods); + g_reset_index = g_vod_mapping_list.next_index = oc_core_get_num_devices(); + oc_vod_map_load(); +} + +/* + * release the resouces. + */ +void +oc_vod_map_free(void) +{ + if (g_vod_mapping_list.vods) { + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + oc_virtual_device_t *v_to_free; + while (v != NULL) { + free(v->v_id); + oc_free_string(&v->econame); + v_to_free = v; + v = v->next; + oc_list_remove(g_vod_mapping_list.vods, v_to_free); + free(v_to_free); + v_to_free = NULL; + } + } +} + +/* + * Reset the vod map as if no VODs had been discovered. + */ +void +oc_vod_map_reset(void) +{ + oc_vod_map_free(); + g_vod_mapping_list.next_index = g_reset_index; + oc_vod_map_dump(); +} + +size_t +oc_vod_map_get_vod_index(const uint8_t *vod_id, size_t vod_id_size, + const char *econame) +{ + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + + while (v != NULL) { + if (v->v_id_size == vod_id_size && + memcmp(vod_id, v->v_id, vod_id_size) == 0 && + (v->econame.size - 1) == strlen(econame) && + memcmp(econame, oc_string(v->econame), v->econame.size) == 0) { + return v->index; + } + v = v->next; + } + return 0; +} + +static void +_update_next_index(oc_virtual_device_t *v) +{ + /* + * continue walking the vods mapping entry list till an open next_index is + * found + * + * if the new VOD mapping entry is inserted in the middle of + * `g_vod_mapping_list.vods` list, find next available index of + * `g_oc_device_info[]` and save index value into the + * `g_vod_mapping_list.next_index` + */ + while (v != NULL) { + if (v->next != NULL && v->next->index == g_vod_mapping_list.next_index) { + g_vod_mapping_list.next_index++; + } + v = v->next; + } +} + +static void +_add_mapping_entry(oc_virtual_device_t *v, oc_virtual_device_t *vod) +{ + /* + * if this is not the first VOD mapping entry of `g_vod_mapping_list.vods` + * list (`g_vod_mapping_list.vods` is not empty).. Traverse + * g_vod_mapping_list.vods to find `v` whose index is + * (g_vod_mapping_list.next_index - 1), and insert new VOD mapping entry after + * `v`. + * + * After that, continue to increase `next_index` until no `v->index` matching + * `next_index` is found. After that, next_index will be updated And + * therefore,`g_vod_mapping_list.vods` list is always sorted in the order of + * `oc_virtual_device_t.index`... + */ + while (v != NULL) { + if ((g_vod_mapping_list.next_index == g_reset_index) || + (v->index == (g_vod_mapping_list.next_index - 1))) { + + if (g_vod_mapping_list.next_index == g_reset_index) { + /* + * if `next_index` points the first node of `oc_vod_mapping_list.vods`, + * (v->index == (g_vod_mapping_list.next_index - 1)) can't be satisfied + * ever... because there is no VOD mapping entry pointing Device before + * the first VOD. + * + * therefore, insert new VOD mapping entry as the the first item in the + * list in this case. + */ + oc_list_insert(g_vod_mapping_list.vods, NULL, vod); + v = oc_list_head(g_vod_mapping_list.vods); + } else { + /* + * if `next_index` points the middle node of `oc_vod_mapping_list.vods`, + * finds `v` whose index is (g_vod_mapping_list.next_index - 1). + * and insert new VOD mapping entry after `v`. + */ + oc_list_insert(g_vod_mapping_list.vods, v, vod); + } + + g_vod_mapping_list.next_index++; + + /* + * continue walking the vods mapping entry list till an open next_index is + * found + * + * if the new VOD mapping entry is inserted in the middle of + * `g_vod_mapping_list.vods` list, find next available index of + * `g_oc_device_info[]` and save index value into the + * `g_vod_mapping_list.next_index` + */ + _update_next_index(v); + break; + } + v = v->next; + } /* while */ +} + +size_t +oc_vod_map_add_mapping_entry(const uint8_t *vod_id, const size_t vod_id_size, + const char *econame) +{ + /* + * try to find this VOD mapping entry is already in `g_vod_mapping_list.vods` + * or not + */ + size_t v_index = oc_vod_map_get_vod_index(vod_id, vod_id_size, econame); + + /* + * if this vod mapping entry is already in `g_vod_mapping_list.vods`, + * return corresponding index of Device in g_oc_device_info[]. + */ + if (v_index != 0) { + return v_index; + } + + /* + * if this VOD mapping entry has not been added to `g_vod_mapping_list.vods`, + * insert it to g_vod_mapping_list.vods. + */ + oc_virtual_device_t *vod = + (oc_virtual_device_t *)malloc(sizeof(oc_virtual_device_t)); + vod->v_id = (uint8_t *)malloc(vod_id_size * sizeof(uint8_t)); + memcpy(vod->v_id, vod_id, vod_id_size); + vod->v_id_size = vod_id_size; + oc_new_string(&vod->econame, econame, strlen(econame)); + vod->is_vod_online = false; + + /* + * save corresponding index of Device in `g_oc_device_info[]` into + * `vod->index` (this Device has not been added to g_oc_device_info[] yet.) + */ + vod->index = g_vod_mapping_list.next_index; + + /* + * if this is the first VOD mapping entry (`g_vod_list.vods` is empty)... + * add new VOD mapping entry to `g_vod_mapping_list.vods` list + */ + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + if (v == NULL) { + oc_list_add(g_vod_mapping_list.vods, vod); + g_vod_mapping_list.next_index++; + } else { + _add_mapping_entry(v, vod); + } + + oc_vod_map_dump(); + return vod->index; +} + +void +oc_vod_map_remove_mapping_entry(size_t device_index) +{ + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + while (v != NULL) { + if (v->index == device_index) { + free(v->v_id); + oc_free_string(&v->econame); + oc_virtual_device_t *v_to_free = v; + oc_list_remove(g_vod_mapping_list.vods, v); + if (device_index < g_vod_mapping_list.next_index) { + g_vod_mapping_list.next_index = device_index; + } + free(v_to_free); + v_to_free = NULL; + break; + } + v = v->next; + } + + oc_vod_map_dump(); +} + +void +oc_vod_map_get_econame(oc_string_t *econame, size_t device_index) +{ + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + while (v != NULL) { + if (v->index == device_index) { + *econame = v->econame; + return; + } + v = v->next; + } +} + +oc_virtual_device_t * +oc_vod_map_get_mapping_entry(size_t device_index) +{ + oc_virtual_device_t *v = oc_list_head(g_vod_mapping_list.vods); + while (v != NULL) { + if (v->index == device_index) { + return v; + } + v = v->next; + } + return NULL; +} + +oc_virtual_device_t * +oc_vod_map_get_mapping_list(void) +{ + return oc_list_head(g_vod_mapping_list.vods); +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ diff --git a/api/unittest/RITest.cpp b/api/unittest/RITest.cpp index 64104a3dc2..945a733d9c 100644 --- a/api/unittest/RITest.cpp +++ b/api/unittest/RITest.cpp @@ -2,6 +2,7 @@ * * Copyright 2018 GRANITE RIVER LABS All Rights Reserved. * 2021 CASCODA LTD All Rights Reserved. + * 2024 ETRI All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -26,10 +27,26 @@ #include "api/oc_runtime_internal.h" #include "port/oc_network_event_handler_internal.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_api.h" +#include "oc_core_res.h" +#include "security/oc_svr_internal.h" +#include "api/oc_core_res_internal.h" +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + +#ifdef OC_SOFTWARE_UPDATE +#include "api/oc_swupdate_internal.h" +#endif + #ifdef OC_TCP #include "messaging/coap/signal_internal.h" #endif /* OC_TCP */ +#ifdef OC_HAS_FEATURE_PUSH +#include "api/oc_push_internal.h" +#endif /* OC_HAS_FEATURE_PUSH */ + #include #include #include @@ -40,19 +57,167 @@ class TestOcRi : public testing::Test { public: void SetUp() override { - oc_network_event_handler_mutex_init(); oc_runtime_init(); oc_ri_init(); + +#ifdef OC_HAS_FEATURE_BRIDGE + oc_core_init(); +#endif /* OC_HAS_FEATURE_BRIDGE */ + + oc_network_event_handler_mutex_init(); + +#if defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) + oc_sec_svr_create(); +#endif /* OC_SECURITY */ + +//#ifdef OC_SOFTWARE_UPDATE +#if defined(OC_SOFTWARE_UPDATE) && defined(OC_HAS_FEATURE_BRIDGE) + oc_swupdate_create(); +#endif } void TearDown() override { +#if defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) + oc_sec_svr_free(); +#endif /* defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) */ + +//#ifdef OC_SOFTWARE_UPDATE +#if defined(OC_SOFTWARE_UPDATE) && defined(OC_HAS_FEATURE_BRIDGE) + oc_swupdate_free(); +#endif /* OC_SOFTWARE_UPDATE */ + +#ifdef OC_HAS_FEATURE_PUSH + oc_push_free(); +#endif /* OC_HAS_FEATURE_PUSH */ + oc_ri_shutdown(); - oc_runtime_shutdown(); oc_network_event_handler_mutex_destroy(); + +#ifdef OC_HAS_FEATURE_BRIDGE + oc_core_shutdown(); +#endif /* OC_HAS_FEATURE_BRIDGE */ + + oc_runtime_shutdown(); } }; +#ifdef OC_HAS_FEATURE_BRIDGE +static void +get_handler(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ +} + +static const std::string kDeviceName{ "Table Lamp" }; +static const std::string kDeviceURI{ "/oic/d" }; +static const std::string kDeviceType{ "oic.d.light" }; +static const std::string kOCFSpecVersion{ "ocf.1.0.0" }; +static const std::string kOCFDataModelVersion{ "ocf.res.1.0.0" }; + +TEST_F(TestOcRi, GetNDeleteAppResourceByIndex) +{ + oc_add_new_device_t cfg{}; + cfg.name = kDeviceName.c_str(); + cfg.uri = kDeviceURI.c_str(); + cfg.rt = kDeviceType.c_str(); + cfg.spec_version = kOCFSpecVersion.c_str(); + cfg.data_model_version = kOCFDataModelVersion.c_str(); + size_t device_count; + + /* -------------------------------------------------*/ + /* + * oc_ri_get_app_resource_by_device(device, reset) + */ + /*--------------------------------------------------*/ + + auto deviceIndex = oc_core_get_num_devices(); + EXPECT_NE(oc_core_add_new_device_at_index(cfg, deviceIndex), nullptr); + + auto deviceInfo = oc_core_get_device_info(deviceIndex); + + ASSERT_NE(deviceInfo, nullptr); + EXPECT_STREQ(kDeviceName.c_str(), oc_string(deviceInfo->name)); + EXPECT_EQ(0, deviceIndex); + + /* + * add Resources + */ + std::string rscURI1{ "/rsc1" }; + auto resource1 = oc_new_resource(nullptr, rscURI1.c_str(), 0, deviceIndex); + oc_resource_set_request_handler(resource1, OC_GET, get_handler, nullptr); + ASSERT_NE(nullptr, resource1); + EXPECT_EQ(deviceIndex, resource1->device); + oc_add_resource(resource1); + + std::string rscURI2{ "/rsc2" }; + auto resource2 = oc_new_resource(nullptr, rscURI2.c_str(), 0, deviceIndex); + oc_resource_set_request_handler(resource2, OC_GET, get_handler, nullptr); + ASSERT_NE(nullptr, resource2); + EXPECT_EQ(deviceIndex, resource2->device); + oc_add_resource(resource2); + + /* + * add collection Resource + */ + std::string colURI1 = "/col1"; + auto colResource3 = + oc_new_collection(nullptr, colURI1.c_str(), 0, deviceIndex); + ASSERT_NE(nullptr, colResource3); + EXPECT_STREQ(colURI1.c_str(), oc_string(colResource3->uri)); + EXPECT_EQ(deviceIndex, colResource3->device); + oc_add_collection_v1(colResource3); + + /* + * try to find from the first entry... + */ + auto rsc1 = oc_ri_get_app_resource_by_device(deviceIndex, true); + ASSERT_NE(rsc1, nullptr); + + auto rsc2 = oc_ri_get_app_resource_by_device(deviceIndex, true); + ASSERT_NE(rsc2, nullptr); + EXPECT_STREQ(oc_string(rsc1->uri), oc_string(rsc2->uri)); + + /* + * try to resume search from the entry which was seen last... + */ + rsc1 = oc_ri_get_app_resource_by_device(deviceIndex, true); + ASSERT_NE(rsc1, nullptr); + + rsc2 = oc_ri_get_app_resource_by_device(deviceIndex, false); + ASSERT_NE(rsc2, nullptr); + EXPECT_STRNE(oc_string(rsc1->uri), oc_string(rsc2->uri)); + + /* + * try to find all app resources mapped to a Device + */ + std::set rscSet{ rscURI1.c_str(), rscURI2.c_str(), + colURI1.c_str() }; + + rsc1 = oc_ri_get_app_resource_by_device(deviceIndex, true); + while (rsc1) { + rscSet.erase(oc_string(rsc1->uri)); + rsc1 = oc_ri_get_app_resource_by_device(deviceIndex, false); + } + + EXPECT_EQ(true, rscSet.empty()); + + /* -------------------------------------------------*/ + /* + * oc_ri_delete_app_resources_per_device(index) + */ + /*--------------------------------------------------*/ + oc_ri_delete_app_resources_per_device(deviceIndex); + rsc1 = oc_ri_get_app_resource_by_device(deviceIndex, true); + + EXPECT_EQ(nullptr, rsc1); + + /* remove device */ + oc_core_remove_device_at_index(deviceIndex); +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ + TEST_F(TestOcRi, StatusCodeToCoapCode) { for (int i = OC_STATUS_OK; i < __NUM_OC_STATUS_CODES__; ++i) { diff --git a/api/unittest/bridgetest.cpp b/api/unittest/bridgetest.cpp new file mode 100644 index 0000000000..b511c08d66 --- /dev/null +++ b/api/unittest/bridgetest.cpp @@ -0,0 +1,298 @@ +/****************************************************************** + * + * Copyright 2023 ETRI Joo-Chul Kevin Lee (rune@etri.re.kr) + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_BRIDGE + +#include +#include + +#include "oc_api.h" +#include "oc_bridge.h" +#include "oc_vod_map.h" +#include "port/oc_storage.h" +#include "api/oc_ri_internal.h" +#include "api/oc_runtime_internal.h" +#include "api/oc_core_res_internal.h" +#include "api/oc_swupdate_internal.h" +#include "security/oc_svr_internal.h" +#include "security/oc_doxm_internal.h" +#include "port/oc_network_event_handler_internal.h" + +#include "tests/gtest/Device.h" + +#ifdef OC_HAS_FEATURE_PUSH +#include "api/oc_push_internal.h" +#endif /* OC_HAS_FEATURE_PUSH */ + +extern "C" { +void bridge_owned_changed(const oc_uuid_t *device_uuid, size_t device_index, + bool owned, void *user_data); +} + +static const oc::DeviceToAdd kBridgeDevice = { "oic.d.bridge", "BridgeDevice", + "ocf.1.0.0", "ocf.res.1.0.0", + "/oic/d" }; + +static constexpr size_t kBridgeDeviceID{ 0 }; + +static const oc::DeviceToAdd kVODDevice = { "oic.d.virtual", "VOD1", + "ocf.1.0.0", "ocf.res.1.0.0", + "/oic/d" }; + +static const std::string kVODDeviceID{ "vod1" }; +static const std::string kVODEconame{ "matter" }; +static const std::string kVODlistRscURI{ "/bridge/vodlist" }; + +class TestBridge : public testing::Test { +private: + void RunBridgeDevice() + { + auto result = oc_init_platform("ETRI", nullptr, nullptr); + EXPECT_EQ(result, 0); + + /* + * add bridge device (oic.d.bridge) + */ + auto bridgeIndex = oc_bridge_add_bridge_device( + kBridgeDevice.name.c_str(), kBridgeDevice.spec_version.c_str(), + kBridgeDevice.data_model_version.c_str(), nullptr, nullptr); + + EXPECT_EQ(bridgeIndex, kBridgeDeviceID); + } + +public: + void SetUp() override + { + oc_runtime_init(); + oc_ri_init(); + oc_core_init(); + oc_network_event_handler_mutex_init(); + RunBridgeDevice(); +#ifdef OC_SECURITY + oc_sec_svr_create(); +#endif /* OC_SECURITY */ +#ifdef OC_SOFTWARE_UPDATE + oc_swupdate_create(); +#endif + } + + void ShutdownConnectivity() + { + for (size_t device = 0; device < oc_core_get_num_devices(); device++) { + if (oc_core_get_device_info(device)->is_removed == false) + oc_connectivity_shutdown(device); + } + } + + void TearDown() override + { +#ifdef OC_SECURITY + oc_sec_svr_free(); +#endif /* OC_SECURITY */ + +#ifdef OC_SOFTWARE_UPDATE + oc_swupdate_free(); +#endif /* OC_SOFTWARE_UPDATE */ + +#ifdef OC_HAS_FEATURE_PUSH + oc_push_free(); +#endif /* OC_HAS_FEATURE_PUSH */ + oc_ri_shutdown(); + ShutdownConnectivity(); + oc_network_event_handler_mutex_destroy(); + oc_core_shutdown(); + oc_runtime_shutdown(); + } +}; + +TEST_F(TestBridge, BridgeAPITest) +{ + /* -------------------------------------------------*/ + /* + * oc_bridge_add_bridge_device() + */ + /* -------------------------------------------------*/ + + /* + * add bridge device (oic.d.bridge) + */ + auto bridgeDeviceInfo = oc_core_get_device_info(kBridgeDeviceID); + ASSERT_NE(bridgeDeviceInfo, nullptr); + + /* check bridge device name */ + EXPECT_STREQ(kBridgeDevice.name.c_str(), oc_string(bridgeDeviceInfo->name)); + + auto bridgeDeviceRsc = oc_core_get_resource_by_uri_v1( + kBridgeDevice.uri.c_str(), kBridgeDevice.uri.size(), kBridgeDeviceID); + ASSERT_NE(bridgeDeviceRsc, nullptr); + + /* check bridge device type */ + EXPECT_STREQ(kBridgeDevice.rt.c_str(), + oc_string_array_get_item(bridgeDeviceRsc->types, 0)); + + /* -------------------------------------------------*/ + /* + * oc_bridge_add_virtual_device() + * oc_bridge_get_virtual_device_index() + */ + /* -------------------------------------------------*/ + size_t vodIndex = oc_bridge_add_virtual_device( + (const uint8_t *)kVODDeviceID.c_str(), kVODDeviceID.size(), + kVODEconame.c_str(), kVODDevice.uri.c_str(), kVODDevice.rt.c_str(), + kVODDevice.name.c_str(), kVODDevice.spec_version.c_str(), + kVODDevice.data_model_version.c_str(), nullptr, nullptr); + EXPECT_NE(vodIndex, 0); + + // oc_device_info_t *vodDeviceInfo = oc_core_get_device_info(vodIndex); + auto vodDeviceInfo = oc_core_get_device_info(vodIndex); + ASSERT_NE(vodDeviceInfo, nullptr); + + /* check VOD device name */ + EXPECT_STREQ(kVODDevice.name.c_str(), oc_string(vodDeviceInfo->name)); + + auto vodDeviceRsc = oc_core_get_resource_by_uri_v1( + kVODDevice.uri.c_str(), kVODDevice.uri.size(), vodIndex); + ASSERT_NE(vodDeviceRsc, nullptr); + + /* check bridge device type */ + EXPECT_STREQ(kVODDevice.rt.c_str(), + oc_string_array_get_item(vodDeviceRsc->types, 0)); + + /* check device index */ + auto vodIndexTemp = oc_bridge_get_virtual_device_index( + (const uint8_t *)kVODDeviceID.c_str(), kVODDeviceID.size(), + kVODEconame.c_str()); + EXPECT_EQ(vodIndexTemp, vodIndex); + + /* -------------------------------------------------*/ + /* + * oc_bridge_remove_virtual_device() + * oc_bridge_get_vod() + * oc_bridge_get_vod_list() + * oc_bridge_get_vod_mapping_info() + * oc_bridge_get_vod_mapping_info2() + */ + /* -------------------------------------------------*/ + /* get vodlist resource */ + auto vodListRsc = oc_ri_get_app_resource_by_uri( + kVODlistRscURI.c_str(), kVODlistRscURI.size(), kBridgeDeviceID); + ASSERT_NE(vodListRsc, nullptr); + EXPECT_EQ(vodListRsc->device, kBridgeDeviceID); + + /* get vod item for VOD */ + // oc_vods_t * vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + auto vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + EXPECT_EQ(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_EQ(vodItem, nullptr); + +#ifdef OC_SECURITY + /* own bridge device */ + bridge_owned_changed(&bridgeDeviceInfo->di, kBridgeDeviceID, true, nullptr); + auto bridgeDoxm = oc_sec_get_doxm(kBridgeDeviceID); + bridgeDoxm->owned = true; + + /* own VODS */ + bridge_owned_changed(&vodDeviceInfo->di, vodIndex, true, nullptr); + auto vodDoxm = oc_sec_get_doxm(vodIndex); + vodDoxm->owned = true; + + /* get vod item for VOD */ + vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + EXPECT_NE(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_NE(vodItem, nullptr); + + /* try to get vod map entry */ + auto vodMapEntry1 = oc_bridge_get_vod_mapping_info(vodIndex); + auto vodMapEntry2 = oc_bridge_get_vod_mapping_info2(vodItem); + EXPECT_EQ(vodMapEntry1, vodMapEntry2); + + /* remove vod from vod list */ + ASSERT_EQ(oc_bridge_remove_virtual_device(vodIndex), 0); + + /* get vod item for VOD */ + vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + EXPECT_EQ(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_EQ(vodItem, nullptr); +#else + /* try to get vod map entry */ + auto vodMapEntry1 = oc_bridge_get_vod_mapping_info(vodIndex); + EXPECT_NE(vodMapEntry1, nullptr); +#endif /* OC_SECURITY */ + + /* -------------------------------------------------*/ + /* + * oc_bridge_add_vod() + */ + /* -------------------------------------------------*/ + ASSERT_EQ(oc_bridge_add_vod(vodIndex), 0); + +#ifdef OC_SECURITY + /* get vod item for VOD */ + vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + EXPECT_NE(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_NE(vodItem, nullptr); +#else + /* get vod item for VOD */ + vodItem = oc_bridge_get_vod(vodDeviceInfo->di); + EXPECT_EQ(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_EQ(vodItem, nullptr); +#endif /* OC_SECURITY */ + + /* -------------------------------------------------*/ + /* + * oc_bridge_delete_virtual_device() + */ + /* -------------------------------------------------*/ + oc_uuid_t vodDi; + memcpy(vodDi.id, vodDeviceInfo->di.id, OC_UUID_ID_SIZE); + + auto result = oc_bridge_delete_virtual_device(vodIndex); + EXPECT_EQ(result, 0); + + /* check vod is removed from vod list */ + /* get vod item for VOD */ + vodItem = oc_bridge_get_vod(vodDi); + EXPECT_EQ(vodItem, nullptr); + + /* get vodlist */ + vodItem = oc_bridge_get_vod_list(); + EXPECT_EQ(vodItem, nullptr); + + /* check vod is removed from vod mapping list */ + auto vodMapEntry3 = oc_bridge_get_vod_mapping_info(vodIndex); + EXPECT_EQ(vodMapEntry3, nullptr); +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ diff --git a/api/unittest/collectiontest.cpp b/api/unittest/collectiontest.cpp index 054e35a98f..866cb6208d 100644 --- a/api/unittest/collectiontest.cpp +++ b/api/unittest/collectiontest.cpp @@ -1,6 +1,7 @@ /**************************************************************************** * * Copyright (c) 2023 plgd.dev s.r.o. + * Copyright (c) 2024 ETRI * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -1015,4 +1016,60 @@ TEST_F(TestCollectionsWithServer, GetRequest_Batch) #endif // !OC_SECURITY || OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM +#ifdef OC_HAS_FEATURE_BRIDGE +/* + * Done: + * oc_collections_free_per_device(device) + */ +TEST_F(TestCollectionsWithServer, FreePerDevice) +{ + /* + * add collection Resource 1 + */ + std::string name1 = "col1"; + std::string uri1 = "/col1"; + oc_resource_t *col1 = + oc_new_collection(name1.c_str(), uri1.c_str(), 0, kDeviceID); + ASSERT_NE(nullptr, col1); + + EXPECT_STREQ(name1.c_str(), oc_string(col1->name)); + EXPECT_STREQ(uri1.c_str(), oc_string(col1->uri)); + EXPECT_EQ(kDeviceID, col1->device); + + EXPECT_TRUE(oc_add_collection_v1(col1)); + + /* + * add collection Resource 2 + */ + std::string name2 = "col2"; + std::string uri2 = "/col2"; + oc_resource_t *col2 = + oc_new_collection(name2.c_str(), uri2.c_str(), 0, kDeviceID); + ASSERT_NE(nullptr, col2); + + EXPECT_STREQ(name2.c_str(), oc_string(col2->name)); + EXPECT_STREQ(uri2.c_str(), oc_string(col2->uri)); + EXPECT_EQ(kDeviceID, col2->device); + + EXPECT_TRUE(oc_add_collection_v1(col2)); + + /* + * check all Collection Resources were inserted successfully + */ + oc_collection_t *col = oc_collection_get_all(); + ASSERT_NE(nullptr, col); + + EXPECT_STREQ(oc_string(col->res.name), name1.c_str()); + col = reinterpret_cast(col->res.next); + EXPECT_STREQ(oc_string(col->res.name), name2.c_str()); + + /* + * check all collection mapped to this Device are removed successfully + */ + oc_collections_free_per_device(kDeviceID); + col = oc_collection_get_all(); + EXPECT_EQ(nullptr, col); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_COLLECTIONS */ diff --git a/api/unittest/coreresourcetest.cpp b/api/unittest/coreresourcetest.cpp index f9a9882b12..14a7e9957a 100644 --- a/api/unittest/coreresourcetest.cpp +++ b/api/unittest/coreresourcetest.cpp @@ -1,6 +1,8 @@ /****************************************************************** * * Copyright 2018 GRANITE RIVER LABS All Rights Reserved. + * 2024 ETRI All Rights Reserved. + * * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -19,6 +21,7 @@ #include "api/oc_core_res_internal.h" #include "api/oc_ri_internal.h" #include "api/oc_runtime_internal.h" +#include "api/oc_swupdate_internal.h" #include "oc_api.h" #include "oc_core_res.h" #include "oc_helpers.h" @@ -32,6 +35,11 @@ #include "api/oc_push_internal.h" #endif /* OC_HAS_FEATURE_PUSH */ +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_bridge.h" +#include "security/oc_svr_internal.h" +#endif /* OC_HAS_FEATURE_BRIDGE */ + #include #include #include @@ -59,20 +67,109 @@ class TestCoreResource : public testing::Test { oc_runtime_init(); oc_ri_init(); oc_core_init(); +#if defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) + oc_sec_svr_create(); +#endif /* defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) */ + +//#ifdef OC_SOFTWARE_UPDATE +#if defined(OC_SOFTWARE_UPDATE) && defined(OC_HAS_FEATURE_BRIDGE) + oc_swupdate_create(); +#endif } void TearDown() override { +#if defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) + oc_sec_svr_free(); +#endif /* defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) */ + +//#ifdef OC_SOFTWARE_UPDATE +#if defined(OC_SOFTWARE_UPDATE) && defined(OC_HAS_FEATURE_BRIDGE) + oc_swupdate_free(); +#endif + #ifdef OC_HAS_FEATURE_PUSH oc_push_free(); #endif /* OC_HAS_FEATURE_PUSH */ - oc_core_shutdown(); + oc_ri_shutdown(); - oc_runtime_shutdown(); oc_network_event_handler_mutex_destroy(); + oc_core_shutdown(); + oc_runtime_shutdown(); } }; +#ifdef OC_HAS_FEATURE_BRIDGE +/* + * Not testable (static functioins) : + * core_update_existing_device_data() + * core_delete_app_resources_per_device() + * core_set_device_removed() + * + * Done: + * oc_core_add_new_device_at_index() + * oc_core_remove_device_at_index() + * oc_core_get_device_index() + * + */ +TEST_F(TestCoreResource, CoreAddNRemoveDeviceAtIndex) +{ + oc_add_new_device_t cfg{}; + cfg.name = kDeviceName.c_str(); + cfg.uri = kDeviceURI.c_str(); + cfg.rt = kDeviceType.c_str(); + cfg.spec_version = kOCFSpecVersion.c_str(); + cfg.data_model_version = kOCFDataModelVersion.c_str(); + size_t device_count; + + /* -------------------------------------------------*/ + /* + * oc_core_add_new_device_at_index() + * oc_core_remove_device_at_index() + * oc_core_get_device_index() + */ + /*--------------------------------------------------*/ + + /* + * device index is outranged + * => should fail + */ + device_count = oc_core_get_num_devices(); + EXPECT_EQ(oc_core_add_new_device_at_index(cfg, device_count + 1), nullptr); + + /* + * add new device at the end of array + * => should succeed + */ + EXPECT_NE(oc_core_add_new_device_at_index(cfg, device_count), nullptr); + EXPECT_EQ(oc_core_get_device_info(device_count)->is_removed, false); + + /* + * try to overwrite new device onto existing device + * => should fail + */ + EXPECT_EQ(oc_core_add_new_device_at_index(cfg, device_count), nullptr); + + /* + * try to overwrite new device into the slot where existing device was deleted + * => should succeed + */ + oc_core_remove_device_at_index(device_count); + EXPECT_NE(oc_core_add_new_device_at_index(cfg, device_count), nullptr); + + /* + * try to find device with device id + * => should succeed + */ + size_t device_index; + oc_core_get_device_index(oc_core_get_device_info(device_count)->di, + &device_index); + EXPECT_EQ(device_index, device_count); + + oc_core_remove_device_at_index(device_count); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + TEST_F(TestCoreResource, CoreDevice_P) { oc_add_new_device_t cfg{}; @@ -81,11 +178,23 @@ TEST_F(TestCoreResource, CoreDevice_P) cfg.rt = kDeviceType.c_str(); cfg.spec_version = kOCFSpecVersion.c_str(); cfg.data_model_version = kOCFDataModelVersion.c_str(); + +#ifdef OC_HAS_FEATURE_BRIDGE + oc_device_info_t *addcoredevice = + oc_core_add_new_device_at_index(cfg, oc_core_get_num_devices()); +#else oc_device_info_t *addcoredevice = oc_core_add_new_device(cfg); +#endif /* defined(OC_HAS_FEATURE_BRIDGE) && defined(OC_SECURITY) */ + ASSERT_NE(addcoredevice, nullptr); size_t numcoredevice = oc_core_get_num_devices(); EXPECT_EQ(1, numcoredevice); + +#ifdef OC_HAS_FEATURE_BRIDGE + oc_core_remove_device_at_index(numcoredevice - 1); +#else oc_connectivity_shutdown(kDevice1ID); +#endif } static void diff --git a/api/unittest/uuidtest.cpp b/api/unittest/uuidtest.cpp index d9aade5cc9..6c00e2ac23 100644 --- a/api/unittest/uuidtest.cpp +++ b/api/unittest/uuidtest.cpp @@ -30,7 +30,7 @@ constexpr const char UUID2[] = "XYZabcdefghijklmnopqrstuvwxyz012"; using uuid_buffer_t = std::array; -TEST(UUID, UUIDIsNill) +TEST(UUID, IsEmptyTest_P) { oc_uuid_t uuid{}; EXPECT_TRUE(oc_uuid_is_empty(uuid)); diff --git a/api/unittest/vodmaptest.cpp b/api/unittest/vodmaptest.cpp new file mode 100644 index 0000000000..c452862d2c --- /dev/null +++ b/api/unittest/vodmaptest.cpp @@ -0,0 +1,529 @@ +/****************************************************************** + * + * Copyright 2020 Intel Corporation + * Copyright 2023 ETRI Joo-Chul Kevin Lee + * + * 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 "util/oc_features.h" + +#ifdef OC_HAS_FEATURE_BRIDGE + +#include +#include +#define OC_STORAGE + +#include "oc_api.h" +#include "oc_bridge.h" +#include +#include "port/oc_storage.h" + +#include +#include + +using test_vod = struct test_vod +{ + std::string vod_id; + std::string eco_system; +}; + +class VodMapTest : public testing::Test { +public: + int dirExists(const char *const path); + std::vector tv = { + { "1b32e152-3756-4fb6-b3f2-d8db7aafe39f", "ABC" }, + { "f959f6fd-8d08-4766-849b-74c3eec5e041", "ABC" }, + { "02feb15a-bf94-4f33-9794-adfb25c7bc60", "XYZ" }, + { "686ef93d-36e0-47fc-8316-fbd7045e850a", "ABC" }, + { "686ef93d-36e0-47fc-8316-fbd7045e850a", "XYZ" } + }; + +protected: + void SetUp() override + { +#ifdef OC_STORAGE +#if defined(_WIN32_) + mkdir("vod_map_test_dir"); +#elif defined(__linux__) + mkdir("vod_map_test_dir", + S_IRWXU | S_IRWXG | S_IRWXG /* 0777 permissions*/); +#endif /* if defined(_WIN32_) */ + oc_storage_config("./vod_map_test_dir/"); +#endif /* OC_STORAGE */ + } + void TearDown() override + { + remove("./vod_map_test_dir/vod_map"); + remove("./vod_map_test_dir/"); + } +}; + +TEST_F(VodMapTest, vod_map_add_id) +{ + oc_vod_map_init(); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + // The vod map code actually expects the zero device to always be a bridge or + // other device for that reason we are inserting a dummy device at index zero + // this will be dumped into the output map file but shouldn't effect the + // test results. + const char *dummy = "dummy"; + EXPECT_EQ( + oc_vod_map_add_mapping_entry((uint8_t *)dummy, strlen(dummy), dummy), 0); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + oc_vod_map_free(); +} + +TEST_F(VodMapTest, vod_map_remove_id) +{ + oc_vod_map_init(); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + // The vod map code actually expects the zero device to always be a bridge or + // other device for that reason we are inserting a dummy device at index zero + // this will be dumped into the output map file but shouldn't effect the + // test results. + const char *dummy = "dummy"; + EXPECT_EQ( + oc_vod_map_add_mapping_entry((uint8_t *)dummy, strlen(dummy), dummy), 0); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + oc_vod_map_remove_mapping_entry(1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + + oc_vod_map_reset(); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + + oc_vod_map_free(); + + // test intermittent removal of vod_id.c_strs + + oc_vod_map_init(); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 0); + + // The vod map code actually expects the zero device to always be a bridge or + // other device for that reason we are inserting a dummy device at index zero + // this will be dumped into the output map file but shouldn't effect the + // test results. + EXPECT_EQ( + oc_vod_map_add_mapping_entry((uint8_t *)dummy, strlen(dummy), dummy), 0); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + oc_vod_map_remove_mapping_entry(2); + oc_vod_map_remove_mapping_entry(4); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 4); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + oc_vod_map_reset(); + oc_vod_map_free(); + + // test consecutive removal of vod_id.c_strs + + oc_vod_map_init(); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 0); + + // The vod map code actually expects the zero device to always be a bridge or + // other device for that reason we are inserting a dummy device at index zero + // this will be dumped into the output map file but shouldn't effect the + // test results. + EXPECT_EQ( + oc_vod_map_add_mapping_entry((uint8_t *)dummy, strlen(dummy), dummy), 0); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + oc_vod_map_remove_mapping_entry(2); + oc_vod_map_remove_mapping_entry(4); + oc_vod_map_remove_mapping_entry(3); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_add_mapping_entry((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 4); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[0].vod_id.c_str(), + strlen(tv[0].vod_id.c_str()), + tv[0].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[1].vod_id.c_str(), + strlen(tv[1].vod_id.c_str()), + tv[1].eco_system.c_str()), + 4); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[2].vod_id.c_str(), + strlen(tv[2].vod_id.c_str()), + tv[2].eco_system.c_str()), + 3); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 2); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 5); + + oc_vod_map_free(); +} + +TEST_F(VodMapTest, vod_map_add_same_id_different_econame) +{ + oc_vod_map_init(); + // verify the vod_id.c_str() are not yet added + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 0); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 0); + + // The vod map code actually expects the zero device to always be a bridge or + // other device for that reason we are inserting a dummy device at index zero + // this will be dumped into the output map file but shouldn't effect the + // test results. + const char *dummy = "dummy"; + EXPECT_EQ( + oc_vod_map_add_mapping_entry((uint8_t *)dummy, strlen(dummy), dummy), 0); + + // even though tv[3] and tv[4] have the same vod_id.c_str() they have + // different econames and should each get a different index + size_t vod_index3 = oc_vod_map_add_mapping_entry( + (uint8_t *)tv[3].vod_id.c_str(), strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()); + size_t vod_index4 = oc_vod_map_add_mapping_entry( + (uint8_t *)tv[4].vod_id.c_str(), strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()); + EXPECT_NE(vod_index3, vod_index4); + + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[3].vod_id.c_str(), + strlen(tv[3].vod_id.c_str()), + tv[3].eco_system.c_str()), + 1); + EXPECT_EQ(oc_vod_map_get_vod_index((uint8_t *)tv[4].vod_id.c_str(), + strlen(tv[4].vod_id.c_str()), + tv[4].eco_system.c_str()), + 2); + + oc_vod_map_free(); +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index f7a1d4fa16..55791d7a06 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -218,6 +218,14 @@ if(UNIX) DEPENDENCIES client-server-static ) endif() + + if(OC_BRIDGE_ENABLED) + oc_add_app_executable( + TARGET dummy_bridge_linux + SOURCES ${PROJECT_SOURCE_DIR}/dummy_bridge_linux.c + DEPENDENCIES client-server-static + ) + endif() if(EXISTS ${PROJECT_SOURCE_DIR}/device_builder_server.c) oc_add_app_executable( diff --git a/apps/Readme.md b/apps/Readme.md index 807f0915d6..4cd466a926 100644 --- a/apps/Readme.md +++ b/apps/Readme.md @@ -152,7 +152,11 @@ naming convention: - ### push_targetserver_multithread_linux.c: PUSH target server which receives PUSH update request from PUSH origin server. It plays the role of PUSH configurator at the same time. - - for more detail see [OCF Push Notification](./docs/push.md) + - for more detail see [Push Notification](./docs/push.md) + +- ### dummy_bridge_linux.c: + VOD testing sample for Bridging feature. + - for more detail see [Bridging](./docs/bridging.md) - ### Other files: The JSON files are the introspection files in JSON. diff --git a/apps/docs/bridging.md b/apps/docs/bridging.md new file mode 100644 index 0000000000..0b4566f7a1 --- /dev/null +++ b/apps/docs/bridging.md @@ -0,0 +1,260 @@ +# Introduction +> Joo-Chul Kevin Lee (rune@etri.re.kr) + +OCF provides bridging feature to support interaction with other NON-OCF ecosystem devices. + +## VOD (Virtual OCF Device) +- OCF Briding feature is based on **VOD (Virtual OCF Device)** concept. +- A VOD an OCF Device which is exactly same as a normal OCF Device except that it represents non-OCF Device and it has additional device type value (i.e. "`oic.d.virtual`"). This additional device type can help users to distinguish VODs from other normal OCF Devices. + +## VOD vs. Bridge Device +- **Bridge Device** is another special OCF Device used to manage VODs. It also has additional device type value (i.e. "`oic.d.bridge`") to identify it from other normal OCF Devices. An OCF Platform works as a Bridge should have one Bridge Device. +- VODs have dependency on a Bridge Device for security reason. In the initial state (the Bridge Device has not been onboarded yet) all other VODs shall be reset state, so they can't be onboarded until the Bridge Device is onboarded. Once the Bridge Device is onboarded all VODs become online and they are ready to be onboarded (RFOTM). If the Bridge Device is reset all otxher VODs shall be reset too (they shall be in RFOTM state). Below figure shows the state diagram of VOD. + + ![bridging_vod_statemachine.png](./resources/bridging_vod_statemachine.png)) +## VOD list Resource +- VOD list Resource (rt : "`oic.r.vodlist`") maintains the list of current onboarded VODs. It has an array property ("`vods`") which keeps the list of VOD information ("`vodentry`"). Each `vodentry` has 3 Properties: `name`, `Device ID`, `econame`. VOD list Resource is managed by the Bridge Device ("`oic.d.bridge`"). + ```json + "vodentry" : { + "description": "Information for a VOD created by the Bridge", + "type": "object", + "properties": { + "n": { + "$ref": "https://openconnectivityfoundation.github.io/core/schemas/oic.common.properties.core-schema.json#/definitions/n" + }, + "di" : { + "$ref": "https://openconnectivityfoundation.github.io/core/schemas/oic.types-schema.json#/definitions/uuid" + }, + "econame": { + "description": "Ecosystem Name of the Bridged Device which is exposed by this VOD", + "type": "string", + "enum": [ "Matter", "BLE", "oneM2M", "UPlus", "Zigbee", "Z-Wave", "EnOcean", "AllJoyn", "LWM2M" ], + "readOnly": true + } + } + } + + /* vod list Resource */ + "vodlist": { + "rt" : { + "description": "Resource Type", + "items": { + "maxLength": 64, + "type": "string", + "enum": ["oic.r.vodlist"] + }, + "minItems": 1, + "uniqueItems": true, + "readOnly": true, + "type": "array" + }, + "vods": { + "description": "Array of information per VOD created by the Bridge", + "type": "array", + "minItems": 0, + "uniqueItems": true, + "readOnly": true, + "items": { + "$ref": "#/definitions/vodentry" + } + } + } + ``` +
+ +# Bridge Framework Implementation in iotivity-lite +Bridge Framework implementation in iotivity-lite is composed of following components: + +## Iotivity-lite Bridging feature : +- supports VOD feature in iotivity-lite stack + +## [Bridge Manager](./../bridge_manager/readme.md) : +- manages ecosystem translation plugin modules +- manages VODs list Resources +- manages VODs state machine +- interacts with CLI + +## [Bridge CLI](./../bridge_cli/readme.md) : +- operates ecosystem translation plugins +- handles CRUDN request from users + +## [Translation Plugin per each Non-OCF Ecosystem](./../matter_translation_plugin/readme.md) +- does OCF Server role +- does non-OCF Client role +- does data model translation +- pairing/unpairing non-OCF Devices + +![bridging_implementation.png](./resources/bridging_implementation.png) +
+ +# How to enable Bridging feature of iotivity-lite +- CMake + ```cmake + OC_DYNAMIC_ALLOCATION_ENABLED = ON + OC_BRIDGE_ENABLED = ON + ``` + +- Makefile + ```console + $ make BRIDGE=1 DYNAMIC=1 + $ sudo make BRIDGE=1 DYNAMIC=1 install + ``` +
+ +# API +## oc_bridge_add_bridge_device() +Adds Bridge Device ("`oic.d.bridge`"). +```cpp +/** + * Add an oic.d.bridge device. + * + * The oic.r.vodlist resource will be registered to the bridge device. + * + * @param[in] name the user readable name of the device + * @param[in] spec_version The version of the OCF Server. + * This is the "icv" device property + * @param[in] data_model_version Spec version of the resource and device + * specifications to which this device data model + * is implemented. This is the "dmv" device + * property + * @param[in] add_device_cb callback function invoked during oc_add_device(). + * The purpose is to add additional device properties + * that are not supplied to + * oc_bridge_add_bridge_device() function call. + * @param[in] data context pointer that is passed to the oc_add_device_cb_t + * + * @return + * `0` on success + * `-1` on failure + */ +OC_API +int oc_bridge_add_bridge_device(const char *name, const char *spec_version, + const char *data_model_version, + oc_add_device_cb_t add_device_cb, void *data); +``` + +## oc_bridge_add_virtual_device() +Adds a VOD to the stack. +```cpp +/** + * Add a virtual ocf device to the the stack. + * + * This function is called to add a newly discovered non-ocf device to a bridge + * device. This will typically be called in response to the non-ocf devices + * discovery mechanism. + * + * The `oc_bridge_add_virtual_device()` function may be called as many times as + * needed. Each call will add a new device to the stack with its own port + * address. Each device is automatically assigned a device index number. Unlike + * the `oc_add_device()` function this number is not incremented by one but + * assigned an index number based on avalibility. The index assigned to the + * virtual device will be returned from the function call. The function + * `oc_bridge_get_virtual_device_index()` can also be used to get the logical + * device index number after this function call. + * + * The function `oc_bridge_add_bridge_device()` must be called before this + * function. + * + * @param virtual_device_id a unique identifier that identifies the virtual + * device this could be a UUID, serial number or other + * means of uniquely identifying the device + * @param virtual_device_id_size size in bytes of the virtual_device_id param + * @param econame ecosystem name of the bridged device which is exposed by this + * virtual device + * @param uri the The device URI. The wellknown default URI "/oic/d" is hosted + * by every server. Used to device specific information. + * @param rt the resource type + * @param name the user readable name of the device + * @param spec_version The version of the OCF Server. This is the "icv" device + * property + * @param data_model_version Spec version of the resource and device + * specifications to which this device data model is + * implemented. This is the "dmv" device property + * @param add_device_cb callback function invoked during oc_add_device(). The + * purpose is to add additional device properties that are + * not supplied to oc_add_device() function call. + * @param data context pointer that is passed to the oc_add_device_cb_t + * + * @return + * - the logical index of the virtual device on success + * - `0` on failure since a bridge device is required to add virtual devices + a zero index cannot be assigned to a virtual device. + * + * @note device index is cast from size_t to int and may lose information. + * The `oc_bridge_add_virtual_device()` function can be used to get + * the non-cast device index. + * @note The function `oc_bridge_add_bridge_device()` must be called before this + * function. + * + * @see init + */ +OC_API +size_t oc_bridge_add_virtual_device( + const uint8_t *virtual_device_id, size_t virtual_device_id_size, + const char *econame, const char *uri, const char *rt, const char *name, + const char *spec_version, const char *data_model_version, + oc_add_device_cb_t add_device_cb, void *data); +``` + +## oc_bridge_add_vod() +Adds new vodentry to the VOD list Resource ("`oic.r.vodlist`") +```cpp +/** + * @brief add new vodentry for an existing VOD to "oic.r.vodlist:vods". + * This function is usually called after `oc_bridge_remove_virtual_device()` + * is called. + * This function DOES NOT add new Device to `g_oc_device_info[]`, but + * just re-registre existing VOD to "oic.r.vodlist:vods" list. + * + * @param device_index Device index of VOD to be online + * @return 0: success, -1: failure + */ +OC_API +int oc_bridge_add_vod(size_t device_index); +``` + +## oc_bridge_remove_virtual_device() +Removes a VOD from the VOD list Resource ("`oic.r.vodlist`") and shutdown connectivity of it. +```cpp +/** + * If the non-ocf device is no longer reachable this can be used to remove + * the virtual device from the bridge device. + * + * This will shutdown network connectivity for the device and will update + * the vodslist resource found on the bridge. + * + * Any any persistant settings will remain unchanged. If the virtual device has + * already been onboarded and permission settings have been modified when the + * device is added again using `oc_bridge_add_virtual_device` those + * persistant settings will still be in place. + * + * @param device_index the index of the virtual device + * + * @return + * - `0` on succes + * - `-1` on failure + */ +OC_API +int oc_bridge_remove_virtual_device(size_t device_index); +``` + +## oc_bridge_delete_virtual_device() +Removes a VOD from the stack and the VOD list Resource ("`oic.r.vodlist`"). +```cpp +/** + * This will remove the virtual device and free memory associated with that + * device. + * + * Delete virtual device will remove all persistant settings. If the virtual + * device is added again the onboarding and device permissions will need to be + * setup as if the device were a new device. + * + * @param device_index index of teh virtual device + * + * @return + * - `0` on success + * - `-1` on failure + */ +OC_API +int oc_bridge_delete_virtual_device(size_t device_index); +``` \ No newline at end of file diff --git a/apps/docs/resources/bridging_implementation.png b/apps/docs/resources/bridging_implementation.png new file mode 100644 index 0000000000000000000000000000000000000000..075f991145e8d799dea3fcad5e659f453d2670cb GIT binary patch literal 75651 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%0D2MTe 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 JH7~*O{{nggFH-;j literal 0 HcmV?d00001 diff --git a/apps/docs/resources/bridging_vod_statemachine.png b/apps/docs/resources/bridging_vod_statemachine.png new file mode 100644 index 0000000000000000000000000000000000000000..90277fdf0de75f315a69c74b01be4d5cb7fd4e56 GIT binary patch literal 82730 zcmb5W2UHc?wk^60s2cMqYmJDl~^v{<%Ia^1djr;l{ywO@_0vI_aGU(Y*V z-G$^f-LzZAdTqKRDV_montFO^DY?3OXY8)W(CGVYI-aoc{>ED$>9-viS?#sz+bK&_zzOI(eYRc;y*s$B~uaqjtEQ{ z-xyP-uBpk%$;r5D*Ph+Gf3~$zZQkglMSN9R@Uca1LzI-W*Ya5Yx5SIQ-ecD%yWfhK z*N?VjCo)rQ!e^3b`BI%?OuZKSLgz+){Foi>sR~$KU1fR0jyHXl?vhh9#IARiT&}#^ zy3D6vU_10Ro?49-@5yhHk8NgGQBiryshF(r#i01c+|akgvXKpV&tkXsh`jq~dtvrN z8RyZTDjC6ektBNXL8F!bty>$Jm_mydXlZHlb8=G4-H5lCw|}t6CF|?Av&=nu^e7|o z{NERM)2D@S^YFOEt@--;&Z+-{7v(pdjBTbr@p3Y7X=G&NhuR_HgVfvoH5Gogwtm0r zjlXrysa%b98iUGT4T%$a}s$R!MwUK6`&7VRAlZC;#`3r}VYvS@SXmG#d~$ATM(w zw%LAYXy}jbZv1I+?4U0GMQXTqHi!6kOv_^nVkiFR6Q!o6X1X!qZQ^VEp+P}6n~RAL z7yrM1^{+d-`UEPJ$PU6a1|A!9~U;3Xe?Zn(e zb{R?UwUvk$GT0}sXph+TC(oWWgwujMx~jV4V|59xgB+Nb;B-izsN3+{+a4^8r6p>;T+~)UhwPY4F3M~)JYmQ z5_9fXaxp2%$hs|WS3uRojSYg~7sRgp%!F~5R$l$S_rQU;t3T2Yv)5b^7K>zKXMYhI zIxsw(9P;GWty{yLu5-i9GyR`M#KhL-+pc%Cw{O|LJ0>MX&pQ_jZNUNCE}WfpWMhkh z!@0S+E&chkdGqEYW5;aT2Rn+LGYm^Z1PpY)6<&NHy+_8)F;u`HY;#C-^aN~UU?AUN zpg!)*Bd^)UGvsGap7e~5%eu`Dj*=ZwCn|wOP69YGOkC< zdanKHeH39hK9W&BKRIdZ;IKTLk`WdX%&X<*;&LHUjQ`lNP)iBBo;%suGk5;knQmCB zJ9g?Jo0ORA^p*ZU-!95^6ggeJeEDTu+yOd%!;(L@X=QDlocOu9wR5eSL-_TSo{MLB zuX|Ow8=IP*BJbL@tGD{m!u5s;$ByN{ zdiBc3$EUmdQu26yjJ=JiX&-Ey^Fo+B`NYeHA3s7R=wJ=c_BUFUkB*LBFJBeo;~QOB zocZ+W)4-Q#u2>2x#?j7FDPiI1mTdFGhYvTu*v-VW@WaUa)alb})3t&W6cp0z6XPAl zax0USC9X3Jp5N0AiZ8vrb>PfHZ9P2^{^Q4w@Adj~*}}rY&Q2)uGiNM9UubA(w2V7` ziI1CmaB*fp!$^Vish#+9g;0T0WHPI`?LIlV^~Sg}-s`IieYK&mofprZy?3PAzFqpp z=+B#*wvstia*r-AEg2Tt`?he7!K5Zi7hd!nW`D#Ld>#R^1y>R-VlJSadFj0$okqsv zr%y#(CcANdHpG)!!SdAAQfe9+F)=Y&_xVx89tV5-6ym?boTN})E zBx!{)`Gx!P{FqHgk)1eef_J&ss(^lhvimtKn&J*KHjDCN=LtzEDMWdW;f~@3SliO< z(9_`H<}y!rj@E4Ri;7&>|0CutZEcAO39W5ykr5HCKYwyB_y+{&IEHGR|7kM<%~>Gb9Pyp~s1wpL)*>*(Y(Gd(>$ zGoz-eT6ANq=9lf}jW+`VsOW-2L*=~JN)#i->H-;d?b1jK|1#k2=63hqy~ux$hzLeT zMxGZ=&d%OrZKlS?^gDNonAPnT2#pL4)p8#wC@6R#<4&&vf7nh
    epOZ({e@84g) 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%0A-Z!$A2W_Seh_s<*p7P;K+BA-3`@uJ-7 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 k$rG`4 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 z zH}>UaBSv`K;jhqdKL}(icx8ENsz7Fe9Lxpn3t&jVC}sx_o}JYiEFaSLuB@t9%5#pW z4KG;Pna9(=ED+A3r^`d_)@RmJUTrccU-CIp^6Z7+vtk{ba~_fg=kutu=ZqC-LmV61 zw_ovzd1+TQ>t}s@`sjl&IKu#4a&b9RrEu^zQeQwbq}SWI3 z;4s}6ymC#*aS^rZHK7Ua*q108MTTRvx0^8eQVlSlrk2 z_39FJQSY*G*+phzcDTk~Wv_1kOXsJ57a9yxJh`uy{8}G9(y(6T6@p90eU z5kw;z!mrEaH(fFNl5#C5)U!7&qol9o)SB4oq2O!VQWWrL*K@_fz3Zhn!$E6QpRH=JJf1bApSs$!@DXga4JYZ({D9`U=mn_-i4?BV3q35S{CGf#VYU1jW zhv?EDZPCP{-e+rDHB2mIuY=LwD8$zdqzs*`$kANn2Mw zrxky^op1eY`pK!B3iE=+S?ibbTe)ue=p`Kbb(H(s)gwuBvnCjm{rx-Yn=`+b-Zg*x zsJwZzjdV;PiLzo)_n|X$09#BdD!EI$#Wqeh4F8yBR`gl4F+0i-bD2VTbW^Eo&5)Lk z(3vg7#Qc164$Zj@ZAU2B#czjO7<;htU9XQ0-yPLTX^*JGMfbz8o}MbiKKRa2T4JA! zeJ@Q&X>b9PCvI{aU6lQseOkN9_3XtB42b?$%qr*pru0-XX0QdSBieIunY!J$8Etk? zII;iR9(w6i1sNHAJ*UTBbU3p$Uuxbn+1e8QzY^Xh^&J_%UC_jPCil)A@qbm-6P%qp zuBCZXymXt93nMwx=f92)dJOd5jB1}D0G>O4VR0{gY>P2Bw=VI-$Ulga*y@%MEl5l# z^m!-lRi%wC`ntX#*!KMsM&huau`9)%0=g-$7 z83rq02tp>>|4Z zzpchUrZNS}JyDc$L5(B5mEAQ*ERf-#d{sGzj{*<#3x|z)H#412 zXf8T#{PLm3j5;~?bR<#ZNO3w|xAlNxaqz>&1#+-W9jnG>|JGaEUe`v9e4w-1^u#yf z4_)I;{qQo^Esjj=99vxZ?lhdoaaLyL*pTjExc7Fv)FJopOJ2⪼ba3V;N7m+MZ-6 z<~gP8`}0Mqv#|+v661F#9Y!vjDdYwW0Rd6pcXcNmG&57)whhQe{{2Z#*TJU0etkja zNHm9rHn7p7;~v=0*q1LNLC&ZkfIG4?L?9;wtogC6@T*Z!suhnb*Vh^Urz%v@^zq3Q zHQtJ~y28^8d-b&IC|lmyDvkCS6`ZkETz}!t0KISMb-A{^B5SKF6gIHl`E(?Dx1FtR zF|ZHmPZ)o~4gU13vJYeeHfUXCVM-E9u(_6ZFcrh_>v^esudz2o9y>lZ$=aPcSluadl!gcMH zrJEfx!HH5zItzVllbjt^PTfcn2-U)uru*}5YPLtw?zyzKaB}BOvqFn6Lqjv$>(Z6C zBHM*Qei9mp+P&c}2SIF8s#?-_;oV2cDX!4WU{WyHG+s9Nz_76u0Sp3w!-r8VC2;Ud z+8cfbA}7?TupQlhb89xfw5f9Gm9DMMt>W+!$8^Pwc*^N{0dy z&N6#-61gF>0ET|OS68IH6aWWC;=QT;Od=%DhyUbfx|V|Jzz=F{zNFbMCs?50GF4_+ zI~M{ZuN+=Vdvh;sBypFPmJB1la7F1r^<%1g7!uWRDL8mZu!+j|-gN?Nu$*z=Ym^Dl z{iJtG&0@Fjlt6)stKl@H9sqU1+hl(D2v@_>I$6o(6^W}3RmGU?-=9GHed#e=i`FZy zJNF1mJB&Kmo3^83kthuhESub4!p>`MX;~nddO94+pWXn@dx?oHfSe0b(j)XSPW#HM z^Lb-lrPTq%< zZ_A3@*^)s5g+T=tEtm|7lz0of&J3&&S5;2yCT&6Z;2wEYTx_dK@2mn_Rc9`%?V{@# zoUdwY=~v}jw15IrR#m-~D7h8mdv=2L(cPXGkFNq`leGE=s0<*pnpa8B{gk-&e8|^nWDTSyDB;cBc5c8v`uV9uP*FEil`)jnXTBAX87d?eAS8RS9 z;Agi2^jU%tA3(J?EueFK=b1^^T3`OYZY$XX3P=j10zhJs-5XZ9dv#Om7AT`g6Xdk4 z`Vu&qqu^0U)$$$33&T(b5F@##>KPLCUpSgCal+F2(fuFq;y?r^H=Kii%g)@y!$4rr zUxD3w01j2~en{2`RtH}~^?KlU-nOd(&`_>^k8MJWR9$s-1DNtP&8u@=V#9{k4@Rc& z1^EG90+q!oe8<&V1Lg==?M0xHPx8pGbX(oO;yYphc6>uKl`vuy?2Dcm$dGgv2r&F5 z@R)jUF7!H}5-=el@&-EjZL(Z0Xy71Sh?*2jz5_vr)gV6Oc9>rSS3>Vk#;yi>k!yRz z*;jGKq2o!(yTWLJJpV$WI(=PNSz_ZxU@lmUGWbx+LUaT6i{t|EWOj2M;0M^P9yI1z zH|PnppR{l}Ybcfs=u}ur${A%j_9RsB4&*QE;yrT2AhSx`zeqvO0zyb9U6RkW*5?2l zy>%pGFD55|>LVrZ7%P?YkWI^;FZQLKDy7Js;=faJ84#@t0B2aASzm}hI6JvV8X*Bj zuW*r?n3}RD-z4Y%zQ_koSA=OG$*h8^Cali(#vlR`UVmtBcTlDOgSKH9~*rH8nA2@RW6K;x5$RaQWJsYlDWlT1zfiZ5eJiHLc1(^X~&V z2X(lr9*aF`T8e1|sUV2ZV>YT9#{|J1Olgj*($vA55XMia+xDa~(}!UKBj(E7G}={w zQS*kfTnZ%lWMSgx#L3EUe_Ap_CALEZrcux?2z-T^5@pvK62LmT(Q4~V^10=vY=6)T(uS$7N$S`vE z-YfxzquZ`TTGhpsWdgAM3{VkMLGhJtG-}Z&Dj>*!qQl3>^5=DiZ^EYV^Y!g5aWE9j zPTYm2j5x3KFi@;A!ovNSX#DWeqt)lU3`HF#eE<7WZT>N}xBxJM2asUNrq=rOSBG2R za|vw64vbz7dqFVW|Fi(um?uOKcj@QCn{IjsAX6k6b+2Kgd%1~9i2kS!ph?(Emv2t8 zdXA1m=-439dVE;?*`)LBLoH=aEp|ZA!GghAHh)@ks|0-5519IZDu*TJr?xh1cwFo@ zsw^VAJQy)k1*2ojy<#o^8TsEt{RD*rOp0GU@Gpq-oBuxM zL517LS}1Q|F?Ne~79ZDh%}yjaK-duBZM?vmLP5JQm~`yf{0|xO1mLT+b#R-O63CV( zz``J;el*}a{DDWh3wM_ltcSpJk<22NPEi0ia9aWy|DKg*4!Wz5mPD9Jj}3i~`mz)h zugw1GVW=A+ki`Is!eiCFa5em{5*~P7U;OKzIS>f#SY(|gl5rR^l(8hBD>)1{6rlj4 zb*Bacu8=PR!9G4K2RMFY@9=p(_Sjv7*+ugkWn>++wszPr;cSOZ7}2eOBm)>7I(BI3 z=svw=aoFwQQ4CYK4^k(Oxw}tbCddUA!Fe+2{O<%Bwp$LQR8-8#S1*1Y9c4_1toPBQ zBaXlQcc5=4=TKV-bSl6hUGSALc;Ua@l27yKgRV?F6a?7q<2{w-TC-uuFD!Q$?8J!n z{He&xFu~Lr{q?KKDYGjP?KSV@tDhoS0=RF|h*mz4JJ0hPVz@Msh#>p&0ES&c_Y%M- zOaKp8>gBtj$p*-*C=sezGw6x>`XOuuP)+1`1tr2f2~vTwlkMmS(zCWs#Zr<_>`~5j ztR1vSWcB9#{j#^dJe*k(3g3Ls=^bQ zS6|SqAi2@v&KpaD+_wF946j6fhMEg=BOj5|f6r6O$lOJxL&EJC$l~Ks_@w{m&(ls$ z{(y%zCGm4{<-jk?wd9%}&NAp|w0F)!w+7hYX1q4Nvn-NWWJm}^`2Q!X7TY|DCBX4v(||h;~qp;ZR`!sEA(q)9SgQWQ0kW^%mfH0j1da!9)Eu1ow~; zYzG&Jd3OKX_Zh0rS#p~;ITl;MNfa`6dNV#p0u6wrS!azI#gHui^ch}eJ|CA=Z z!tsh2i{gY+=gFN$K8iazC-23Tp}tK|*PQ0$Ov>jXQOq=m~(Ie=d22G z6i`v;DPPcd7#1eYt&2+o%4=ykxwzGgsSYy+pG`zfJ7XXRylN+}@rHmpPfI(hbUgOu zS7b=g=pnvAvjI2JC-Hjq!$=lH6QjtxADJ9Hoc=u`_3F%h1_r92LLhtsaB%nGGeCPM zgkswg5Zy3#`!wDFiOL-YS*}=8;(gvE3DqzVdD~$=s-dQ|5fjD`?0>nCJG!`N$a75A+&gLj7DS0O+ z2RGBDTXQKfu(|dJ2=r)uM14Gh{SwU65lX_@YKaVEj44@_HRQbF9LFX{l3>YwAZo1N zT?LAIaC&53MTY!RKJ!vUFNiB2r5#7%f`^bd-Sd&=W*k1no0IoD5wkEaAmPEEcJpJH z8#NfKy>>M?R@^cz1^s>-4a1=8n_0Ojj*vE!_`VU z9^y_xr2;G4280q635brkHKsA93Ix`sRNK3VaRg#CbRMigH;XNvV(c%#M+ddmCu{?! zic^A&h13VR{gO`Dihseff*L0&GSkzTu%?XRqzJc=LuD9Sq?UKr)Cm8(2ip(dT)mH^ zlw7jF&%)Am>Dx)9(`p!Q5`vdS+KjhD8!~K>M{(ofGFu)>b~`sW$V~vNd2qDq2|_0_ zYlN$VA4UXNh5^rD^QmC)I}(^XSB|@);KAFi{@4KB#(XVQl9*?=Ap}?Y6lS_BDz2-+ zX|l4mrm%N#sDK^j+*}<0Hlwbj^r9j>ln7Xa&(6*Ysrim!psCBcc9=0@fCc&gKNfEH zpAZf~@z>vP^WFHtj`gIj?D7TDxgGx<89Qu0d0-Tu5dqE!_Y2@6oGK09F_H$mrgu}x z9_88JO#1izs5Gn%kXjlos1!yqK1~CWhc_API8Vu8RQ`O;p z(^)up=3N(ipfp6mw?}Mxq(xhGYan=7NPgw5KXi3j&JTwo@=Z0HZhDIShiE(x)57uc zKG4_dQ1-pex@tbS0e^6LnZeP~5d~EH_Nr6L$vIA<_|j2h3$2IU@3v*?Fk)s)R8$nM z1+*OqgzOfT3NQui+BwF+sBv`rwcA$Zi-KM>5q@*o0e2@gcLvNn^%*roK7l+F4M1!8 z__`2dN_HB4lIp(x(hjz=bhNbBS&f(S*rV@g^&E2$Ck9VwNFT|a)Z&VLWdDX8`Er6# z_ET2ZgAK<**|eUeXbS7^-HWlTa-mO(#fYH8OQ9-F)+UL`isTZ1MQI z8>c^dw`fnn`~f8Cst#|j;^;@e{0jXeg*$YEetxsKTijK!f0R;F$fybJ^wh)z8$C5f z5+mC+UH`JV`O-_f>GAGal)|Q`Nm>q@9sSllwY8a0PPC6{k^!DgbiqPMs!}~RQ3^r%$&)9oWv*I0+W=GH&S|keb!OMEq{UmS=vP8wB1B*MkI2g#2Hdyy*DKoi zwCgUM0qX3CaYeRYaNyx#&-3QbTF-UYRucu=EB9hPHaAZ|Q-JK2YkQ0V`R1)f?d?}= z$r}oJI4fBqllwi8RFjDc;chM1-Xkc2XOiQ%+7b$qoja>kW3=q!yyLgwDr5q*gdLXk+i6X{ zi&jQaULG=m_gUvk@zy7AqxBOk6fShm%!Ez${!?31jFPdbC;*XfVQ3pH#K;D>z~dh6hsTp{Co%lnb5B%swK?vMf8b9lST3 zA{f-*pN_ZBFEW<$mD=W!JauCF63f_Sb3)~CMuFc2$=8ef2bWbIC9jvY{gQm`_F|6j z-3LGJxtXjtim)IwB|J;Cx@d^ccXs{|8Sfh#KDm)gLoh_w*d;r6)O5X(Y2}yNH?e2E zbYs{}f1SyoYlM;Pu zA|j&DW59%#$#`kr<yT(O*W%u%DiIHd^cU!)PPotJ^8FeXGkUZ*rMfB;>e?ZR$6eO+;1nhk zfz1O}N8b+s0=S+-6+NESO#kyRU~;Gy7a=Kt*f?nusp!M4iJ6pYIJs!vWl)1XKiqcw zAH_F(b)Q^6aql{p+8F0u9%*}O3gdbPQ-49Lw!s|_Mz_?SF`Kf5N>*k6A5#pqbm!@gVrC7Mfw z@!6q`Z9aoOGu$@UHBC&*KtK zQ1!b{e5DDfyKwnXu0Qlp_Vz`07=?E9%UZwft1RzmHWpZShe$8F^vFG9^7p{Y<>jZv z$I%69S}>`}$9H)R2ldkYn%sRH#N#tpycU#)#}4=2RM?@U^yW*Q6><}X2Sfo`b7QEA&9KmMV$71~gfGAmgwD!}0v;^P~WgtCE?LsW9_x8s9NwM(>D zy%T3x$F`+A+hd+(>$v)k`^{ABE@9%bxJ;zfa@9cP_1WERD77hmkVrBD2w<22;%t$h7^hKn#Z z(|sMEnI~y9`q7Svj~69QDSLa1S5y=_Hi(0rg))Pkc=F`(XDhCd4h!G2sxBRyO$jZL z$5wA5SxkCU)ff_l9xlw9QyE*(Gc&IV8F7lAO2?$DRvnXuKYOFz|Nm=#a9v~YDO_MN z#FrlvP?0)ANdA2T>b@gS{fjX}2pX=ElF1T>ccNlq!wt!~z?|~(9>bn!GJ66gImVmd z?z(SPK~7mAkR&0Q{LtL&|M0@@ixc_v31LwQ${KavOkb5VB-dorH9ImKP1FA9>i&pG zSK<6bm$8{*gP83@HiF$(%vMOnpt(%@^*&-^a)+JS`Fqs&m7fUF>8+7B68WwYla}?t z*XfbYKMm74KZZ0Ro!Zky9nB>{l~=s{M;POjZM-se{ZV5F?kQj3ge3cJPL3}?OH^!7 zmRsz~h^dNxX{VfQJ>xYu*sexD+LJJNde+b)#q*uSujOhm=**g#ZItXH8;8>@dJcBn zx!d~i9{ct4#=#SnsSn>;%1Agw4)~~xd@VtWG;`U?Svxjwb zveB7>uW|-X+lP~=SRy|F`M^YUj0gatqH$(&_k06IO>ggV^~y3iX9ncW9~5Z}0Wwlj zP(s};GS19vV7Di1xO0mP4{EUMcHDWW_0h+<;?0|<(yc$mtv9uf=TCWO73Dv)sEe^? zd;KPsUD;!PXW`pt!bH#Z)MP?MxB6Pyt+_VB4_o*46fOq*`*- zjYu|eH8g#3hkg#-7!<&$C;WE+dfv~91 z(WOGVQXppf$=gR}MaU46dD>Y@3}5el79hMUL$fI385Hjs-v~*Gbh2ZOXCM^)bdAJ` z|Cbi#u$yw*clt6>;+w8^E(hV%z3Tu6;VG0k!n^!E&c^)PYaG)|_NznM4LroH?Vm=j zvTH>>U1-f6K-&~>Bj=MRyO80QLsC2X};btW>)$fxi$YHF*%BNTl(zKOMfI10|q7h{-LW@fNTGX$O*(>62&x$h3=&N_ANS*v4d+O(X-|j<{@fJNLZSH@Q zcaF>)+&3Xp)d!y*2mU z!%>Re6kidSpfBlr;&b{DRVr8r1IWnySx;Wad| zZ{OX{txFef-Liij|23M!)WlSRea+&cfx5S_o?c6VslksQ7SEpEG30K^JCMCJpD!g- zSEISu<3~vZEimlAZRYK9Kws#ZntXXFn@qbzvew57S$AWxNLjxs4uhLz>6mG}PtZ2WOcc%hi?a-rf^uypp3L9D7;@+A2Tk44!@$*l)%TMB3z-Wy@KA|8IkXrWSyL z{NP*lRt7@kr;Eo0I91ft%q34yM42+hrmWm6GQRWS+NZ(7sbTiyc87O!u3VGFMYV5h z9jpHA%nq`DKI;E?(Qt0*X!YUT?A)U4;bgIbn}439-R05lOwS82o|CYyv_!FTul8hG zn%4fNSzRq-^Q5tE^WwXsPtW`^m@6vEELU5Xy7S$Y<)>$-E8RBu`DIiFylml=7OMYf zHuY`KuFl}bTQ6@bjaF#@1}O4#<4irhCh^#+%*4#}8OE9cwSmk8Ju~PZqQjIRon5=W zpeX=wA2bWzUS0$4DyTC5b#4HK_|)9|cYgGN%&(JhRYLufve{M(yrsSUx5}8Ze2z3B z-BBqvuFTDA`R>WME$Du<)(>Zky|f~vC@cI`Y$JOnyZhDO^R%aegt@e2Lt@maEA*Mq zJHC9)l}YnLLv_NJr&bg4-DIpEyo?ia#j6Rt!zNP1i3Zt8nwkGIoCL^~-k$5^}-{QHjwRXrx z-=Q6-CA;dt+V~`0sJ{xN1V z?fHlq*qr`n&z?JLDtdgw{&rKt)y6ZRygNX*eeN%@OrxS2}w8p z>TnqUnOlSmWO}SqK}{{7o{f(={j`+K%I&Jki?PzK14JNe_SNuKIYIVT$<}MMtSY`Q;c8M`SaqJoQ7RKg%8gq~hsQdrF3b@R?&kzulT^MpMd`=6=5cT-9E z+jOzE!7GhtCzA4#L&D*@3}ZeaY<4VwqLvCTp@;=|G8Xz4I)W&VOU=4RFm~kiYd??| z8P(oySCf)Pzkgr*M?pY65NLuL8YADmi=__)(glJcF?B6j#nLY^b~|aHK;IxGg_$#q zBt{3G3nOzb#>9v|t8{qBb@0RLFJ>eJ1@NQOA+D%s0ze5xh*2&sK{+5igyoSVj57~PDqF?!I9*`2WIYKsex~uei)-E@2r+9Ee)-sznLty8Nd;*=x(rBbFEXK7yFsbV~^{ zm(TqPh+Qql&;II`=>mj;atMNU3Ua0}6b8koI)H}73u*iuA729Ijy5R_y08ldzySyd z{2>KJ=mUGGiX0r`Fy_r?^g2)h+%_=e1G+em+6!w+XrhxCZ-_o$i3xWAI}og30vN^!BI#32?QLm+K8_LpxMoeXPAu2-&^PED z(54FjAETbp=}%(fXd9p=0Iz_ChV~eoGza0WL`6kxaRpf+Mu36w*2uu(4OSaY1Gz;8 z#kgh-MGZ14{47jOMrSJ>wy?m$16f7Z5nXZ)Tn4~gvWxsf=jz99F8^+>VF_ne7;U4V zMHWIn!5hU-7pj&a(Y-HP;-c2#)qfX#o<#AU5z!S@5Wt+s}Pm}9R=WO zcPCL1Ma9K$-oL*H!@M?<;=}AulCMOC_7h=t`59WhckuiS^b#lfAyI=nV6hzy{w&pKc4p>h)4ykAEYEz`)E(b{8z5*n z=az&qaXe<&WGA_FjE0+8V)5w>?)&H8S-EpsQNH=paeGh^xh_bzOQgl##k$QULPPRnLiy1F(|70d=YGLn+| zUtrO)23j%J zFL(0+i2oWGAbk;@zYAg05~GIoFY2LAspfL@-V1X{Yehv*-Hj`I^vDOL9JJxs@~Gr~ zS;m>cyb1Nti$Cbf28|jW69YvY2-9G1U7|A~!#H*87sBzF?h1@z#pU5h&#`8yE;*Z}(j z(h7V=KaG<(FKBn^DaXX#n>TKh0w*jj<>BYA1{3S{iw{fnq6Q|%Pvm2w*$$1E>jZ$% z!%oM%IgN9RDspnT=Et)HK`C&_S>m)_y>zMO&6^6SQ4-`qYQ25Sw{G3H(NPySx4xw# z2xVv~hvyAi@mQFdQx#6IFftk*JlKj70Q1$7FJBfF7cT`JGe3WpPyvAgku``MOe`YD ztPOTRfC9}T{usIpXgi@rA~qvELSX`i8r~&k3yomLloS+3pXmTLX>9z7m4zlS4J(q9 z<9MuBVfEQ7*38f7b?8-5Lb{D3grU?FHP4@a0h}x}Er8C)dxeE5z{g8Vy=`pzhmBlL zoH$`;H`($E0KqaMDFP6>vT@utK6#~nb}Gs!7Zla4+qZ*}0^<-P*+YZ_0b~xn>gLS^ zfssP;)eQLHm}Q82<@L*#hb=AH6{6NYQi{;xRXS-hn~L+LzB3G+n^1=F^B=)D?xY7V z$7ml3(UV)XIb`Gr|2V!%cXxN?v$1Er7BD>?E)dEr*Tm(ByedHrin^f>aZt-4{lLU! z7A7VLV%w2NPQ?&4wY9C&PZwt7(LRnGlT_Zo?iqX(h`{;5B@L83ZD5k2xInD?bo{2i zkB<^&R&`3Po|9*pZk+dGVld6z=u?uivNH5L`1W`J|MFWQ&PPDyQM02@z#m&14hd)j ziyqfloT$Tftg$l~mn$6en%fn8&~yj{9x_jB#w~0~6Vuaj($Y`dznsU;Cj%Z13j92< zJirh9_IG4Ki~)MDATLiq#oY#|7gkxc>|t>+e7d>e#NAhc?S`O~1+%#_SBRF7cOqx& z#>%zjnc&YP|KHyLUaSyPeM$nIhq+thhcq_6YkTho5jE_O8%A>}? z};vyf=~alZfgIUpR#mMP+5DA?WCW2X;1k(Rx@}nVAue ztuU1j`w`0Y3m&h-EtKth*{Mzv zw?bN75|)1Sc~k5ePzNXRP!MeF>2_6>K>Gm+9`d8hg5mZ-X`^uSpn;lh{YA;Bj2ACo z+HPOXXmXl|W5CuFBauPYA$Ui?pIAk z%SKva;_v0904QUSUOjKTKyF;cqWMSee{g6ZdZ8h3v%t#dettf0x(So`wr#UWAh2IB z0&1h0p&NKGa+W750*tmiR%Q6EwstF`4`Lj$w2bJ;|6b3u#TSo=_kPbNXeX_Y9lM>F zSd4794;DWikPkt>tAg$6u2E(^pRJkK8xb0H4e zZ(Ln0J{yacb8olQ zP1xB9F)^*p%{NdJAOa(wMo`f6&2!r*Dft)?954;qrI#S@#|81fH{`(^enj)`i1$3q zd$9fB-U-08wX5sN@z*RRUemEimtg*mRY2z~23ezF&hWprSmwJ?!EzxRi7})Lrq*CSGefc7CVwNO<**+xs%{(Z!CEgq6N^h71`MlW5%lw z;aDW6>jP9f373@0p@ASJ`~w3p2SL_n7aSa1!m(OJ1<{z5R*8y;9I&v^_l?OA+q4PN zyjLLGVf7?`q>O3f10C|!#Oh+o%yCelj5Wls<)o+ggUJ~h;^N>Svl3*)S5jBsHIL4s zWhMq0^)&+tHJa@JSn<$t^I&>#5mle^^aS zSvf4KF)?tzFT;g?v`!8WZnMjr<8@1Kc@ z4(JG>>V-fYpdfztLh^+Rv}2`lY+8AGSlLiqm5z=ZI;D7SL1cnz8!sxP9yo$0!O5xP zE2v(ljSf`WOtmz5maOaXw@(E=qgm>tt_-_+lP$+a- z(6qJ|>~;!h>gf23CB=;l)^B=hDsX0u_rI^VBVK_(AB5RW!qY!CJQ??$9^*tFfw?*K?`ZnP z^m$KgE39jYHhDIqw%h1*pMXB|+U3iDAw<;bH$&@QisOc`jtRUN15Y~5;86TqYPPJp zn3*Gphp%71#*25bb94K7d8vUq!BBF@#L>k4Um&?~fpfeZ!1>wvDXC7W3)@kY<7R`9 zdidM7$E(vZZvQ(acE*S}jE4_*0)Xg$QL3$Z;D86RA@uN)K32$bz-ayNHS2VJ2}eM0 ze4L!5TG#i|+Fd}s5QQZq2BEGZTMy7%78l3--%>~x#m(>|-2;@w^IDEs0?-03NwAuV zK=hGsA=mxyQqZo6nc;`l5?}#D%a_K4#p2V_5Q!Eres#T|U={9?OPARG#g!;~d}1}U z2AtX;{th=F-nJ5X2`(#WKx|hmv93ekMirz(WWyf-iQg4y) z!4+*^HfmH`WGh6Gtv05vf*4%AJXa%t$azlJ;D#Gp2Cz8Dd{V)a`;B)^lgzAygq-!0 zb)Ou_{iREZok*sY$cYhhk!Y>pPVK<8g)4C2>(_ICzaR@D{(XBVHd`Gvf}ACdcCeB| z=?TtGPKy9ban{fL{u%-15=&hFF9Pif5NAr2`^G3@1l6kYnaW>mB?U7;E9_=jH{Hi`V0 zLhy1R@={dq@CyOPPy{J;mLOUnkhN;4ii?V7KCs=fb0^(5mdY~ab=ql8{$WK$b>-rl!CTAzS>7lf4jZg!v%2tneK%)deoX$R>hH>vDx6(P;Mb zS+zb!+uGT+4hOm|ed+a=WQlyTyRE@ z=Nffw0AFZ+Lw4Wz_AT^NiGaK`_ML`@Un~2hENB0Y=h<9!-K!yAfgo~Rxk>!#bV!N#7t(%0v7OIx_y5t{}+BIMJ9=N&wbjp48t zW$ypUGBdN;fx+r&M}>qi;q7hHyCW)Yydilf0ZjoeCjiBXSFSt+sDlIr5Z`T%*6&kL z=HoUcCUDl!>X4CP>*?wF>GfJRw)Ctl1$?^>R{2Skf?zOEJ|bJkh%t%~tN`M!nyM;h zD3SAH7UlrpArB`)46=bpq4N-m&i;Wt%AWhRN5tvN!|&`P2#JJ#Yr=jkthW2oZ<%$y zVTEF;0k0l}7$1*HW1dnbLSp>s6(aP?*tQE=N>dO&Ci^pZ!;fg)kZOBINjT+)+w6?; zzVP`2LCE`R<*nIqn0jC8ia~}n&H7Hw@PX=$`9aJXuh^70j^3sugHW@Lj@l(W4&Wly z#%)h3Dx6(hu*YyFFt~`4z@VpqfCbb(Pi8U>=A-(JijP-^4&BU*t9TGmANA07)DRXH zJb+mU`0X_|HT{Bm98eN8aX)^0_V)Hd#|%jwwoH%Oc08TJ=9LZImEjwRCA= zcJcY?n(^^ehfPHNn_d1Y?{Blh6NLC&K{YryF|sy1yk=(H_z0t*V9i^G#KegyDXU7_ z-OdfqDZ0AcC&h`H<)tEDxzEp-OG;9Q11E@Ew}e%A)~i#?%je}&5bl1Fm408kl$@OY zUiVv0CiQ=~I-vAMp-oPOM^%B~IDhss@>eGp7mRa;cjqR{QhEYNDLypnY22zEkCGuK z$JO+}y7J@17oD?b&cMlVgRE>}S=sf!76A6i9S*`5DK3mkr~NC?gsG~grl+F=e_A9( zOAB#1cK&l}7CWi$>KR{QmPpWi!Tv(fws!rclr<;2HRsigJE;ld1qPFz=gKUqmS2MC z3C%@=%@Y*wjM=vSEIiBK8~Vzq=qz=Ni|}iHA|!&U;DhEYy<}ndS!rXpkbR0gY3FYh zHWJaJH65()g+ju`?AKEg{uM&&KGk0Bu90ITbA zC@CqC0*NVO6t-XAFlak03Q8QbJ3$Nif(xetSw0{UJHsPKj-V6mJPqUfh6dn2#{g^% z44y*4Dkp~~hA&^gZpCEr`{PY%KEBRMThAxSt8FtdI3G?;gcNNyG50%NT0#j(GyMJg z6@mIXs+gFGADl%+1S{+Nn^b>>wRLE`mAL$>a;ChSKhY7JmBv4_3$d~aov+y5(M6fc z;?}2ih@-77q*RdrI)3b27+nP5AQWQAL6PMGo%2TS!k5|nGZmExy6lbf+d$!yvd$N} z$O3$QXYL0fD}<^RMID9BWF+^^#Kc|RajB|4(!~<$>dQzuQEVZm&0EnzE{xI&6iKvh zR%b`Y;zC>jw{SXDVG2RiUdfOid>Fm`rR_xqqDDnK?@f3-myX_60ksa=(n_XfaQOArK<#5DA@dDatql85JfI4t=oJZsxYQG(3Ew42DEL2uirT)FF zmEkSONUURsNKRh38}$yIhySq)Ul3u8@bWC+kzm{D#PCIxO9`%&8@DR_3qc=z{v_q zqN?f-Q{iWurSH{y&XxzwOMqpPm){Q%b0t?xo6wLS(!m^`m>6|9|7^L1ln5to&p!aD zLsxAA{e%F%$S#cdri3^KM#e)2zMpwoQBvXwlnDD7IhCEAC?P2=y&$k2{hGW3m#$J*R7`vq4B+I{*Cz;i)|J^Gp`n_&+FlBe zD=1P~vh=99xu2e)A|^&!k|vobkg4D{xxF(0Xt+8P2jf@(Ff)`r`Oo3cXq8$*(?$7S>1eK>8 z)m$dongJYyrnBP3_Jvc;7qs$nDG3vCqFS}coyb*b-bxS#S*qPu$?rn-)`zgP@i+f| zGyL{0lc|)ZTi8d-yuH-W6H+D!TzyGN7`tg5peJg0I3+Q)NRnmcFH=x%db+SoY!&y7 z3kV=+#3C62HVzK4(~=@N!kae556ar`j|<;KIuD*|1-%@Qs{ic%gjh`WVW-|tPbXzj z>l3bBxstr)_yy$Ph(+i((bJ1W$^+^O|AO*1G?W4u%;^02aQd;ikdujYYfZEfuG>whMhu6Hg`2$yycnK}bjlgr%SxRkm;U31+QS3trx_#ft`{ z9iSz3zedoLdy|#-A!jf!i23XC8m&}7*HQBWEPV0$$Cod~06HOoL{~CevS!q`{FX?S&&y^<|G8xIM`Tie#G8}}MztdMkT*I6 zG3`3>f`;LKNQlv}`ch3}V=wT%rYq?p=!xj68ep~)1w^-F#}1SYRqY)e0G9XoGE5$r z-~Nn0=s5y1T4&7eTXi=50iXrUj&w)W$^h^T9up-$tG-qnzqjhn)qWODOkjEQCi=z= zJ54fAs;UC;xPs~)O%-5vc=Hrk4XYO?V?km-yxzIX_yLTb?NC&)@16~Ou>;W@HCVi1 zr2?aBD$3tU>rkwp zI57+sPtK z;CYK|NZP$O14G4-54}5F6XR>_;qHD4BsOyKq5l36KFSqWfbjUCmFAgO#%kgRDbyNz zdf^|IpgWdRj7?3IU~ZHCl{o-zFc~dQtM78?P#uO@lkb8`6)}ta$LX2zBV)S_Q5uFB zYHE^Qe>fCN$8Ioep+~b702Wl5!16G{?Hfw8zReMG^R&3-K?RY2ebCyv0i)8of*3eo zh^J`;-)|2Z;(O77-duoaBxV)DIi5~J61Z!y_pp2hzFdknh@{AlfB$LH;9j>*)}p8% z4u=+}n9ETA_}t5kj{YhS`%?q;RTUKv;};2Iq?H|cQnXfJ$=!LJBku{h4m zED0Y?)WWDArwt(4Z1k+TxPFJf&509wSl}Zd1KL|#YejC5pOgiF3m!T~?S1a)$v2Vi z^k^mqfaIAqn;IKKkD-C;X72VNt8q1&vGA;IUQ^|0W*_v3IhQlA+E}&;x0Eki+Y!`i z;H*T|XtJ;?^6nk2>S}HEguoePtNn>xR0*l6L8z@sT2D!-3MmZK4M<^$cMc-80F48x z!}&!IfB}H@bElNC5U4Dxsn0G-^;Tgus2n04*L75D+4&0mpxjc$|<5*^#IeR)=VT+c_7m||$ zAXY;U4&)N3I)w4*cLV$P<(id`G;X)VK8Rz+{Ht`{72#@nnZ!nqq;@_Ln&7uA4Z!|^XI3J9!&$EfcN{Sj~`#r+=?uQ7uY@^|9IHIby0-`EdJesJ1joqdlf+F z+FC6^K|#Dt8tA_joH8te7$A{r=mfTgIeL5hwK3s5R{iKXs{d&L$WBOvE`b9RYfoi4 ztV7`iNe!ewX!DGXiXs=z8_yhvdgW*3FHA2e#g!5qypof%0bh**J`X8eaf$HRfDz6W zEL;qFJ7M;T(EEnv{Cq#Y7jAHDRD6oM=gO9}wNSL!+lxy{0m*K_a)O-()mfsQ@f@+n z!Pz+<r_*Xd}-Ul5YwSS#>Z^JnsZ+f5JB@Vx&)!xq5_8k z;R_bDgkwt{G9Bf#Waw!GR*^RpC_^@7&ObDg;pxsJq*#Q+s{6 zbi-XsnSgFW;U~3i+b2wi%+JR_*+lG_F=Hq9-xCwKx1gBK%FO%75O8y=D>n_yr<>J`0yQ~v|>DDhZqey8Ov8M z-@Yw?qZ!~BaG=1u9^%&1(V@v55t_kyM7V^EA7~peAdHc~)w=;qmQWBk$hwXW3#=;& zw~~kHD-$y_X+R{+kl1u_)8Q8#fnn3-EuC z$U)Kyik&n7;Myh@iUbR{c}j{EV1J0MAZP;RUL5P11d`6n%L^!8u!OicX$=5a8Q*oM zp^*`ScsYhQAY{L3JU>4K7#(9ZG?xF0;gIpoWI1erJO;YR=!695y5_;&qu&p(-vxaX zpT^|RCGg9DEseS;3FsxDRuj}Xw*y}z7hD|=`W?HwuLx34BSYw4ocn`{I!)1;z4Nef zpMO9AbO(rkSH^@dvdCBtkMH89UJ*$ zuyOhC(>yys}!H;8}$>R0mbMyF=l$%*GG~_z^_$Q)4rRSn`45< zFMQO$f9}oVzT6P?7I1ML2u}#KBIDx(6#3);cVL0=^m}{Hp{2ooBcCcx6dn*#^w$CF zzM=W_K~ByPh*?AiRZebhIPhb6;dsuMOKqzP@E-6-Df)~(JVewO6#z9@paf%pH-V86 z6hZiTnZH->DjHy6QUFEc*9W)OmSnz1&X4emB<&5$R!{6OGz0wvxv~L|!y9ln%16Mx zfDSLG(o91G*8vh4^%z!RpD)*jFwXJKkmRp}u#AffwHp*$u8xknAfUXF$MFgXz;fp} zLIW_Y#Ag4BlkEjSHqbV6MbJK+Bv7H_$k}^S3jjHGDl#bSdJrKMPoM7S^f11iy5r1N zB_*`9?$gnU7?XWZC)4#sR#fyWJ_3Yp(1zlEQhDq14KgHp#Mm`!60co@JKZ*fM70*- z7ziFgU}7RhJhKu4H{>x^TEv*V5<1o8sV_E*od??+JR!896EZs6BIwQ23VE1uAD zp2-jNSgqGMCo3&|5-6*!?G_|R2(%*XxQ_$`_GYzU$KfY(RNxgTT2U^x4QloET3Ibf zg_pbCs^e-7L^DZXehZ=tG68s9*V1&~02sW2YYG?R_{fMpmJl2aJt!J(U_0F0Cm;<& z12c!%MFD{P$PyBSG|F%zxV!V5vtIlE)b$mi9+`V0<0Q}{7!-J#19)fTYAy)2*rUGSM(lOGhqm1sy}S&Ic|fLB4H)DKe5))puRxZC_70B>-5LwNSPcp-C~?ehbp{~Vntb=+0|)@O zqN5?|g5n0&>*N{LaJ*L7&6@yx2XGk&2H>wl=T4Db?s;rLB%ORp3uSvtO^L0K;Nwn{dmDUj5gNRp8NHEVYZ0-1%)M@a`2J*O3LSV1O5B_pnKW`@bK8Z zRJ28Knh1*k0Q@t`GCcNq2vgDF{~l#_WbrNS$FBj7bO-6CZR;(4iOscZ)o=n}?<_4X zU1`4`QPOri8~nhm?C`WSeE(m_B4PDpEtGH9PEJ!GP5_+(G_(r3fq9H}(sWh6Cu(~K zZWGbXK*ZY7ef!2yd&3tcCaKnJpRDhhU`Tb{r_cgCU+ga8QY1szV4f5KXWT_x z;D3QMn;Zfpd{@Y?f*+veo@fDD`ZTb4SZeUxiGb&d{uLF#ws0G$#}xtAf^kROfh`8_ zv>YC4fxC{etHFHi*BHX! zhpZwL*(|b*d-5gVO4RS-5)v1n27J>e*g$NQm#1g!-kX)6A`=fMUJU&QIA?Omr#ac# zQ4N-F69{6uF8m1h0Gw>>Gh@^&vVZv2N<7-&tnza4KV@z7bpj zL{&j|cYxVATt*#P4M||5{CkvlQc@n{Ss*b4uPL%iF_RMl@aSw1J#Zj+b-B$(YY|5d z(U!ZL+iNqaQ7|{`aIP>m2XyiMGSpqr!t@803RvL3?iVD2i0W#U@l?RBQdA@YbSSX9 zCtB^n`(`TTbwM=&%dFZnE;W@HS>G*RWwKxeCH&xpqcFkv;uw(8tiAMfX~`8(AaF4T zy>?tNXpEttdME8RBXTbHI{O)%<;+af7!)rZyrP=j0ctDP3GY*RT{y)ExSh9?U0DKF z4i^L08MRji6R)tSIQrOV4NJrKAO-0A60s-z1iep1{{&j#XV6#AvAkHjt=Tm_k+F_`hBN8WI+5s7qN`d z$nmF;rphYCO~7Z~T=MvX2nYsI>TZF;r)n<|adCIZzk=gBi|`3Z+nbgaDa&)#EX4j1 z5I*VXR+g7v9| zP1vqumyRNtLjZsUo-aNDJ`)Z;e)fw5vY~#-;C3uTabL^qyvAzhOOaO0mE$!>4-R*5sWlsN#GYt{~1;|8b)&xQgr zvK!FY)zu+PYllHqpil$IP|_8LOKyGl4q5gCDHk5R^NABT@X3LY1m7_IavhN(NL*S- zmq64@GV^0(W}f@@tv+j|73CsiP~rai|Kb&^cW@~%{cEAxd0_G4<}9InjsOGr&T;60 zGBY2P**(L7D8MD7!$$BVns?4_%4=!a^Zhi*CaV8&r(8ouf~GM#F1h=0al(KrVcI~Q zQ>a-&iUf%dtxR3`Q@kxSB=8`94p#;KC773P~p&|BD9LWw8ks508dUJ5kI7-++z%aOGH9)OUYolLr zOIG&v-9Y38^K+OP1$(kN`i)U|RVRwUIOBLGhvxAh?RA}ric)p?p1I2BwG!cV|i2sV&EHX z9u^f%y6h_~yvxMo$-8&5!^6R!)YG)Br@xI&eZC_iei38mZZ^~oOF)B(`-cXIRR}kk zJaW;pr|B+0gwHTtmPp@1ogG0lux+f!4IR2=3d2$x$yVGfygRUs_I9M%hBHW$Z_c?M z0C|2Fs*12(9n zJb(v@$jkFwN(3M`CTas@u&^el?@KzPZ=&D-Nv+aovmHM|k# zOeh@HJiKpTCM-Qh)DUAD0Z^T?ln>~GTVCJ*--8yBU2^rAoJwQOanq^dBL`%*U7}Q1 z$LZimJT`Q4DM=eUbeg!XIXd#bJokCYxa~7YxJ0i2r?&8U8^SnLTj1z%#l-VtPQ&J) zEN^7AirAl+TVPI^gakaP84Lv~q4Li$?&(DcqvA=4Qkpn552YsF8H{?m-#$OMqMVFJZi0%>fO(Nj13t1Yi|O z@$nq!d;#{Ho0HQ_*K8TRcm31*?_A(ThQ=qoO-yhb>~L-&JSeHGOlF;VfKDM4+Hfeb z^7175Z+mBKs${W`5D-k} z=|!SM>-y*kMCOWL{9Su6V%D6r_k^^^#G9*|9}+>pJw3DpAoo&!6*eR?^7P*ik=xbr z(1ZoGU?dSJ!s829R)F6_cTK)V_}eeK{h}L`;Q8&?0dgLLS#Mw8SFEWih%z2QkOV{@ zxhg_zcp}dxm5?82&pw!)xdQg?A94`(#+Y{OVAwNUaI5^cq~s^RU(et^VVrRJuwcay z3m|v|)03k8T>U**_$r=s{0ZaEo!~`b9$zvPzlr$6G{dikTP&GdpCSCN!s}`e-Y;>^ z3W|d;TeuEz#+D~z;Rq8F_O{$k21QQVwzDs9wzDrxDXhMmnNUB-K0MOUK=R{L2pL5{ z7zH1*q#IREE(u|4jk-M@lL=NAS8MEtR50DErXE-& zQ|(9V;6Na_x#5M4&fi!G@nsWq>`^r)mT_7I>wL#!;U7WPMu7#FozX=c#7L!-)whAH z>NuC;?W0HB zM`?I#M^30}7T0k6xtd=$M~>$GwdZ>pd$|6}hdWd#=z8@r(RE^9seLGriJ4U;WSTfX$8o3n!b&$nG^3)t{g zQzs#WN8WrJvrn95X4YcO)u@6&TH4$5DNfFSNQD^(<@ zZtbF?x2T=I51_d||1gx((csaD*aR|Y>kbH{9&s_m#|Rh|%d4v9y+~3}%0l3b0y~hJ zrpqoaC5SCy)lh~(CBr$HLlim($~GT?+Cvos%iSJP;(ohfq|$&`0II5-K zF##m=5y|Qj{&K7wjpL0@vn*gKUAs2Fc&Pau_MH$=DYXZ#J~m++6_vcGm+=Kf8=G`N zOB>K3UXfmYt7x!`a)*MitdTJpA^)-BRet8(!8r#-#TD(3$@)}Ebk9*-enTMSJaoT!wL8B(w1=}}IySMlGB4MPUdH$D ziHa&ES5ZmnS8d4zg0r)^9o5mJ^{vz~oFoL<8-tBL+Xyo=_3t;+^V4g|UcJJrzJATX zN=X3Ep|UdLG1bBXSdfJJmu@F4!ComemZ`I}4qLp=D)EC~aIzKMWnp2e(6L7?QtfUD zaz#NoNZfREcBUM+dIwUH)8d&ofbr#B8}`3p1=Hu!Esy|j@8UN!|I06$k9`g&M`VYE z@Tw`vRk|i41GLApkGc`DBf$mtoXO4pTp&NvpDY#Y+n7s2dhlR<6o=Cofgu0;%oA5> zhHd`8qrRJ&sJvKD#gwf+obvP_Q|sBSn-#IShhQzLJj`u($hga&BJUsA^!ZX$Cl^k3 znEBob4m!>aoq$iOxm`fgoL{hs;Bj!u-5m8Nasm5=GrBUfCrbKeymzGgJWF@T(KR2H zyC?pALXFEbmwMsTr#ku(pDujz*LTKz`t-IOVbJvs6Sf#qg8!0Mb3Z-TkDl(5$Z3KRI9DNS3YQ7HzR}Cu}GN z1KQeR($<|jA0F6m`Qo%J{!%d6)7^7&Xqb|~t6jggJocSdOY3j@wkr;FCDx6}A_T(2 zhsDGwl$2afoLaHG=lXX=SWoLx!KoCdk z{L=x&F*2gFaUC!ps2N41NuC1S-13(e6_j&d4VG~^E-eP_U}{$Tr5qTTrpM>%@mG%R z3@gZ)fSdE(Q-1*4KJ%y>Y79H|Xn6rV{X;W~3 zfP?{w<%mCNcTbNx{Dp?b&cB-o@lm}}S)>`T>95C^4V41@E+*wVNd}UMf_g|(1O1r) z^@}XvhgK!|hCvv^1duSq`T}r3oI7Aqz6Bq|l*61HO9zMcfRR`Rc0NSOqB|GBYXKdb zpWkCtExY3-`*q~z;Scws;FQ&N$WO3(aZ}*`aRGLIK&0QI5&coE7yW*V3eL{L3q~f; zyMh;l9mY>&?XWrcz2AipMST66((r{?=p=C)MR`D!$kv0%PPz-D?^PHgp`VPM-#jN_9LcUJ^Red zFT%?CRiUAbOL=r@+Je{&K1*$+Q$hdRle$|(D5fQ(^&Y@ShMPYtDn_m zX9s&~%OZmv;o3ov&+Dq{ot*X7o(>HO$(@b6C-7qS88rYHd@h{u1-1J*`1_!}@EJ$& zBI7x=&lpYe4#Sf8XrJTq&2}h>oMy@#C&wvNBnt+EGuvPvr6_x0K_@6#04J3-WSfqn z7y!-)o?|CUY%q2BrL$8}b{}WNPhvF{hw8-(rb9e<*E701V@!$S1&?*I4t~Fw>Td9i z>zMFJPp{J5YExyWzWAc+$)4!f=pPL_k=G)iWfxVRCT;^f$$fZt?5 z_&&$Q#L!#wC>&t>Cwalz45+c{w=^V3(5+RBUYs%0XL#)R3@=}%@EM~SZsC-!?mSQv zz+R|yp9(qhkBZ2R;$~^_&JRS|fE(!uigf=mCh!xF0Sn~_U~_mK1T5@PD&!A4Yydo( z?x?bxb>`Zrt67h}J)^;Oax0l_jX-ubmrtDhnYq#-mY{$XG4}W;^_Pa5@(-Vi-^hw^ z5Eu3<5su*LsIXPp*P&I+VbW2?y>g3(<$~espw1jgHI8>}KVw>H?3&lH38-+0&tHFbjai6qqu=8EcpCtN<^7s3{lKgKZkL& zu_`HuJXY7&Q4qfEpcYZO$0(leC!!5#;Q_b@82SrnCVoRf5$6p{EAb)A&9M8;Hro_z z#O%Zb1lsPmU^tv>MJ3~&wwRNolama4-#<8EWHkTvWQh&q*$ZcN2=a1w}pQH%bDGXttlD=vD1tWuZSG?RmH$cfZam#ju%rbLAOamTAvr?Y>#Pc#^eUO8@~KoV$9(&EGv zT_0;K`&UY`tyWDQOsD0b*ugIi!1_Y{1}f0e1OZNIrxZ`#Z!<0RX;J#GDw zRsMo6ocMx@1V8WWKAU!jCb~|o`O~_)-uRCQ;^i>RK8sm^#^uFM`&6T2Q#JMVN1&so zu0Gz(SY%?+#wsi8`AaK6`F4Lv7#T%=(U%TY+dGXkoNbB6Jlq}Gm>YzI0vlt=_GYk0 zH(oX;`8qC2uILlXvZpn8C6+#IOnRx~#bwf(ah|y@>zi!m6tx+_-#>Ed$C4C%WYSos z70(OQXQkkAYBCBjYuGKD!F-FPIn0+bguS0Wt)4tOSg=BA$H4}s7j!)~(5$T#xi+%nVLL*v9^21u;n+D#YyA{Ljw5B6Xlu;J`QKzu|VR6jzkf~U2ELfJib zWk(Vot4sS3h5w!U`k2$2ZXB&G=`hKA7-_-rQ)!R%vF9=u7!`BR(&AP^N>9E)qpwCI z57ZrE!DXfYG77!d&JtM_2oXXf-O(O_U+Os=tIz1^9avsPlY=8_X>cj%WGB>aEVjaA zzzMbEwBax9hAHSf$a6#`BIV0nJ znYRzMB&8Txw(A;eUfM5M8y-V1C%3xx+{eRI#pg45Z7mHO>DcU0sTei&;ETEmpMs@2 z()S88KMaD!)Xv9>J9y(*sdo*yEkb5jBW1>e@p5_%62bk-xOcB$CJSzmLv6xESc=bjs}=5rCVi? zxbP9I3Dr@`4oh*4-#E^FtBU^#O%`LsPFQHq!kAuob(Cqcva_w*?(H)W5)(_fA}LlU z4x4=9#3t?z@Q#EQOQJ|ACY)GIReXWXhuZzY8khiN|EL57zi8O!c(h|Gr^Ur3_o$A=}PQclfD)=(Vak;#CNwHj-p@0P5 zgg<-Ig3751L$@@Ge^SRT(m#KZ+12UCT=SLe+N{>P&|}A)#gSKyC4UKse`fXEDm%63dtoeD6g(_x$_EwFXAIn(3X!l{Gmm;pT z$KxOV9C)&~j6GUb$%?aV*Xrip7-#)mam=B)96v5kOQ(tayptO4w)@D_zLA<$=DIc& zhg)r>MRBQLr50&eii|v7_KXUT2QvPpW+cxPJFz9%+k5aJ1DB4cB+r|zq{5y{M%VQV z?DhTn*!@NX>y#@4Ejn)OuArM5BelC>UD)AH2R9E)0t~GM zfOZsIM1dsY*f`XbwLopn{j5F;4ljGP6fmR~+3&|o;s&reJ=kusv`V&L2G zvuSc)rFrf9p|Q`%T~xi>G8^?)t5xcBil+^e+DUuUSSwSb88==sb5C(qoK%_D-Z=SX zvVeQ}=n=|+xB1y7=QRx&1BPjQ{7Dl@Ha505N{F}~E1Pn&f9QAWty56B@H~fhJ9+b9 zK*)g@hrC}d`d`$`Pz`zkBK3cGm#9}M>bl_U5J0QBj@ey{i_&N|NBtf739TFa63$Ah z3155*=19cGSy)~J;>v-#zjs-!Rp@6d?XmnRwD=(Ko$O%DeoL;#38`P&BULY*6Sp+KNvA#MH#x8Kjb!@` z%?P>XNt@6o2W>ch#Kdi}$q%Z!`XrB^%&dOe*b?INpM8r@*fsHHVY$zwLigmPd?vT( z-S??d9?^xyEzRaj-(Ko%uE#%UZwQ|2SNP>m%g^6p`{}9ec6P$MjOu}bP<>sh1j1xc zcC87!C--+vyB$d!)}`;I8g~&~=SB2qIg-}A9pulOzV6Cn^&{Wv znbs6Y2->mK()l52YrJkZO*U&zFV(brw5xOZTLrgYP`#1v%yWO6{Pxf>$K~d;v0}0Q zNy4J@Z6Y6r8x25gLkZsNXU%WFwMWzFJ@8(_bR2yZl{X+PBjIx#t{{<=k&zf!5C9h) zYOeBt0&-uMVE2*x2Y&ph`-pxKSpm4`d!(*gqW+7&iv3SjMOD)U2r(FZhoy>6W)@~0 z-<`k}&?R(Br)){K=m5!{?U~vXeA0m~3;`47-L{0|QartTHblbqe`L-&{J83)Go`%! zp`Js0G6YH5LKy?k$R!;$n$N4$`bmUgwagKDLe-~x;ui$xa+5BiwM>_-K+!dh|aBZ>tz(@j1UN>v=Z9R z-vkeCkNb1O^rioSZDE0g_C^P`$oY)$GZaDGrO%tyK7al;oZJvROI^2__|0ZJ-Kj^D z?#{MYrlB~=_x2SL%w_@a}U~;pOJOGRXmpb zp>6?J^DK=+F`4T}<gH zT*fG9qQnn7=jZL4A4CUE*$a@NYPb+r@Nh(W3F~+v0p2hRGuf>q487b(8@cKTRQP< zO8vsjU#?v(&Iqg?aG9p@%868HeKtEX=~`k(K-xl#WgKSErvlkWbZnqFuNfS3!`Igj zmJ%pTOVES zY5Z?)rA$%r-C4?aD6Xo~;30-G6h|ELE`530!pK zq!5k8v?F&A+P7{F(`ZU`Kg!=|9yROR?aALqpShGZ?`dk5J;A;BJ8MRvfpN}<%!aDi zRnCA?;CYt^hW{uL>Y$a;sxx(7LjX0*AETt(6RAbY~fwzomqdbH`Jfx~rl|?!NGX>urT5zn|1o+@Ykj4VP-7i9&^R(e z>=Srez4$JGM5VI{^wCD&Y@&AI`}c(a+`Y=s7KPBx9kS8z_e+b5opxm)C=naZU@^_j zGepI}Xc>pp8VxlyUUC*`m}qug1SE5+*MC%*-B>oj(4NK}X9*;&YJO}X+a8wF+^U>n znz5_&ioP+Cf8^3PX+mA)o*NIIc)FI+7*UOh-tG1;mSJ6&^ZYP2SwQ~Nt~>m%;!3pg z03*MS>xBSLM?{8I!H)`a{$2{8W2&r9Y+}s!&RSB;d;ikg9>i*t&q2Pjd)}5jIh~h| znBroYT|(w0vW*l>{ev6E==r=nYhT}L3wBHhUIMdMlwAvDbkqc+gs=+!D$TDr0qojW14;w_2V1e-{=UDCJBp@)`~2rhMgi zrR$-fO3VXWt3!UjC{>=AjehS?CH7cK^YASZ%Yd{qqU_4%mrJy@oA3({QR>>mJ#81+la=< zt=RyprVf@MAGhFM17yK)Gfer)mG3e?X%MCW>P%>z0=R+L4SyeX14Es`mfo(e5>TRm zH6!!G#=dY|^D8bZ!+H7x4!EoI2vJS6O^YiMXc#(AKnBVKdnPAGQd)X#eH~p=UKp?Z z{2`gqwsDJt#JmdcZQ-Ep>A8v%)@|noh#15}=+F^~<+c`qhI#r&UJOrja?*>XY4b*P zRrvl{L_;F%ye39FqLm2RgQ_s&1J>y^1!r1==7kut>Btp4Gk&z@2X{hN#;7#>e94b< zCcg(JV&W5CZ*BT6qaew^aoez!Hyxs_6kHi+7vAv~?)|J}ySe6|BO7-lz6E$Su~QUE zZc!?bGJ1}vte38^@N7X3AOX-*@KeegSQYSatWJRnK$T02JNX||*{6OZ0&o={9v2MM zfLVTq55sv33MgqTpZX1?zZiAs85<-8gBs0QNU74ai2)y2IA{$5EL2rC;mvAr?p#67 z3=%3<6;`lhAokD+=F9)lYpZV6%3js>|u>TWpLPeOG#pyfy z3GZT0VWlwA`;w^LTUzx9>I?;qH=a2nEMfhYZfnC@C2}lEVt(Fy-96{ew`b4V(myQE zl5gL+JFG6Q>gr6E)rf_)hh< zD;q?P0hAvVWc?#x(!(5MQ;4d2EDFWZZ_H@5!4l>ZBfL1x##FmXEhRkhdY z^!DyYvVRUHCmR{}S>3X=ZF)UY>ntL&@?+}bzUu1L4HpyBfvTjf&GXNKet*i&3i??) z5wiQ<@el#i)j1KlHOk68Awi8{>e|qQeyDZ%6}gI-QLFoVcKydH9$>e<-Ys>5-P#%x z=YN>^ZN%kjo_`U2lRUs}*61a(|KBya3uGZdtx11A_y`SbazD;f|JdmuV z#bVxEz|^k1XyfMv6_xMRUYChT`H>?Kslx0kWSeLky^K3LcrPMCy)j!c)5cRYc9u2L`&}p)i30L?0`B@CLgzyU%3OVP`A5PGi&euQk9qLjeBMBP0ctzCLT=kD66#jmD zh?1T$u7fJ*W6J*h7>PD4C2s$!C8Ag;APscFds0k3J`a4N?S~|N5*311r*>cTB+=1% z;Lq8pqIhh6A=B(+k;(Y44_=>lY-~ECb%&#V`2N;rM3mK(#^c8o?uA>AJF=gZUm2Es zs5LR!qQX|_XYk(J*4EQ&e2#{Jxyd?xWMqP#ot!&qVocsySX+CKRM7a4o1PvO)vD65 zl*unV>CmyqWF%MVw7cW#qxUq`EdcYQmUUN9<8Zd?wR>8<;yhDn=q+7)(qrfLM$JRw z+Ky1abHzL~EzeH#T)ijC(P;zD`ZQneU?+u=v;ztc*m*3lfc8SWHYYpq(Iea08b1Y_ z#(#m>RqqYwCxz;2+;R`*olc()P%#cyA8OVdVqkq&=24zlx#U*R{ZRDW#mxLF@qK|G zPV&k@o9yA42V>L>F)-JGNzYQS*68 zL`1{;sEakRt4VRqNn!Mil$$%F!=SPf zRi7gZza}~lEiMw=e_oj%`zuzg$_XkM*gspvdYR}w1Ni#j-aSdoHGly{&;Uk6nY(Tf zw`})+f$8nY$zdsJk0KokGSct}iz$ujn#0GYM*qHfM`ez=Q~v&^wc=Vn+WgpjIPpf< zmG6UWV{f7FhOmrBuT~o=OjHZ+pC!GEHxkCiRL&`%Dy~}C`d(h{`-@gbt|kA>TtB*y zeskWLoPGMIua1%qCk0bs`+5gOAua~L6_V*gb=>^pPT z2U0}$on|0curY3uBsw$VAKNpr;e~_nzmMhrAmFewYRxx{cFaDYW!0_gv2Rp!;4n~7 z7zv`mfAcE%;2T_-c_Ap=YQH#R(_F0H;5b=+=JneWSImIi3Kb^4()$Rxe-NabSjm;~ zAF!wsZ=9FyO#e)_$Zoohd~Ho)JAne&OIM5U7Y;pR zYyqwl-gg^<&m5T^OmM6#EC5Z%hEL-V6OXQemF!^XnL~#Jn!cmCzAo85)R8!A zJt!)0ak7o=O^o%7{&kmBLm`m^ha+seJb%`xOvT0BmsHTEW2YS|Ur4IsP6A=z#jo#` zE~g)~Q42cFvDRlP&kZPD+W)jp+ivFj0c4sNYnoduR<5ngk*v0w-4l&yPdih<(PL5) z8h3yrAz$BjVVhuG+t#L~731C;>q9%RRmT{JyI*Mgmd<;{a>=H;#%sfQWiGR&NU*GI zZlpDR>r|e$?+TlQoV50|h+A2g->QT#@$Qe0i>)s*y_?wk{khK!^PfMb({815 z2-eX+sK@C)WcStl`0CmxIqd2{X~et}6}8uIQvz&1X)=w=FBLIU(`TjEyuFNA_sZf6P&gW%>1bwR5*~Gk zH_U&Fsq#MT`PQbo&et&ocL-iii0hm&Zgu$(m;Ql*OUOgO%gb#&G5Ma;ru4ZY(Z}~w zHIwM zwq8E*n8amRND@y6q}PA+4ZyT!<|EJm-oGi zpS3HJ_YnSx8<8IUz_Lz6>AI%yrVg5^2@UE9)&yqo!YU=uX1Ri?VM`8)k}BE zlkeJa|3Jg|U$l^yx^v^Y8C=>!JltP~9L;_$4NG2W>lr$?%dZZtSl@cT7mkhb8p|*! zFboR)=Y_TJOJao0+fSeJf`Utq9&LJ>eKPiHAG3_dT0#=V=?5FhQBhh~!w%rH^ZcuD zjAGU<{hrll`QJ>mwAjMUH0pey{z4)6Q~?AE9-d2*W!#j%1BuU!Xy|U1YqjCv{ri1T mocNCajGle!|NjrZ|0Vn6Vrf3llHm)8}D67{vl;B!D(LLq|@zwyZ7z4>wf~nLWsd!^3d8OFlRIfKr^@A2$_;5 z&9)MnB#~g$3MHKrqa-BMhUP{&o)$a+r>A%%jJ;+7*x@M$!?Z}UulxIlp)f>)QK@D? ziY*Teu_2IO_qMVHwIrlHARd4uN_vWF4jfncN3U}in3EKVxfgh^3VagoJmi<3LtZ(E57V3sgYj(*PZF&X&{lvM zQ90svkbrNj7BUbq?eBmLY`FXiUK(c6w^7Gr;AK$d6?cZT6 zWY>q=_bc?6=s-(U)O59@Th@ENGQWAL+UpUk-&TH4(U$VLJU}9(j-yZB7MC9 zCIq=Z!9E9e?x4WOF(u|>#f!U{0nd^vWXAry1PNYRRy%~@%}iYZDJrF8a2o8RInIk5 z6jb_zfQSX8)iYLK=J1MqDDMQA4JY4>Dwu!lmY^bNLO%o6FU*zbIQ;5#}}j95jw+lL*p z-)WOp({<|spz2~hRnQh3Wt*?t?6=?lnB!z~n<+CeSZL%;sUXLsAY0HAD8Of-U?D+T zLO)TaL{sDw8ZHFvFDgAQCQ!nO7L^6#Ur+D~#u=)zc9hfb?ymYPG~r?OH`nkSc9$yQ z6FxlvERSVe)x-xJf>=ssKTPXxB76ZVp@`FXDTfqf4F;QRB{?(eCD@fOGDOh_zmNfz zVp}W;_U!m9OW!U%su)0YC~Aw|8oEt-UvS7@_|QE)BiHA%(|S^lc^2vNt4g4A_%Q7d zRcmAMjioYQ5N3F`=poiyymYd=SBpBrL)?yL%qZt>Du6HFK0V5{uXXW#?P(L&?(uo! zsEKlj&#U;{JU(~xPtLR5jp^=6CFfouKIiJ|YHiiKv7>Qi2#!6mW%a$8t5B-pK$r0&HJw3xYhtYq-)ju9~x--EkA>qW|Qz+I;s(4ZSGG+#7Yu18m4eRXZE2`$#} zJqGI@XDyF9JKgs7n}cl(EFZ6X+3dANEhW1+sYGO_2(~obU!Rr-`32@Y9O&^ +#elif defined(__linux__) +#include +#else +#error "Unsupported OS" +#endif +#include +#include +#include +#include + +#if defined(OC_IDD_API) +#include "oc_introspection.h" +#endif + +#if defined(_WIN32) +static HANDLE event_thread; +static CRITICAL_SECTION app_sync_lock; +static CONDITION_VARIABLE cv; +static CRITICAL_SECTION cs; + +/* OS specific definition for lock/unlock */ +#define app_mutex_lock(m) EnterCriticalSection(&(m)) +#define app_mutex_unlock(m) LeaveCriticalSection(&(m)) + +#elif defined(__linux__) +static pthread_t event_thread; +static pthread_mutex_t app_sync_lock; +static pthread_mutex_t mutex; +static pthread_cond_t cv; + +/* OS specific definition for lock/unlock */ +#define app_mutex_lock(m) pthread_mutex_lock(&(m)) +#define app_mutex_unlock(m) pthread_mutex_unlock(&(m)) + +static struct timespec ts; +#endif + +int quit = 0; + +/* + * There are two ways that GET/POST/PUT calls can get the information about a + * virtual device. The information can be passed to the GET/PUT/POST callback + * via the user_data context pointer, or the device index can be used to obtain + * the virtual device information and that information can then be used to look + * up the virtual device. + * + * Both methods are shown in this sample if USE_VIRTUAL_DEVICE_LOOKUP is `1` + * then the device index will be used to obtain the virtual device info. If it + * is `0` then the information will be sent via the user_data context pointer. + */ +#define USE_VIRTUAL_DEVICE_LOOKUP 1 + +#define UUID_LEN 37 + +static bool discover_vitual_devices = true; +static bool display_ascii_ui = false; + +typedef struct virtual_light_t +{ + const char device_name[32]; + const char uuid[UUID_LEN]; + const char eco_system[32]; + bool on; + bool discovered; + bool added_to_bridge; +} virtual_light_t; + +#define VOD_COUNT 5 +struct virtual_light_t virtual_lights[VOD_COUNT] = { + { "Light 1", "1b32e152-3756-4fb6-b3f2-d8db7aafe39f", "ABC", true, false, + false }, + { "Light 2", "f959f6fd-8d08-4766-849b-74c3eec5e041", "ABC", false, false, + false }, + { "Light 3", "686ef93d-36e0-47fc-8316-fbd7045e850a", "ABC", true, false, + false }, + { "Light 4", "02feb15a-bf94-4f33-9794-adfb25c7bc60", "XYZ", false, false, + false }, + { "Light 5", "e2f0109f-ef7d-496a-9676-d3d87b38e52f", "XYZ", true, false, + false } +}; + +#if defined(_WIN32) +HANDLE hConsole; +CONSOLE_SCREEN_BUFFER_INFO consoleInfo; +WORD saved_attributes; + +#define C_RESET \ + do { \ + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); \ + SetConsoleTextAttribute(hConsole, saved_attributes); \ + } while (false) +#define C_YELLOW \ + do { \ + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); \ + GetConsoleScreenBufferInfo(hConsole, &consoleInfo); \ + saved_attributes = consoleInfo.wAttributes; \ + SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN | FOREGROUND_RED | \ + FOREGROUND_INTENSITY); \ + } while (false) + +#elif defined(__linux__) +#define C_RESET OC_PRINTF("\x1B[0m") +#define C_YELLOW OC_PRINTF("\x1B[1;33m") +#endif + +static void +print_ascii_lights_ui(void) +{ + OC_PRINTF("\n"); + + for (size_t i = 0; i < VOD_COUNT; i++) { + if (virtual_lights[i].discovered) { + if (virtual_lights[i].on) { + C_YELLOW; + } + OC_PRINTF(" %s ", " _ "); + if (virtual_lights[i].on) { + C_RESET; + } + } else { + OC_PRINTF(" "); + } + } + OC_PRINTF("\n"); + for (size_t i = 0; i < VOD_COUNT; i++) { + if (virtual_lights[i].discovered) { + if (virtual_lights[i].on) { + C_YELLOW; + } + OC_PRINTF(" %s ", (virtual_lights[i].on) ? "(*)" : "(~)"); + if (virtual_lights[i].on) { + C_RESET; + } + } else { + OC_PRINTF(" "); + } + } + OC_PRINTF("\n"); + for (size_t i = 0; i < VOD_COUNT; i++) { + if (virtual_lights[i].discovered) { + if (virtual_lights[i].on) { + C_YELLOW; + } + OC_PRINTF(" %s ", " # "); + if (virtual_lights[i].on) { + C_RESET; + } + } else { + OC_PRINTF(" "); + } + } + OC_PRINTF("\n"); + for (size_t i = 0; i < VOD_COUNT; i++) { + if (virtual_lights[i].discovered) { + OC_PRINTF(" %s ", (virtual_lights[i].on) ? "ON " : "OFF"); + } else { + OC_PRINTF(" N/A "); + } + } + OC_PRINTF("\n"); +} + +static void +set_idd_from_file(const char *file_name, size_t device) +{ + (void)file_name; + (void)device; +#if defined(OC_IDD_API) + FILE *fp; + uint8_t *buffer; + size_t buffer_size; + const char introspection_error1[] = "\tERROR Could not read "; + const char introspection_error2[] = + "\tIntrospection data not set for device.\n"; + fp = fopen(file_name, "rb"); + if (fp) { + fseek(fp, 0, SEEK_END); + buffer_size = ftell(fp); + rewind(fp); + + buffer = (uint8_t *)malloc(buffer_size * sizeof(uint8_t)); + size_t fread_ret = fread(buffer, buffer_size, 1, fp); + fclose(fp); + + if (fread_ret == 1) { + oc_set_introspection_data(device, buffer, buffer_size); + OC_PRINTF("\tIntrospection data set for device.\n"); + } else { + OC_PRINTF("%s %s\n %s", introspection_error1, file_name, + introspection_error2); + } + free(buffer); + } else { + OC_PRINTF("%s %s\n %s", introspection_error1, file_name, + introspection_error2); + } +#endif +} + +static int +app_init(void) +{ + int ret = oc_init_platform("Desktop PC", NULL, NULL); + ret |= oc_bridge_add_bridge_device("Dummy Bridge", "ocf.2.0.0", + "ocf.res.1.0.0, ocf.sh.1.0.0", NULL, NULL); + return ret; +} + +static void +register_resources(void) +{ + set_idd_from_file("dummy_bridge_bridge_device_IDD.cbor", 0); +} + +static void +signal_event_loop(void) +{ +#if defined(_WIN32) + WakeConditionVariable(&cv); +#elif defined(__linux__) + app_mutex_lock(mutex); + pthread_cond_signal(&cv); + app_mutex_unlock(mutex); +#endif +} + +static void +handle_signal(int signal) +{ + (void)signal; + signal_event_loop(); + quit = 1; +} + +static virtual_light_t * +lookup_virtual_light(size_t device_index) +{ + oc_virtual_device_t *virtual_device_info = + oc_bridge_get_vod_mapping_info(device_index); + for (size_t i = 0; i < VOD_COUNT; ++i) { + if (strncmp(virtual_lights[i].eco_system, + oc_string(virtual_device_info->econame), 32) == 0) { + if (memcmp(virtual_lights[i].uuid, virtual_device_info->v_id, + virtual_device_info->v_id_size) == 0) { + return &virtual_lights[i]; + } + } + } + return NULL; +} + +static void +get_binary_switch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + (void)user_data; + virtual_light_t *light = NULL; +#if USE_VIRTUAL_DEVICE_LOOKUP + light = lookup_virtual_light(request->resource->device); +#else + light = (virtual_light_t *)user_data; +#endif + + oc_status_t resp = OC_STATUS_OK; + oc_rep_begin_root_object(); + if (light) { + switch (iface_mask) { + case OC_IF_BASELINE: + oc_process_baseline_interface(request->resource); + /* fall through */ + case OC_IF_A: + case OC_IF_RW: + oc_rep_set_boolean(root, value, light->on); + break; + default: + resp = OC_STATUS_BAD_REQUEST; + break; + } + } else { + resp = OC_STATUS_BAD_REQUEST; + } + oc_rep_end_root_object(); + oc_send_response(request, resp); +} + +static void +post_binary_switch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + (void)iface_mask; + (void)user_data; + virtual_light_t *light = NULL; +#if USE_VIRTUAL_DEVICE_LOOKUP + light = lookup_virtual_light(request->resource->device); +#else + light = (virtual_light_t *)user_data; +#endif + OC_PRINTF("POST_BinarySwitch\n"); + if (light) { + oc_rep_t *rep = request->request_payload; + if (rep != NULL) { + switch (rep->type) { + case OC_REP_BOOL: + oc_rep_get_bool(rep, "value", &light->on); + break; + default: + oc_send_response(request, OC_STATUS_BAD_REQUEST); + break; + } + } + if (display_ascii_ui) { + print_ascii_lights_ui(); + } + oc_send_response(request, OC_STATUS_CHANGED); + } else { + oc_send_response(request, OC_STATUS_BAD_REQUEST); + } +} + +static void +put_binary_switch(oc_request_t *request, oc_interface_mask_t iface_mask, + void *user_data) +{ + post_binary_switch(request, iface_mask, user_data); +} + +static void +register_binaryswitch_resource(const char *name, const char *uri, + size_t device_index, void *user_data) +{ + oc_resource_t *r = oc_new_resource(name, uri, 1, device_index); + oc_resource_bind_resource_type(r, "oic.r.switch.binary"); + oc_resource_bind_resource_interface(r, OC_IF_A); + oc_resource_set_default_interface(r, OC_IF_A); + oc_resource_set_discoverable(r, true); + oc_resource_set_request_handler(r, OC_GET, get_binary_switch, user_data); + oc_resource_set_request_handler(r, OC_POST, post_binary_switch, user_data); + oc_resource_set_request_handler(r, OC_PUT, put_binary_switch, user_data); + oc_add_resource(r); +} + +/* + * TODO place this in a thread loop + * When a device is discovered it will be added to + * the bridge as a virtual_device + */ +static void +poll_for_discovered_devices(void) +{ + size_t virtual_device_index; + for (size_t i = 0; i < VOD_COUNT; i++) { + if (virtual_lights[i].discovered && !virtual_lights[i].added_to_bridge) { + OC_PRINTF("Adding %s to bridge\n", virtual_lights[i].device_name); + app_mutex_lock(app_sync_lock); + + virtual_device_index = oc_bridge_add_virtual_device( + (uint8_t *)virtual_lights[i].uuid, OC_UUID_LEN, + virtual_lights[i].eco_system, "/oic/d", "oic.d.light", + virtual_lights[i].device_name, "ocf.2.0.0", + "ocf.res.1.0.0, ocf.sh.1.0.0", NULL, NULL); + if (virtual_device_index != 0) { +#if USE_VIRTUAL_DEVICE_LOOKUP + register_binaryswitch_resource(virtual_lights[i].device_name, + "/bridge/light/switch", + virtual_device_index, NULL); +#else + register_binaryswitch_resource( + virtual_lights[i].device_name, "/bridge/light/switch", + virtual_device_index, &virtual_lights[i]); +#endif + // the immutable_device_identifier ("piid") + oc_uuid_t piid; + oc_str_to_uuid(virtual_lights[i].uuid, &piid); + oc_set_immutable_device_identifier(virtual_device_index, &piid); + // Set Introspection Device Data + set_idd_from_file("dummy_bridge_virtual_light_IDD.cbor", + virtual_device_index); + } + + app_mutex_unlock(app_sync_lock); + virtual_lights[i].added_to_bridge = true; + } + } +} + +#if defined(_WIN32) +DWORD WINAPI +ocf_event_thread(LPVOID lpParam) +{ + oc_clock_time_t next_event; + while (quit != 1) { + app_mutex_lock(app_sync_lock); + next_event = oc_main_poll(); + app_mutex_unlock(app_sync_lock); + + if (next_event == 0) { + SleepConditionVariableCS(&cv, &cs, INFINITE); + } else { + oc_clock_time_t now = oc_clock_time(); + if (now < next_event) { + SleepConditionVariableCS( + &cv, &cs, (DWORD)((next_event - now) * 1000 / OC_CLOCK_SECOND)); + } + } + } + + oc_main_shutdown(); + return TRUE; +} +#elif defined(__linux__) +static void * +ocf_event_thread(void *data) +{ + (void)data; + oc_clock_time_t next_event; + while (quit != 1) { + app_mutex_lock(app_sync_lock); + next_event = oc_main_poll_v1(); + app_mutex_unlock(app_sync_lock); + + app_mutex_lock(mutex); + if (next_event == 0) { + pthread_cond_wait(&cv, &mutex); + } else { + ts.tv_sec = (__time_t)(next_event / OC_CLOCK_SECOND); + ts.tv_nsec = (__syscall_slong_t)((double)(next_event % OC_CLOCK_SECOND) * + 1.e09 / OC_CLOCK_SECOND); + pthread_cond_timedwait(&cv, &mutex, &ts); + } + app_mutex_unlock(mutex); + } + oc_main_shutdown(); + return NULL; +} +#endif + +static void +display_menu(void) +{ + OC_PRINTF("\n"); + if (display_ascii_ui) { + print_ascii_lights_ui(); + } + OC_PRINTF("################################################\n"); + OC_PRINTF("Dummy Bridge\n"); + OC_PRINTF("################################################\n"); + OC_PRINTF("[0] Display this menu\n"); + OC_PRINTF("-----------------------------------------------\n"); + OC_PRINTF("[1] Simulate discovery of 'Light 1'\n"); + OC_PRINTF("[2] Simulate discovery of 'Light 2'\n"); + OC_PRINTF("[3] Simulate discovery of 'Light 3'\n"); + OC_PRINTF("[4] Simulate discovery of 'Light 4'\n"); + OC_PRINTF("[5] Simulate discovery of 'Light 5'\n"); + OC_PRINTF(" Select simulate discovery of any device again\n"); + OC_PRINTF(" to simulate that device being disconnected.\n"); + OC_PRINTF("-----------------------------------------------\n"); + OC_PRINTF("[6] Display summary of dummy bridge.\n"); + OC_PRINTF("[7] Start/Stop virtual device discovery.\n"); + OC_PRINTF("[8] Enable/Disable ASCII light bulb UI.\n"); + OC_PRINTF(" A representation of the bridged lights\n"); + OC_PRINTF(" using ASCII art.\n"); +#ifdef OC_SECURITY + OC_PRINTF("[9] Reset Device\n"); + OC_PRINTF("[10] Delete Device\n"); +#endif /* OC_SECURITY */ + OC_PRINTF("-----------------------------------------------\n"); + OC_PRINTF("[99] Exit\n"); + OC_PRINTF("################################################\n"); + OC_PRINTF("Select option: \n"); +} + +static void +disconnect_light(unsigned int index) +{ + virtual_lights[index].discovered = false; + virtual_lights[index].added_to_bridge = false; + size_t device = oc_bridge_get_virtual_device_index( + (uint8_t *)virtual_lights[index].uuid, OC_UUID_LEN, + virtual_lights[index].eco_system); + if (device != 0) { + if (oc_bridge_remove_virtual_device(device) == 0) { + OC_PRINTF("%s removed from the bridge\n", + virtual_lights[index].device_name); + } else { + OC_PRINTF("FAILED to remove %s from the bridge\n", + virtual_lights[index].device_name); + } + } else { + OC_PRINTF("FAILED to find virtual light to remove."); + } +} + +static void +discover_light(unsigned int index) +{ + virtual_lights[index].discovered = !virtual_lights[index].discovered; + // virtual_lights[index].discovered = true; + // TODO Move the poll code into its own thread. + + if (virtual_lights[index].discovered && discover_vitual_devices) { + poll_for_discovered_devices(); + } else { + if (!virtual_lights[index].discovered) { + disconnect_light(index); + } + } +} + +static void +display_summary(void) +{ + for (size_t i = 0; i < VOD_COUNT; i++) { + char di_str[OC_UUID_LEN] = "\0"; + if (virtual_lights[i].added_to_bridge) { + size_t device = oc_bridge_get_virtual_device_index( + (uint8_t *)virtual_lights[i].uuid, OC_UUID_LEN, + virtual_lights[i].eco_system); + if (device != 0) { + oc_uuid_t *id = oc_core_get_device_id(device); + oc_uuid_to_str(id, di_str, OC_UUID_LEN); + } else { + strcpy(di_str, "ERROR FETCHING"); + } + } + + OC_PRINTF("%s:\n", virtual_lights[i].device_name); + OC_PRINTF("\tVirtual Device ID :%s\n", virtual_lights[i].uuid); + OC_PRINTF("\teconame: %s\n", virtual_lights[i].eco_system); + OC_PRINTF("\tlight switch is: %s\n", (virtual_lights[i].on ? "ON" : "OFF")); + OC_PRINTF("\tAdded to bridge: %s\n", + (virtual_lights[i].discovered ? "discovered" : "not discovered")); + OC_PRINTF("\tOCF Device ID: %s\n", + (virtual_lights[i].added_to_bridge ? di_str : "N/A")); + } + OC_PRINTF((discover_vitual_devices) ? "ACTIVELY DISCOVERING DEVICES\n" + : "NOT DISCOVERING DEVICES\n"); +} +#define SCANF(...) \ + do { \ + if (scanf(__VA_ARGS__) <= 0) { \ + OC_PRINTF("ERROR Invalid input\n"); \ + while ((c = getchar()) != EOF && c != '\n') \ + ; \ + fflush(stdin); \ + } \ + } while (0) + +#ifdef OC_SECURITY +static void +reset_light(unsigned int index) +{ + (void)index; + size_t device_index = oc_bridge_get_virtual_device_index( + (uint8_t *)virtual_lights[index].uuid, OC_UUID_LEN, + virtual_lights[index].eco_system); + if (device_index != 0) { + oc_reset_device(device_index); + OC_PRINTF("device %zu is being reset!!\n", device_index); + virtual_lights[index].discovered = false; + virtual_lights[index].added_to_bridge = false; + } +} + +static void +reset_device(void) +{ + OC_PRINTF("################################################\n"); + OC_PRINTF("[0] Reset Bridge\n"); + OC_PRINTF(" Reseting the Bridge will reset all Virtual\n"); + OC_PRINTF(" Devices exposed by the Bridge.\n"); + OC_PRINTF("-----------------------------------------------\n"); + OC_PRINTF("[1] Reset 'Light 1'\n"); + OC_PRINTF("[2] Reset 'Light 2'\n"); + OC_PRINTF("[3] Reset 'Light 3'\n"); + OC_PRINTF("[4] Reset 'Light 4'\n"); + OC_PRINTF("[5] Reset 'Light 5'\n"); + OC_PRINTF("################################################\n"); + OC_PRINTF("Select option: \n"); + int c = 1000; + SCANF("%d", &c); + switch (c) { + case 0: + oc_reset_device(0u); + break; + case 1: + reset_light(0u); + break; + case 2: + reset_light(1u); + break; + case 3: + reset_light(2u); + break; + case 4: + reset_light(3u); + break; + case 5: + reset_light(4u); + break; + default: + break; + } +} +#endif /* OC_SECURITY */ + +static void +delete_light(unsigned int index) +{ + size_t device_index = oc_bridge_get_virtual_device_index( + (uint8_t *)virtual_lights[index].uuid, OC_UUID_LEN, + virtual_lights[index].eco_system); + if (device_index != 0) { + oc_bridge_delete_virtual_device(device_index); + virtual_lights[index].discovered = false; + virtual_lights[index].added_to_bridge = false; + } +} + +static void +delete_device(void) +{ + OC_PRINTF("################################################\n"); + OC_PRINTF("[1] Delete 'Light 1'\n"); + OC_PRINTF("[2] Delete 'Light 2'\n"); + OC_PRINTF("[3] Delete 'Light 3'\n"); + OC_PRINTF("[4] Delete 'Light 4'\n"); + OC_PRINTF("[5] Delete 'Light 5'\n"); + OC_PRINTF("################################################\n"); + OC_PRINTF("Select option: \n"); + int c = 1000; + SCANF("%d", &c); + switch (c) { + case 1: + delete_light(0u); + break; + case 2: + delete_light(1u); + break; + case 3: + delete_light(2u); + break; + case 4: + delete_light(3u); + break; + case 5: + delete_light(4u); + break; + default: + break; + } +} + +static bool +directoryFound(const char *path) +{ + struct stat info; + if (stat(path, &info) != 0) { + return false; + } + if (info.st_mode & S_IFDIR) { + return true; + } + return false; +} + +int +main(void) +{ + int init; +#if defined(_WIN32) + InitializeCriticalSection(&cs); + InitializeConditionVariable(&cv); + InitializeCriticalSection(&app_sync_lock); +#elif defined(__linux__) + struct sigaction sa; + sigfillset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = handle_signal; + sigaction(SIGINT, &sa, NULL); +#endif + +#if 0 + oc_log_set_level(OC_LOG_LEVEL_DEBUG); +#endif + + static const oc_handler_t handler = { .init = app_init, + .signal_event_loop = signal_event_loop, + .register_resources = + register_resources }; + + oc_set_con_res_announced(false); + // max app data size set to 13k large enough to hold full IDD + oc_set_max_app_data_size(13312); +#ifdef OC_STORAGE + if (!directoryFound("dummy_bridge_linux_creds")) { + printf( + "Creating dummy_bridge_linux_creds directory for persistant storage."); +#ifdef WIN32 + CreateDirectory("dummy_bridge_linux_creds", NULL); +#else + mkdir("dummy_bridge_linux_creds", 0755); +#endif + } + oc_storage_config("./dummy_bridge_linux_creds/"); +#endif /* OC_STORAGE */ + + init = oc_main_init(&handler); + if (init < 0) + return init; + +#if defined(_WIN32) + event_thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ocf_event_thread, + NULL, 0, NULL); + if (NULL == event_thread) { + return -1; + } +#elif defined(__linux__) + if (pthread_create(&event_thread, NULL, &ocf_event_thread, NULL) != 0) { + return -1; + } +#endif + + int c; + while (quit != 1) { + display_menu(); + SCANF("%d", &c); + switch (c) { + case 0: + continue; + break; + case 1: + discover_light(0u); + break; + case 2: + discover_light(1u); + break; + case 3: + discover_light(2u); + break; + case 4: + discover_light(3u); + break; + case 5: + discover_light(4u); + break; + case 6: + display_summary(); + break; + case 7: + discover_vitual_devices = !discover_vitual_devices; + break; + case 8: + display_ascii_ui = !display_ascii_ui; + break; +#ifdef OC_SECURITY + case 9: + reset_device(); + break; +#endif /* OC_SECURITY */ + case 10: + delete_device(); + break; + case 99: + handle_signal(0); + break; + default: + break; + } + } + +#if defined(_WIN32) + WaitForSingleObject(event_thread, INFINITE); +#elif defined(__linux__) + pthread_join(event_thread, NULL); +#endif + return 0; +} diff --git a/apps/dummy_bridge_virtual_light_IDD.cbor b/apps/dummy_bridge_virtual_light_IDD.cbor new file mode 100644 index 0000000000000000000000000000000000000000..d63df467fd7dbd7a7dfac07ad82e296a3579cb6e GIT binary patch literal 2885 zcmcImTTdHD6fXT0G*YFNl801^_l84oUJ^<`vIM0GkBBIDZFDX1r)@jdSbjtah8CW@gy~#KH-L@6IMuVuDAkSpaRgBH1z#RT;Y&Q_Dn%M zdh*m`5Ocw;O6~p11ySRJN`AmVQ^gB=^69uBisWEG>5CiD)u-Nk)8hiqVnU%qrjLe* zxL}cF<5a=t5{a^FCJ?3CYr)e5{s&9WAF`yyA@(GvUUB&_mtotfzh}h6Qspge4TnBN zL$w5PF^ktEYXZk=^5#uQU%5q1;t z)1Vama|xBo>p80gEhJ@T7App#6d6Z6{&Pm@$C}clqI8j3N<4Ony!nWThKvhn@_G!H z>V6FeLz0y+sdkb`$_$8TshDN)d409UlF>0xlinslGf00&oVB7f_dzpWEs(p6uH!H> zTHR;#Ld6S$(Qv>wDfI}lEEjn@4<8`}xe(NRnML+C*%Hh?o|pMb9Rd5`-~-um&Rlzg zcI$>^lzoY9ITAxld^H8F(+S_+UIX40}zL)c>Y9YXQaLif~ZRTL)!w`kft zw{-Pw%g_b+Fe}cswz@>nOsT)a%=BbgAv#>XbZqS9vOp-r%yga^P2VNheX;9_(d&=T z5B*N>=#TE%FP-NBeuqb={$YSyxL(6`aC910S5EC|Z+7~u<@2cE4?*pHJS3fXv$Ip- zZJc$Q%kDDEAUyj9U7P7mWpG9ebPUhoZp&M&(M9?{)f8gX?f(KDV55XCx2>ywIvE$S zoF+pYJ#Z39LqF*GVek?RHyKC`3QscZM`8D67}zN!*Zc7SZgUEwH+GoPuRRYNqrf>z z;9Jax+>B#c3N#Wmx(PRF8M$0~R4VY^^$)goH~V|rAKaWJIBo$}{MBI8p|Th&J7A68 z&psWbv);j{t*-~rrG6Yy*oYYIM#S6gZ*Bi}u#JXQgVu0= zYsYy<3~}}(gUs2jjcv>(lR +#include + +#ifndef OC_VOD_MAP_H +#define OC_VOD_MAP_H + +#ifdef __cplusplus +extern "C" { +#endif +/** + * vod mapping list example : + * { + * "vods" : [ + * {"vod_id":"virtual_device_id-1", "econame": "UPnP", "index":1}, + * {"vod_id":"virtual_device_id-2", "econame": "ZigBee", "index":2} + * ], + * "next_index": 3 + * } + */ + +/** + * @struct oc_vod_mapping_list_t + * - vods : list of VOD (oc_virtual_device_t) + * - next_index : index of g_oc_device_info[]. + * next new VOD will be added to g_oc_device_info[next_index] + */ +typedef struct oc_vod_mapping_list_s +{ + OC_LIST_STRUCT(vods); + size_t next_index; ///< index of g_oc_device_info[]. new VOD will be added to + ///< this position +} oc_vod_mapping_list_t; + +/* + * open vod_map file from creds directory and populate `oc_vod_mapping_list_t` + * initilize this from the add_bridge + */ +/** + * @brief + * - initialize VOD list : `g_vod_mapping_list.vods` + * - initialize next_index with `g_device_count` + * - load existing `g_vod_mapping_list` from disk + */ +void oc_vod_map_init(void); + +/* + * release all of the memory + */ +void oc_vod_map_free(void); + +/* + * Reset the vod map as if no VODs had been discovered. + */ +void oc_vod_map_reset(void); + +/** + * @brief find Device in `g_vod_mapping_list.vods` list and return + * Device index of it (index of g_oc_device_info[]). + * + * @param vod_id id to be used as VOD's ID + * (UUID, serial number, or any other identifier that can + * identify the VOD) + * @param vod_id_size size of vod_id + * @param econame econame string + * + * @return index of the vod + * @return 0 if not found + */ +size_t oc_vod_map_get_vod_index(const uint8_t *vod_id, size_t vod_id_size, + const char *econame); + +/** + * + * @brief add new VOD mapping entry (identified by vod_id) to the proper + * position of `g_vod_mapping_list.vods` list, and update + * `g_vod_mapping_list.next_index`. finally, write updated vod_map file. + * + * @param vod_id id to be used as VOD's ID + * (UUID, serial number, or any other identifier that can + * identify the VOD) + * @param vod_id_size size of vod_id + * @param econame econame string + * + * @return index of just added vod (index of `g_oc_device_info[]`) + */ +size_t oc_vod_map_add_mapping_entry(const uint8_t *vod_id, + const size_t vod_id_size, + const char *econame); + +/* + * Remove the vod_id at the given device index + * This will update the next_index so freed indexes + * can be reused. The virtual device associated + * with this index should + */ +void oc_vod_map_remove_mapping_entry(size_t device_index); + +/* + * Walk the vodmap and return the econame at the given index + */ +void oc_vod_map_get_econame(oc_string_t *econame, size_t device_index); + +/** + * @brief retrieve oc_virtual_device_t entry mapped to `device_index` + * @param device_index device index + * @return + * - oc_virtual_device_t * + * - NULL on error + */ +oc_virtual_device_t *oc_vod_map_get_mapping_entry(size_t device_index); + +/** + * @brief retrieve list of all oc_virtual_device_t instances + * + * @return head of g_vod_mapping_list.vods + */ +oc_virtual_device_t *oc_vod_map_get_mapping_list(void); + +#ifdef __cplusplus +} +#endif + +#endif // OC_VOD_MAP_H diff --git a/port/linux/Makefile b/port/linux/Makefile index 42fba4c0bd..15aa11e7a0 100644 --- a/port/linux/Makefile +++ b/port/linux/Makefile @@ -290,6 +290,12 @@ ifeq ($(PUSH_DEBUG), 1) endif endif +# for Bridging +ifeq ($(BRIDGE), 1) + EXTRA_CFLAGS += -DOC_BRIDGE + SAMPLES += dummy_bridge_linux +endif + ifneq ($(SECURE),0) SRC += $(addprefix ../../security/, oc_acl.c oc_ael.c oc_audit.c oc_certs.c oc_certs_generate.c oc_certs_validate.c \ oc_cred.c oc_cred_util.c oc_csr.c oc_doxm.c oc_entropy.c oc_keypair.c oc_oscore_engine.c oc_oscore_crypto.c \ @@ -659,6 +665,10 @@ push_configurator_multithread_linux: libiotivity-lite-client-server.a $(ROOT_DIR @mkdir -p $@_creds ${CC} -o $@ ../../apps/$@.c libiotivity-lite-client-server.a -DOC_CLIENT -DOC_SERVER ${CFLAGS} ${LIBS} +dummy_bridge_linux: libiotivity-lite-client-server.a $(ROOT_DIR)/apps/dummy_bridge_linux.c + @mkdir -p $@_creds + ${CC} -o $@ ../../apps/$@.c libiotivity-lite-client-server.a -DOC_CLIENT -DOC_SERVER ${CFLAGS} ${LIBS} + $(SO_DPP_OBJ): $(ROOT_DIR)/apps/streamlined_onboarding/ocf_dpp.c ${CC} -o $@ -c $^ ${CFLAGS} ${SECURITY_HEADERS} ${LIBS} diff --git a/security/oc_acl.c b/security/oc_acl.c index f5694e635d..daea03d1bd 100644 --- a/security/oc_acl.c +++ b/security/oc_acl.c @@ -77,6 +77,48 @@ oc_sec_acl_init(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +_init_acl(size_t device_index) +{ + memset(&g_aclist[device_index], 0, sizeof(oc_sec_acl_t)); + OC_LIST_STRUCT_INIT(&g_aclist[device_index], subjects); +} + +void +oc_sec_acl_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + /* + * if `g_oc_device_info[device_index]` is newly allocated entry... + */ + g_aclist = (oc_sec_acl_t *)realloc(g_aclist, oc_core_get_num_devices() * + sizeof(oc_sec_acl_t)); + if (!g_aclist) { + oc_abort("Insufficient memory"); + } + + _init_acl(device_index); + + size_t i = 0; + while (i < device_index) { + OC_LIST_STRUCT_REINIT(&g_aclist[i], subjects); + i++; + } + } else if (device_index < oc_core_get_num_devices()) { + /* + * if `g_oc_device_info[device_index]` is an existing entry... + */ + oc_sec_acl_clear(device_index, NULL, NULL); + _init_acl(device_index); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_sec_acl_t * oc_sec_get_acl(size_t device) { @@ -1011,7 +1053,13 @@ void oc_sec_acl_free(void) { for (size_t device = 0; device < oc_core_get_num_devices(); ++device) { - oc_sec_acl_clear(device, NULL, NULL); +#ifdef OC_HAS_FEATURE_BRIDGE + if (oc_core_get_device_info(device)->is_removed == false) { +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_sec_acl_clear(device, NULL, NULL); +#ifdef OC_HAS_FEATURE_BRIDGE + } +#endif /* OC_HAS_FEATURE_BRIDGE */ } #ifdef OC_DYNAMIC_ALLOCATION if (g_aclist != NULL) { diff --git a/security/oc_acl_internal.h b/security/oc_acl_internal.h index 392fbfba36..8e36a078f0 100644 --- a/security/oc_acl_internal.h +++ b/security/oc_acl_internal.h @@ -36,6 +36,20 @@ extern "C" { #define OC_ACE_WC_ALL_PUBLIC_STR "-" void oc_sec_acl_init(void); + +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief increase existing memory for acl for all Devices + * by the size of `oc_sec_acl_t` + * + * @param[in] device_index index of `g_oc_device_info[]` where new Device is + * stored + * @param[in] need_realloc indicates whether reallocation of memory for SVR is + * needed or not + */ +void oc_sec_acl_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_acl_free(void); void oc_sec_acl_default(size_t device); bool oc_sec_encode_acl(size_t device, oc_interface_mask_t iface_mask, diff --git a/security/oc_ael.c b/security/oc_ael.c index a7904b9998..8a363d5dde 100644 --- a/security/oc_ael.c +++ b/security/oc_ael.c @@ -127,11 +127,58 @@ oc_sec_ael_init(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +_init_ael(size_t device_index) +{ + memset(&ael[device_index], 0, sizeof(oc_sec_ael_t)); + OC_LIST_STRUCT_INIT(&ael[device_index], events); +} + +void +oc_sec_ael_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + /* + * if `g_oc_device_info[device_index]` is newly allocated entry... + */ + ael = (oc_sec_ael_t *)realloc(ael, oc_core_get_num_devices() * + sizeof(oc_sec_ael_t)); + if (!ael) { + oc_abort("Insufficient memory"); + } + + _init_ael(device_index); + + size_t i = 0; + while (i < device_index) { + OC_LIST_STRUCT_REINIT(&ael[i], events); + i++; + } + } else if (device_index < oc_core_get_num_devices()) { + /* + * if `g_oc_device_info[device_index]` is existing entry... + */ + oc_sec_ael_reset(device_index); + _init_ael(device_index); + } + +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_ael_free(void) { for (size_t device = 0; device < oc_core_get_num_devices(); device++) { - oc_sec_ael_reset(device); +#ifdef OC_HAS_FEATURE_BRIDGE + if (oc_core_get_device_info(device)->is_removed == false) { +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_sec_ael_reset(device); +#ifdef OC_HAS_FEATURE_BRIDGE + } +#endif /* OC_HAS_FEATURE_BRIDGE */ } #ifdef OC_DYNAMIC_ALLOCATION free(ael); @@ -139,6 +186,14 @@ oc_sec_ael_free(void) #endif /* OC_DYNAMIC_ALLOCATION */ } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_sec_ael_free_device(size_t index) +{ + oc_sec_ael_reset(index); +} +#endif + void oc_sec_ael_default(size_t device) { diff --git a/security/oc_ael_internal.h b/security/oc_ael_internal.h index 16babf4b95..78ee3e840b 100644 --- a/security/oc_ael_internal.h +++ b/security/oc_ael_internal.h @@ -92,6 +92,12 @@ typedef struct oc_sec_ael_t } oc_sec_ael_t; void oc_sec_ael_init(void); + +#ifdef OC_HAS_FEATURE_BRIDGE +void oc_sec_ael_new_device(size_t device_index, bool need_realloc); +void oc_sec_ael_free_device(size_t index); +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_ael_free(void); void oc_sec_ael_default(size_t device); diff --git a/security/oc_cred.c b/security/oc_cred.c index d8eb1bfe5d..c56260be6a 100644 --- a/security/oc_cred.c +++ b/security/oc_cred.c @@ -92,6 +92,48 @@ oc_sec_cred_init(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +_init_cred(size_t device_index) +{ + memset(&g_devices[device_index], 0, sizeof(oc_sec_creds_t)); + OC_LIST_STRUCT_INIT(&g_devices[device_index], creds); +} + +void +oc_sec_cred_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + /* + * if `g_oc_device_info[device_index]` is newly allocated entry... + */ + g_devices = (oc_sec_creds_t *)realloc(g_devices, oc_core_get_num_devices() * + sizeof(oc_sec_creds_t)); + if (!g_devices) { + oc_abort("Insufficient memory"); + } + + _init_cred(device_index); + + size_t i = 0; + while (i < device_index) { + OC_LIST_STRUCT_REINIT(&g_devices[i], creds); + i++; + } + } else if (device_index < oc_core_get_num_devices()) { + /* + * if `g_oc_device_info[device_index]` is existing entry... + */ + oc_sec_cred_clear(device_index, NULL, NULL); + _init_cred(device_index); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + static oc_sec_cred_t * cred_get_by_credid(int credid, bool roles_resource, const oc_tls_peer_t *client, size_t device) @@ -306,7 +348,13 @@ void oc_sec_cred_deinit(void) { for (size_t device = 0; device < oc_core_get_num_devices(); device++) { - oc_sec_cred_clear(device, NULL, NULL); +#ifdef OC_HAS_FEATURE_BRIDGE + if (oc_core_get_device_info(device)->is_removed == false) { +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_sec_cred_clear(device, NULL, NULL); +#ifdef OC_HAS_FEATURE_BRIDGE + } +#endif /* OC_HAS_FEATURE_BRIDGE */ } #ifdef OC_DYNAMIC_ALLOCATION if (g_devices != NULL) { diff --git a/security/oc_cred_internal.h b/security/oc_cred_internal.h index 1b5172ba09..31abdc2bf6 100644 --- a/security/oc_cred_internal.h +++ b/security/oc_cred_internal.h @@ -86,6 +86,20 @@ oc_sec_cred_t *oc_sec_cred_remove_from_device_by_credid(int credid, void oc_sec_cred_default(size_t device); void oc_sec_cred_init(void); void oc_sec_cred_deinit(void); + +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief increase existing memory for cred for all Devices + * by the size of `oc_sec_creds_t` + * + * @param[in] device_index index of `g_oc_device_info[]` where new Device is + * stored + * @param[in] need_realloc indicates whether reallocation of memory for SVR is + * needed or not* + */ +void oc_sec_cred_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_encode_cred(size_t device, oc_interface_mask_t iface_mask, bool to_storage); bool oc_sec_decode_cred(const oc_rep_t *rep, oc_sec_cred_t **owner, diff --git a/security/oc_doxm.c b/security/oc_doxm.c index ca6b9d2d20..5213b01234 100644 --- a/security/oc_doxm.c +++ b/security/oc_doxm.c @@ -206,6 +206,27 @@ oc_sec_doxm_init(void) oc_set_select_oxms_cb(NULL, NULL); } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_sec_doxm_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + g_doxm = (oc_sec_doxm_t *)realloc(g_doxm, oc_core_get_num_devices() * + sizeof(oc_sec_doxm_t)); + if (!g_doxm) { + oc_abort("Insufficient memory"); + } + memset(&g_doxm[device_index], 0, sizeof(oc_sec_doxm_t)); + } else if (device_index < oc_core_get_num_devices()) { + memset(&g_doxm[device_index], 0, sizeof(oc_sec_doxm_t)); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + static void doxm_evaluate_supported_oxms(size_t device) { diff --git a/security/oc_doxm_internal.h b/security/oc_doxm_internal.h index 55c01e112e..ba5b647211 100644 --- a/security/oc_doxm_internal.h +++ b/security/oc_doxm_internal.h @@ -66,6 +66,18 @@ typedef struct /** @brief Allocate and initialize global variables */ void oc_sec_doxm_init(void); +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * by the size of `oc_sec_doxm_t` + * + * @param[in] device_index index of `g_oc_device_info[]` where new Device is + * stored + * @param[in] need_realloc indicates whether reallocation of memory for SVR is + * needed or not + */ +void oc_sec_doxm_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** @brief Deallocate global variables */ void oc_sec_doxm_free(void); diff --git a/security/oc_pstat.c b/security/oc_pstat.c index fc9e6673a1..860c1c1d0b 100644 --- a/security/oc_pstat.c +++ b/security/oc_pstat.c @@ -97,6 +97,27 @@ oc_sec_pstat_init(void) #endif /* OC_DYNAMIC_ALLOCATION */ } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_sec_pstat_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + g_pstat = (oc_sec_pstat_t *)realloc(g_pstat, oc_core_get_num_devices() * + sizeof(oc_sec_pstat_t)); + if (!g_pstat) { + oc_abort("Insufficient memory"); + } + memset(&g_pstat[device_index], 0, sizeof(oc_sec_pstat_t)); + } else if (device_index < oc_core_get_num_devices()) { + memset(&g_pstat[device_index], 0, sizeof(oc_sec_pstat_t)); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + static bool nil_uuid(const oc_uuid_t *uuid) { @@ -787,6 +808,10 @@ void oc_reset_devices_in_RFOTM(void) { for (size_t device = 0; device < oc_core_get_num_devices(); device++) { +#ifdef OC_HAS_FEATURE_BRIDGE + if (oc_core_get_device_info(device)->is_removed) + continue; +#endif if (g_pstat[device].s == OC_DOS_RFOTM) { oc_pstat_reset_device(device, true); } diff --git a/security/oc_pstat_internal.h b/security/oc_pstat_internal.h index 1b49bb51cf..4b7e9019d2 100644 --- a/security/oc_pstat_internal.h +++ b/security/oc_pstat_internal.h @@ -64,6 +64,19 @@ void oc_sec_pstat_init(void); /** @brief Deallocate global variables */ void oc_sec_pstat_free(void); +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief increase existing memory for pstat for all Devices + * by the size of `oc_sec_pstat_t` + * + * @param[in] device_index index of `g_oc_device_info[]` where new Device is + * stored + * @param[in] need_realloc indicates whether reallocation of memory for SVR is + * needed or not + */ +void oc_sec_pstat_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** * @brief Get pstat resource representation for given device * diff --git a/security/oc_sdi.c b/security/oc_sdi.c index 5b137c3edb..58e0b29937 100644 --- a/security/oc_sdi.c +++ b/security/oc_sdi.c @@ -55,6 +55,28 @@ oc_sec_sdi_init(void) #endif /* OC_DYNAMIC_ALLOCATION */ } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_sec_sdi_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + g_sdi = (oc_sec_sdi_t *)realloc(g_sdi, oc_core_get_num_devices() * + sizeof(oc_sec_sdi_t)); + if (!g_sdi) { + oc_abort("Insufficient memory"); + } + memset(&g_sdi[device_index], 0, sizeof(oc_sec_sdi_t)); + } else if (device_index < oc_core_get_num_devices()) { + oc_free_string(&(g_sdi[device_index].name)); + memset(&g_sdi[device_index], 0, sizeof(oc_sec_sdi_t)); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_sdi_free(void) { @@ -64,7 +86,13 @@ oc_sec_sdi_free(void) } #endif /* OC_DYNAMIC_ALLOCATION */ for (size_t device = 0; device < oc_core_get_num_devices(); ++device) { - oc_free_string(&(g_sdi[device].name)); +#ifdef OC_HAS_FEATURE_BRIDGE + if (oc_core_get_device_info(device)->is_removed == false) { +#endif /* OC_HAS_FEATURE_BRIDGE */ + oc_free_string(&(g_sdi[device].name)); +#ifdef OC_HAS_FEATURE_BRIDGE + } +#endif /* OC_HAS_FEATURE_BRIDGE */ } #ifdef OC_DYNAMIC_ALLOCATION diff --git a/security/oc_sdi_internal.h b/security/oc_sdi_internal.h index 06aaa4fc69..bdd57c92d3 100644 --- a/security/oc_sdi_internal.h +++ b/security/oc_sdi_internal.h @@ -49,6 +49,10 @@ typedef struct */ void oc_sec_sdi_init(void); +#ifdef OC_HAS_FEATURE_BRIDGE +void oc_sec_sdi_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** * @brief Deallocate all sdi resource data. */ diff --git a/security/oc_sp.c b/security/oc_sp.c index b87546e4b6..b6ea0ccdd2 100644 --- a/security/oc_sp.c +++ b/security/oc_sp.c @@ -75,6 +75,45 @@ oc_sec_sp_init(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +static void +_init_sp(size_t device_index) +{ + memset(&g_sp[device_index], 0, sizeof(oc_sec_sp_t)); + memset(&g_sp_mfg_default[device_index], 0, sizeof(oc_sec_sp_t)); + + g_sp_mfg_default[device_index].current_profile = OC_SP_BASELINE; + g_sp_mfg_default[device_index].supported_profiles = OC_SP_BASELINE; + g_sp_mfg_default[device_index].credid = -1; +} + +void +oc_sec_sp_new_device(size_t device_index, bool need_realloc) +{ +#ifdef OC_DYNAMIC_ALLOCATION + if ((device_index == (oc_core_get_num_devices() - 1)) && need_realloc) { + g_sp = (oc_sec_sp_t *)realloc(g_sp, oc_core_get_num_devices() * + sizeof(oc_sec_sp_t)); + if (!g_sp) { + oc_abort("Insufficient memory"); + } + + g_sp_mfg_default = (oc_sec_sp_t *)realloc( + g_sp_mfg_default, oc_core_get_num_devices() * sizeof(oc_sec_sp_t)); + if (!g_sp_mfg_default) { + oc_abort("Insufficient memory"); + } + + _init_sp(device_index); + } else if (device_index < oc_core_get_num_devices()) { + _init_sp(device_index); + } else { + OC_ERR("device index error ! (%zu)", device_index); + } +#endif /* OC_DYNAMIC_ALLOCATION */ +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_sp_free(void) { diff --git a/security/oc_sp_internal.h b/security/oc_sp_internal.h index 880583d114..ff8c089751 100644 --- a/security/oc_sp_internal.h +++ b/security/oc_sp_internal.h @@ -51,6 +51,10 @@ typedef struct /** @brief Allocate and initialize global variables */ void oc_sec_sp_init(void); +#ifdef OC_HAS_FEATURE_BRIDGE +void oc_sec_sp_new_device(size_t device_index, bool need_realloc); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** @brief Deallocate global variables */ void oc_sec_sp_free(void); diff --git a/security/oc_store.c b/security/oc_store.c index 95d54fba9c..798b272f70 100644 --- a/security/oc_store.c +++ b/security/oc_store.c @@ -92,7 +92,7 @@ void oc_sec_load_pstat(size_t device) { if (oc_storage_data_load("pstat", device, store_decode_pstat, NULL) <= 0) { - OC_DBG("failed to load pstat from storage for device(%zu)", device); + OC_ERR("failed to load pstat from storage for device(%zu)", device); oc_sec_pstat_default(device); return; } diff --git a/security/oc_svr.c b/security/oc_svr.c index ed35991c80..b5bed01717 100644 --- a/security/oc_svr.c +++ b/security/oc_svr.c @@ -34,6 +34,9 @@ #include "oc_sp_internal.h" #include "oc_svr_internal.h" #include "port/oc_log_internal.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_store.h" +#endif void oc_sec_svr_create(void) @@ -70,6 +73,68 @@ oc_sec_svr_create(void) } } +#ifdef OC_HAS_FEATURE_BRIDGE +void +oc_sec_svr_create_new_device(size_t device_index, bool need_realloc) +{ + oc_sec_doxm_new_device(device_index, need_realloc); + oc_sec_pstat_new_device(device_index, need_realloc); + oc_sec_acl_new_device(device_index, need_realloc); + oc_sec_cred_new_device(device_index, need_realloc); + oc_sec_ael_new_device(device_index, need_realloc); + oc_sec_sp_new_device(device_index, need_realloc); + oc_sec_sdi_new_device(device_index, need_realloc); + + oc_sec_doxm_create_resource(device_index); + oc_core_populate_resource(OCF_SEC_PSTAT, device_index, "/oic/sec/pstat", + OC_IF_RW | OC_IF_BASELINE, OC_IF_RW, + OC_DISCOVERABLE | OC_OBSERVABLE, get_pstat, 0, + post_pstat, 0, 1, "oic.r.pstat"); + oc_core_populate_resource(OCF_SEC_ACL, device_index, "/oic/sec/acl2", + OC_IF_RW | OC_IF_BASELINE, OC_IF_RW, + OC_DISCOVERABLE | OC_SECURE, get_acl, 0, post_acl, + delete_acl, 1, "oic.r.acl2"); + oc_sec_cred_create_resource(device_index); + oc_core_populate_resource(OCF_SEC_AEL, device_index, "/oic/sec/ael", + OC_IF_RW | OC_IF_BASELINE, OC_IF_RW, + OC_DISCOVERABLE | OC_SECURE, get_ael, 0, post_ael, + 0, 1, "oic.r.ael"); + + oc_sec_sp_create_resource(device_index); + oc_sec_sdi_create_resource(device_index); +#ifdef OC_PKI + oc_sec_csr_create_resource(device_index); + oc_sec_roles_create_resource(device_index); +#endif /* OC_PKI */ +} + +void +oc_sec_svr_init_new_device(size_t device_index) +{ + oc_sec_load_unique_ids(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading pstat(%zu)", device_index); + oc_sec_load_pstat(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading doxm(%zu)", device_index); + oc_sec_load_doxm(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading cred(%zu)", device_index); + oc_sec_load_cred(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading acl(%zu)", device_index); + oc_sec_load_acl(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading sp(%zu)", device_index); + oc_sec_load_sp(device_index); + OC_DBG("oc_core_add_new_device_at_index(): loading ael(%zu)", device_index); + oc_sec_load_ael(device_index); +#ifdef OC_PKI + OC_DBG("oc_core_add_new_device_at_index(): loading ECDSA keypair(%zu)", + device_index); + oc_sec_load_ecdsa_keypair(device_index); +#endif /* OC_PKI */ + OC_DBG("oc_core_add_new_device_at_index(): loading sdi(%zu)", device_index); + oc_sec_load_sdi(device_index); +} + +#endif /* OC_HAS_FEATURE_BRIDGE */ + void oc_sec_svr_free(void) { diff --git a/security/oc_svr_internal.h b/security/oc_svr_internal.h index 173dc6856d..47757207c1 100644 --- a/security/oc_svr_internal.h +++ b/security/oc_svr_internal.h @@ -19,6 +19,9 @@ #ifndef OC_SVR_INTERNAL_H #define OC_SVR_INTERNAL_H +#include "util/oc_features.h" +#include + #ifdef __cplusplus extern "C" { #endif @@ -28,6 +31,28 @@ extern "C" { */ void oc_sec_svr_create(void); +#ifdef OC_HAS_FEATURE_BRIDGE +/** + * @brief add SVR for the Device which is added dynamically. + * new Device should be added to `g_oc_device_info[]` + * before calling this function. + * + * @param[in] device_index index of `g_oc_device_info[]` where new Device is + * stored + * @param[in] need_realloc indicates whether reallocation of memory for SVR is + * needed or not + */ +void oc_sec_svr_create_new_device(size_t device_index, bool need_realloc); + +/** + * @brief update SVR with stored values, + * if there is no store data, initialize with default value. + * + * @param[in] device_index index of Device stored in `g_oc_device_info[]` + */ +void oc_sec_svr_init_new_device(size_t device_index); +#endif /* OC_HAS_FEATURE_BRIDGE */ + /** * @brief Deinitialize secure vertical resources; */ diff --git a/security/unittest/acltest.cpp b/security/unittest/acltest.cpp index a8d1b48841..d4f72f9867 100644 --- a/security/unittest/acltest.cpp +++ b/security/unittest/acltest.cpp @@ -1,6 +1,7 @@ /****************************************************************** * * Copyright 2022 Daniel Adam, All Rights Reserved. + * Copyright 2024 ETRI Joo-Chul Kevin Lee, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -34,6 +35,11 @@ #include "api/oc_push_internal.h" #endif /* OC_HAS_FEATURE_PUSH */ +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_bridge.h" +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + #include "gtest/gtest.h" #include @@ -219,4 +225,39 @@ TEST_F(TestAcl, oc_sec_check_acl_in_RFOTM) } #endif /* OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM */ +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsAclEntryInitialized(const oc_sec_acl_t *aclEntry) +{ + /* + * resource owner should be null + * subject list should be empty + */ + if ((oc_uuid_is_empty(aclEntry->rowneruuid)) && + !oc_list_length(aclEntry->subjects)) { + return true; + } + return false; +} + +/* + * oc_sec_acl_new_device(device_index, need_realloc) + */ +TEST_F(TestAcl, AclNewDevice) +{ + /* + * overwrite entry in the existing position + */ + auto aclEntry = oc_sec_get_acl(device_id_); + auto orgAcl = std::make_unique(); + + memcpy(orgAcl.get(), aclEntry, sizeof(oc_sec_acl_t)); + + oc_sec_acl_new_device(device_id_, false); + EXPECT_EQ(true, IsAclEntryInitialized(aclEntry)); + + memcpy(aclEntry, orgAcl.get(), sizeof(oc_sec_acl_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/security/unittest/credtest.cpp b/security/unittest/credtest.cpp index 3d9ad4bcfd..dc334039c6 100644 --- a/security/unittest/credtest.cpp +++ b/security/unittest/credtest.cpp @@ -1,6 +1,7 @@ /**************************************************************************** * * Copyright (c) 2023 plgd.dev s.r.o. + * Copyright (c) 2024 ETRI Joo-Chul Kevin Lee * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -41,6 +42,11 @@ #include "api/oc_push_internal.h" #endif /* OC_HAS_FEATURE_PUSH */ +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_bridge.h" +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + #include #include #include @@ -405,4 +411,40 @@ TEST_F(TestCreds, Serialize_Fail) #endif /* OC_PKI && (OC_DYNAMIC_ALLOCATION || OC_TEST) */ +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsCredsEntryInitialized(const oc_sec_creds_t *credsEntry) +{ + /* + * resource owner should be null + * subject list should be empty + */ + if ((oc_uuid_is_empty(credsEntry->rowneruuid)) && + !oc_list_length(credsEntry->creds)) { + return true; + } + + return false; +} + +/* + * oc_sec_cred_new_device(device_index, need_realloc) + */ +TEST_F(TestCreds, CredNewDevice) +{ + /* + * overwrite entry in the existing position + */ + auto credsEntry = oc_sec_get_creds(kDeviceID); + auto orgCreds = std::make_unique(); + + memcpy(orgCreds.get(), credsEntry, sizeof(oc_sec_creds_t)); + + oc_sec_cred_new_device(kDeviceID, false); + EXPECT_EQ(true, IsCredsEntryInitialized(credsEntry)); + + memcpy(credsEntry, orgCreds.get(), sizeof(oc_sec_creds_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/security/unittest/doxmtest.cpp b/security/unittest/doxmtest.cpp index 61faecaf04..40b2332bfe 100644 --- a/security/unittest/doxmtest.cpp +++ b/security/unittest/doxmtest.cpp @@ -1,6 +1,8 @@ /****************************************************************** * * Copyright 2023 Daniel Adam, All Rights Reserved. + * Copyright 2024 ETRI Joo-Chul Kevin Lee, All Rights Reserved. + * * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -35,6 +37,11 @@ #include "tests/gtest/Resource.h" #include "tests/gtest/Storage.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include "oc_bridge.h" +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + #include #include #include @@ -1076,4 +1083,38 @@ TEST_F(TestDoxmWithServer, Owned_F) #endif /* OC_DYNAMIC_ALLOCATION */ } +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsDoxmEntryInitialized(const oc_sec_doxm_t *doxmEntry) +{ + auto emptyDoxm = std::make_unique(); + memset(emptyDoxm.get(), 0, sizeof(oc_sec_doxm_t)); + + if (!memcmp(doxmEntry, emptyDoxm.get(), sizeof(oc_sec_doxm_t))) { + return true; + } + return false; +} + +TEST_F(TestDoxmWithServer, DoxmNewDevice) +{ + /* + * overwrite entry in the existing position + */ + /* + * add new acl entry to the end of the array + */ + auto doxmEntry = oc_sec_get_doxm(kDeviceID); + auto orgDoxm = std::make_unique(); + + memcpy(orgDoxm.get(), doxmEntry, sizeof(oc_sec_doxm_t)); + + oc_sec_doxm_new_device(kDeviceID, false); + + EXPECT_EQ(true, IsDoxmEntryInitialized(doxmEntry)); + + memcpy(doxmEntry, orgDoxm.get(), sizeof(oc_sec_doxm_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/security/unittest/pstattest.cpp b/security/unittest/pstattest.cpp index 4385810248..a5d273e928 100644 --- a/security/unittest/pstattest.cpp +++ b/security/unittest/pstattest.cpp @@ -1,6 +1,7 @@ /****************************************************************** * * Copyright 2023 Daniel Adam, All Rights Reserved. + * Copyright 2024 ETRI Joo-Chul Kevin Lee, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -34,6 +35,10 @@ #include "tests/gtest/Resource.h" #include "util/oc_features.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + #ifdef OC_SOFTWARE_UPDATE #include "api/oc_swupdate_internal.h" #endif /* OC_SOFTWARE_UPDATE */ @@ -218,4 +223,34 @@ TEST_F(TestPstatWithServer, DeleteRequest_Fail) error_code); } +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsPstatEntryInitialized(const oc_sec_pstat_t *pstatEntry) +{ + auto emptyPstat = std::make_unique(); + memset(emptyPstat.get(), 0, sizeof(oc_sec_pstat_t)); + + if (!memcmp(pstatEntry, emptyPstat.get(), sizeof(oc_sec_pstat_t))) { + return true; + } + return false; +} + +TEST_F(TestPstatWithServer, PstatNewDevice) +{ + /* + * overwrite entry in the existing position + */ + auto pstatEntry = oc_sec_get_pstat(kDeviceID); + auto orgPstat = std::make_unique(); + + memcpy(orgPstat.get(), pstatEntry, sizeof(oc_sec_pstat_t)); + oc_sec_pstat_new_device(kDeviceID, false); + + EXPECT_EQ(true, IsPstatEntryInitialized(pstatEntry)); + + memcpy(pstatEntry, orgPstat.get(), sizeof(oc_sec_pstat_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/security/unittest/sditest.cpp b/security/unittest/sditest.cpp index a8efa095ca..3720977b78 100644 --- a/security/unittest/sditest.cpp +++ b/security/unittest/sditest.cpp @@ -1,6 +1,7 @@ /****************************************************************** * * Copyright 2023 Daniel Adam, All Rights Reserved. + * Copyright 2024 ETRI Joo-Chul Kevin Lee, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -41,6 +42,10 @@ #include #include +#ifdef OC_HAS_FEATURE_BRIDGE +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + using namespace std::chrono_literals; static const std::string testStorage{ "storage_test" }; @@ -400,4 +405,34 @@ TEST_F(TestSdiWithServer, DumpAndLoad) oc_free_string(&def.name); } +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsSdiEntryInitialized(const oc_sec_sdi_t *sdiEntry) +{ + auto emptySdi = std::make_unique(); + memset(emptySdi.get(), 0, sizeof(oc_sec_sdi_t)); + + if (!memcmp(sdiEntry, emptySdi.get(), sizeof(oc_sec_sdi_t))) { + return true; + } + return false; +} + +TEST_F(TestSdiWithServer, SdiNewDevice) +{ + /* + * overwrite entry in the existing position + */ + auto sdiEntry = oc_sec_sdi_get(kDeviceID); + auto orgSdi = std::make_unique(); + + memcpy(orgSdi.get(), sdiEntry, sizeof(oc_sec_sdi_t)); + oc_sec_sdi_new_device(kDeviceID, false); + + EXPECT_EQ(true, IsSdiEntryInitialized(sdiEntry)); + + memcpy(sdiEntry, orgSdi.get(), sizeof(oc_sec_sdi_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/security/unittest/sptest.cpp b/security/unittest/sptest.cpp index 86489efb61..a0e48aef20 100644 --- a/security/unittest/sptest.cpp +++ b/security/unittest/sptest.cpp @@ -1,6 +1,7 @@ /****************************************************************** * * Copyright 2023 Daniel Adam, All Rights Reserved. + * Copyright 2024 ETRI Joo-Chul Kevin Lee, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"), * you may not use this file except in compliance with the License. @@ -40,6 +41,10 @@ #include "tests/gtest/Storage.h" #include "util/oc_macros_internal.h" +#ifdef OC_HAS_FEATURE_BRIDGE +#include +#endif /* OC_HAS_FEATURE_BRIDGE */ + #ifdef OC_HAS_FEATURE_PUSH #include "api/oc_push_internal.h" #endif /* OC_HAS_FEATURE_PUSH */ @@ -468,4 +473,39 @@ TEST_F(TestSecurityProfileWithServer, DeleteRequest_Fail) error_code); } +#ifdef OC_HAS_FEATURE_BRIDGE +static bool +IsSpEntryInitialized(const oc_sec_sp_t *sdiEntry) +{ + auto emptySp = std::make_unique(); + memset(emptySp.get(), 0, sizeof(oc_sec_sp_t)); + + if (/*!memcmp(sdiEntry, emptySp.get(), sizeof(oc_sec_sp_t)) + &&*/ + (sdiEntry->current_profile == OC_SP_BASELINE) && + (sdiEntry->supported_profiles == OC_SP_BASELINE) && + (sdiEntry->credid == -1)) { + return true; + } + return false; +} + +TEST_F(TestSecurityProfile, SpNewDevice) +{ + /* + * overwrite entry in the existing position + */ + auto spEntry = oc_sec_sp_get(kDeviceID); + auto orgSp = std::make_unique(); + + memcpy(orgSp.get(), spEntry, sizeof(oc_sec_sp_t)); + oc_sec_sp_new_device(kDeviceID, false); + oc_sec_sp_default(kDeviceID); + + EXPECT_EQ(true, IsSpEntryInitialized(spEntry)); + + memcpy(spEntry, orgSp.get(), sizeof(oc_sec_sp_t)); +} +#endif /* OC_HAS_FEATURE_BRIDGE */ + #endif /* OC_SECURITY */ diff --git a/sonar-project.properties b/sonar-project.properties index e2156596da..644f51c8af 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -6,7 +6,7 @@ sonar.organization=iotivity-lite #sonar.projectVersion=1.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -#sonar.sources=. +# sonar.sources=. # TODO: Java, Python, HTML, JavaScript and CSS are disabled until a maintainer is found sonar.exclusions=apps/**,deps/**,docker/**,patches/**,tests/**,tools/**,**/*.java,**/*.py,**/*.html,**/*.js,**/*.css diff --git a/util/oc_features.h b/util/oc_features.h index 908993caa4..d41d4957ff 100644 --- a/util/oc_features.h +++ b/util/oc_features.h @@ -39,6 +39,11 @@ #define OC_HAS_FEATURE_PUSH #endif +#if defined(OC_BRIDGE) && defined(OC_SERVER) && defined(OC_CLIENT) && \ + defined(OC_DYNAMIC_ALLOCATION) +#define OC_HAS_FEATURE_BRIDGE +#endif + #if defined(OC_SECURITY) && defined(OC_RESOURCE_ACCESS_IN_RFOTM) #define OC_HAS_FEATURE_RESOURCE_ACCESS_IN_RFOTM #endif