From 655bbf190cd8601b366b355a6e736137005d1cfc Mon Sep 17 00:00:00 2001 From: Florian Reimold <11774314+FlorianReimold@users.noreply.github.com> Date: Fri, 12 Jan 2024 07:34:47 +0100 Subject: [PATCH] Initial commit --- .clang-tidy | 66 ++ .github/workflows/build-macos.yml | 47 ++ .github/workflows/build-ubuntu.yml | 143 ++++ .github/workflows/build-windows.yml | 166 ++++ .gitignore | 22 + .gitmodules | 9 + CMakeLists.txt | 101 +++ LICENSE | 177 ++++ README.md | 27 + cmake/ecaludp-module/Findecaludp.cmake | 2 + cmake/ecaludpConfig.cmake.in | 6 + cpack_config.cmake | 20 + ecaludp/CMakeLists.txt | 195 +++++ ecaludp/ecaludp_version.h.in | 5 + ecaludp/include/ecaludp/error.h | 114 +++ ecaludp/include/ecaludp/owning_buffer.h | 55 ++ ecaludp/include/ecaludp/raw_memory.h | 153 ++++ ecaludp/include/ecaludp/socket.h | 156 ++++ ecaludp/src/protocol/datagram_builder_v5.cpp | 227 +++++ ecaludp/src/protocol/datagram_builder_v5.h | 48 ++ ecaludp/src/protocol/datagram_description.h | 56 ++ ecaludp/src/protocol/header_common.h | 29 + ecaludp/src/protocol/header_v5.h | 62 ++ ecaludp/src/protocol/header_v6.h | 45 + ecaludp/src/protocol/portable_endian.h | 134 +++ ecaludp/src/protocol/reassembly_v5.cpp | 288 +++++++ ecaludp/src/protocol/reassembly_v5.h | 94 +++ ecaludp/src/socket.cpp | 237 ++++++ ecaludp/version.cmake | 3 + samples/ecaludp_sample/CMakeLists.txt | 41 + samples/ecaludp_sample/src/main.cpp | 132 +++ samples/integration_test/CMakeLists.txt | 39 + samples/integration_test/src/main.cpp | 65 ++ tests/ecaludp_private_test/CMakeLists.txt | 47 ++ .../src/fragmentation_v5_test.cpp | 774 ++++++++++++++++++ tests/ecaludp_test/CMakeLists.txt | 42 + tests/ecaludp_test/src/atomic_signalable.h | 205 +++++ .../ecaludp_test/src/ecaludp_socket_test.cpp | 158 ++++ .../src/fragmentation_v5_test.cpp | 774 ++++++++++++++++++ thirdparty/asio | 1 + thirdparty/asio-module/Findasio.cmake | 30 + thirdparty/build-asio.cmake | 2 + thirdparty/build-gtest.cmake | 19 + thirdparty/build-recycle.cmake | 5 + thirdparty/googletest | 1 + thirdparty/googletest-module/FindGTest.cmake | 1 + thirdparty/recycle | 1 + thirdparty/recycle-module/Findrecycle.cmake | 1 + 48 files changed, 5025 insertions(+) create mode 100644 .clang-tidy create mode 100644 .github/workflows/build-macos.yml create mode 100644 .github/workflows/build-ubuntu.yml create mode 100644 .github/workflows/build-windows.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/ecaludp-module/Findecaludp.cmake create mode 100644 cmake/ecaludpConfig.cmake.in create mode 100755 cpack_config.cmake create mode 100644 ecaludp/CMakeLists.txt create mode 100644 ecaludp/ecaludp_version.h.in create mode 100644 ecaludp/include/ecaludp/error.h create mode 100644 ecaludp/include/ecaludp/owning_buffer.h create mode 100644 ecaludp/include/ecaludp/raw_memory.h create mode 100644 ecaludp/include/ecaludp/socket.h create mode 100644 ecaludp/src/protocol/datagram_builder_v5.cpp create mode 100644 ecaludp/src/protocol/datagram_builder_v5.h create mode 100644 ecaludp/src/protocol/datagram_description.h create mode 100644 ecaludp/src/protocol/header_common.h create mode 100644 ecaludp/src/protocol/header_v5.h create mode 100644 ecaludp/src/protocol/header_v6.h create mode 100644 ecaludp/src/protocol/portable_endian.h create mode 100644 ecaludp/src/protocol/reassembly_v5.cpp create mode 100644 ecaludp/src/protocol/reassembly_v5.h create mode 100644 ecaludp/src/socket.cpp create mode 100644 ecaludp/version.cmake create mode 100644 samples/ecaludp_sample/CMakeLists.txt create mode 100644 samples/ecaludp_sample/src/main.cpp create mode 100644 samples/integration_test/CMakeLists.txt create mode 100644 samples/integration_test/src/main.cpp create mode 100644 tests/ecaludp_private_test/CMakeLists.txt create mode 100644 tests/ecaludp_private_test/src/fragmentation_v5_test.cpp create mode 100644 tests/ecaludp_test/CMakeLists.txt create mode 100644 tests/ecaludp_test/src/atomic_signalable.h create mode 100644 tests/ecaludp_test/src/ecaludp_socket_test.cpp create mode 100644 tests/ecaludp_test/src/fragmentation_v5_test.cpp create mode 160000 thirdparty/asio create mode 100644 thirdparty/asio-module/Findasio.cmake create mode 100644 thirdparty/build-asio.cmake create mode 100644 thirdparty/build-gtest.cmake create mode 100644 thirdparty/build-recycle.cmake create mode 160000 thirdparty/googletest create mode 100644 thirdparty/googletest-module/FindGTest.cmake create mode 160000 thirdparty/recycle create mode 100644 thirdparty/recycle-module/Findrecycle.cmake diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..a78a584 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,66 @@ +--- +# Resons why specific warnings have been turned off: +# +# -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling +# This warns about memcpy and wants us to use memcpy_s, which is not available in our gcc setup. +# +# -cppcoreguidelines-pro-type-vararg +# This forbids using functions like printf, snprintf etc. We would like to use those either way. +# +# -misc-no-recursion +# Recursion with functions can be an elegant way of solving recursive problems +# +# These checks have been disabled to keep compatibility with C++14: +# -modernize-concat-nested-namespaces +# -modernize-use-nodiscard +# + +Checks: "-*, + clang-analyzer-*, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-narrowing-conversions, + + cppcoreguidelines-*, + -cppcoreguidelines-avoid-c-arrays, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-narrowing-conversions, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-no-malloc, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-reinterpret-cast, + + misc-*, + -misc-include-cleaner, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + + modernize-*, + -modernize-avoid-c-arrays, + -modernize-pass-by-value, + -modernize-use-trailing-return-type, + -modernize-use-auto, + -modernize-concat-nested-namespaces, + -modernize-return-braced-init-list, + -modernize-use-nodiscard, + + performance-*, + + readability-*, + -readability-braces-around-statements, + -readability-identifier-length, + -readability-magic-numbers, + -readability-redundant-access-specifiers, + -readability-function-cognitive-complexity, + -readability-else-after-return, +" +WarningsAsErrors: '' +HeaderFilterRegex: '^((?!/thirdparty/|/_deps/).)*$' +FormatStyle: none diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 0000000..c245076 --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,47 @@ +name: macOS + +on: + push: + pull_request: + branches: [ master ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build-macos: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: macos-latest + + steps: + + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + fetch-depth: 0 + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: | + cmake -B ${{github.workspace}}/_build \ + -DECALUDP_BUILD_SAMPLES=ON \ + -DECALUDP_BUILD_TESTS=ON \ + -DECALUDP_USE_BUILTIN_ASIO=ON \ + -DECALUDP_USE_BUILTIN_RECYCLE=ON \ + -DECALUDP_USE_BUILTIN_GTEST=ON \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + shell: bash + + - name: Build + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} + + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build + diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml new file mode 100644 index 0000000..f2662a9 --- /dev/null +++ b/.github/workflows/build-ubuntu.yml @@ -0,0 +1,143 @@ +name: Ubuntu + +on: + push: + pull_request: + branches: [ master ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + PROJECT_NAME: ecaludp + +jobs: + build-ubuntu: + + strategy: + matrix: + library_type: [static, shared, object] + os: [ubuntu-22.04, ubuntu-20.04] + + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ${{ matrix.os }} + + steps: + + - name: Set Variables + run: | + if [[ '${{ matrix.library_type }}' == 'static' ]]; then + echo "build_shared_libs=OFF" >> "$GITHUB_ENV" + echo "ecaludp_library_type=STATIC" >> "$GITHUB_ENV" + echo "package_postfix=static" >> "$GITHUB_ENV" + elif [[ '${{ matrix.library_type }}' == 'shared' ]]; then + echo "build_shared_libs=ON" >> "$GITHUB_ENV" + echo "ecaludp_library_type=SHARED" >> "$GITHUB_ENV" + echo "package_postfix=shared" >> "$GITHUB_ENV" + elif [[ '${{ matrix.library_type }}' == 'object' ]]; then + echo "build_shared_libs=OFF" >> "$GITHUB_ENV" + echo "ecaludp_library_type=OBJECT" >> "$GITHUB_ENV" + echo "package_postfix=object" >> "$GITHUB_ENV" + fi + + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + fetch-depth: 0 + + ############################################ + # Test-compile the project + ############################################ + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: | + cmake -B ${{github.workspace}}/_build \ + -DECALUDP_BUILD_SAMPLES=ON \ + -DECALUDP_BUILD_TESTS=ON \ + -DECALUDP_USE_BUILTIN_ASIO=ON \ + -DECALUDP_USE_BUILTIN_RECYCLE=ON \ + -DECALUDP_USE_BUILTIN_GTEST=ON \ + -DECALUDP_LIBRARY_TYPE=${{env.ecaludp_library_type}} \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DBUILD_SHARED_LIBS=${{ env.build_shared_libs }} + + - name: Build + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} + + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build + + - name: Read Project Version from CMakeCache + run: | + cmake_project_version_string=$(cat "${{github.workspace}}/_build/CMakeCache.txt" | grep "^CMAKE_PROJECT_VERSION:") + arr=(${cmake_project_version_string//=/ }) + cmake_project_version=${arr[1]} + echo "CMAKE_PROJECT_VERSION=$cmake_project_version" >> "$GITHUB_ENV" + shell: bash + + - name: CPack + run: cpack -G DEB + working-directory: ${{ github.workspace }}/_build + if: ${{ matrix.library_type != 'object' }} + + - name: Rename .deb installer + run: | + mv *.deb '${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }}.deb' + shell: bash + working-directory: ${{github.workspace}}/_build/_package/ + if: ${{ matrix.library_type != 'object' }} + + - name: Upload binaries + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }} + path: ${{github.workspace}}/_build/_package/*.deb + if: ${{ matrix.library_type != 'object' }} + + ############################################ + # Test if our binary can be linked against + ############################################ + + - name: Install binaries + shell: bash + run: sudo dpkg -i ${{ github.workspace }}/_build/_package/*.deb + if: ${{ matrix.library_type != 'object' }} + + - name: Compile integration test (Release) + run: | + cmake -B ${{github.workspace}}/samples/integration_test/_build/release \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MODULE_PATH="${{ github.workspace }}/thirdparty/asio-module/" + + cmake --build ${{github.workspace}}/samples/integration_test/_build/release + + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} + + - name: Run integration test (Release) + run: ./integration_test + working-directory: ${{ github.workspace }}/samples/integration_test/_build/release + if: ${{ matrix.library_type != 'object' }} + + - name: Compile integration test (Debug) + run: | + cmake -B ${{github.workspace}}/samples/integration_test/_build/debug \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_MODULE_PATH="${{ github.workspace }}/thirdparty/asio-module/" + + cmake --build ${{github.workspace}}/samples/integration_test/_build/debug + + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} + + - name: Run integration test (Debug) + run: ./integration_test + working-directory: ${{ github.workspace }}/samples/integration_test/_build/debug + if: ${{ matrix.library_type != 'object' }} + diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 0000000..7e6a912 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,166 @@ +name: Windows + +on: + push: + pull_request: + branches: [ master ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + INSTALL_PREFIX: _install + PROJECT_NAME: ecaludp + VS_TOOLSET: v140 + VS_NAME: vs2015 + +jobs: + build-windows: + + strategy: + matrix: + library_type: [static, shared, object] + build_arch: [x64, win32] + + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: windows-2019 + + steps: + + - name: Set Variables + run: | + if ( '${{ matrix.library_type }}' -eq 'static' ) + { + echo "build_shared_libs=OFF" >> "$Env:GITHUB_ENV" + echo "ecaludp_library_type=STATIC" >> "$Env:GITHUB_ENV" + echo "package_postfix=static" >> "$Env:GITHUB_ENV" + } + elseif( '${{ matrix.library_type }}' -eq 'shared' ) + { + echo "build_shared_libs=ON" >> "$Env:GITHUB_ENV" + echo "ecaludp_library_type=SHARED" >> "$Env:GITHUB_ENV" + echo "package_postfix=shared" >> "$Env:GITHUB_ENV" + } + elseif( '${{ matrix.library_type }}' -eq 'object' ) + { + echo "build_shared_libs=OFF" >> "$Env:GITHUB_ENV" + echo "ecaludp_library_type=OBJECT" >> "$Env:GITHUB_ENV" + echo "package_postfix=object" >> "$Env:GITHUB_ENV" + } + + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'true' + fetch-depth: 0 + + ############################################ + # Test-compile the project + ############################################ + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + shell: cmd + run: | + cmake -B ${{github.workspace}}/_build ^ + -G "Visual Studio 16 2019" ^ + -A ${{ matrix.build_arch }} ^ + -T ${{ env.VS_TOOLSET }} ^ + -DECALUDP_BUILD_SAMPLES=ON ^ + -DECALUDP_BUILD_TESTS=ON ^ + -DECALUDP_USE_BUILTIN_ASIO=ON ^ + -DECALUDP_USE_BUILTIN_RECYCLE=ON ^ + -DECALUDP_USE_BUILTIN_GTEST=ON ^ + -DECALUDP_LIBRARY_TYPE=${{env.ecaludp_library_type}} ^ + -DCMAKE_INSTALL_PREFIX=${{env.INSTALL_PREFIX}} ^ + -DBUILD_SHARED_LIBS=${{ env.build_shared_libs }} + + - name: Build (Release) + shell: cmd + run: | + cmake --build ${{github.workspace}}/_build --config Release --parallel + + - name: Install (Release) + shell: cmd + run: | + cmake --build ${{github.workspace}}/_build --config Release --target INSTALL + if: ${{ matrix.library_type != 'object' }} + + - name: Build (Debug) + shell: cmd + run: | + cmake --build ${{github.workspace}}/_build --config Debug --parallel + + - name: Install (Release) + shell: cmd + run: | + cmake --build ${{github.workspace}}/_build --config Debug --target INSTALL + if: ${{ matrix.library_type != 'object' }} + + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build + + - name: Read Project Version from CMakeCache + run: | + $cmake_project_version_line = cat ${{github.workspace}}/_build/CMakeCache.txt | Select-String -Pattern ^CMAKE_PROJECT_VERSION: + $cmake_project_version = $cmake_project_version_line.Line.split("=")[1] + echo "CMAKE_PROJECT_VERSION=$cmake_project_version" >> "$Env:GITHUB_ENV" + + - name: Upload binaries + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-windows-${{ matrix.build_arch }}-${{ env.VS_NAME }}-${{ matrix.library_type }} + path: ${{github.workspace}}/${{env.INSTALL_PREFIX}} + if: ${{ matrix.library_type != 'object' }} + + ############################################ + # Test if our binary can be linked against + ############################################ + + - name: CMake integration test + shell: powershell + run: | + $module_path="${{github.workspace}}/thirdparty/asio-module" + $module_path_posix=$module_path.Replace('\', '/') + + cmake -B "${{github.workspace}}/samples/integration_test/_build" ` + -A ${{ matrix.build_arch }} ` + -DCMAKE_PREFIX_PATH="${{github.workspace}}/${{env.INSTALL_PREFIX}}" ` + -DCMAKE_MODULE_PATH="$module_path_posix" + + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} + + - name: Compile integration test (Release) + shell: cmd + run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Release + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} + + - name: Run integration test (Release) + run: | + if ( '${{ matrix.library_type }}' -eq 'shared' ) + { + $Env:Path = '${{github.workspace}}/${{env.INSTALL_PREFIX}}/bin;' + $Env:Path + } + .\integration_test.exe + working-directory: ${{ github.workspace }}/samples/integration_test/_build/Release + if: ${{ matrix.library_type != 'object' }} + + - name: Compile integration test (Debug) + shell: cmd + run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Debug + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} + + - name: Run integration test (Debug) + run: | + if ( '${{ matrix.library_type }}' -eq 'shared' ) + { + $Env:Path = '${{github.workspace}}/${{env.INSTALL_PREFIX}}/bin;' + $Env:Path + } + .\integration_test.exe + working-directory: ${{ github.workspace }}/samples/integration_test/_build/Debug + if: ${{ matrix.library_type != 'object' }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccabed3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*Debug +ipch + +*.sdf +*.suo +*.aps +*.user +*.opendb +*.db +*.vscode + +/_install +/.vs +/CMakeLists.txt.user + +# Build directories +/_build* +/build* +/samples/integration_test/_build* + +# Temporary Vim files +*.swp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0a83e87 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "thirdparty/asio"] + path = thirdparty/asio + url = https://github.com/chriskohlhoff/asio.git +[submodule "thirdparty/googletest"] + path = thirdparty/googletest + url = https://github.com/google/googletest.git +[submodule "thirdparty/recycle"] + path = thirdparty/recycle + url = https://github.com/steinwurf/recycle.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..af5a4ae --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,101 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +cmake_minimum_required(VERSION 3.13) + +include(CMakeDependentOption) + +# Project call +include("${CMAKE_CURRENT_LIST_DIR}/ecaludp/version.cmake") +project(ecaludp VERSION ${ECALUDP_VERSION_MAJOR}.${ECALUDP_VERSION_MINOR}.${ECALUDP_VERSION_PATCH}) + +# Normalize backslashes from Windows paths +file(TO_CMAKE_PATH "${CMAKE_MODULE_PATH}" CMAKE_MODULE_PATH) +file(TO_CMAKE_PATH "${CMAKE_PREFIX_PATH}" CMAKE_PREFIX_PATH) +message(STATUS "Module Path: ${CMAKE_MODULE_PATH}") +message(STATUS "Prefix Path: ${CMAKE_PREFIX_PATH}") + +# CMake Options +option(ECALUDP_BUILD_SAMPLES + "Build project samples." + ON) +option(ECALUDP_BUILD_TESTS + "Build the eCAL UDP tests" + OFF) + +option(ECALUDP_USE_BUILTIN_ASIO + "Use the builtin asio submodule. If set to OFF, asio must be available from somewhere else (e.g. system libs)." + ON) +option(ECALUDP_USE_BUILTIN_RECYCLE + "Use the builtin steinwurf::recycle submodule. If set to OFF, recycle must be available from somewhere else (e.g. system libs)." + ON) +cmake_dependent_option(ECALUDP_USE_BUILTIN_GTEST + "Use the builtin GoogleTest submodule. Only needed if ECALUDP_BUILD_TESTS is ON. If set to OFF, GoogleTest must be available from somewhere else (e.g. system libs)." + ON # Default value if dependency is met + "ECALUDP_BUILD_TESTS" # Dependency + OFF) # Default value if dependency is not met + +# Set Debug postfix +set(CMAKE_DEBUG_POSTFIX d) +set(CMAKE_MINSIZEREL_POSTFIX minsize) +set(CMAKE_RELWITHDEBINFO_POSTFIX reldbg) + +# Use builtin asio +if (ECALUDP_USE_BUILTIN_ASIO) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/build-asio.cmake") +endif() + +# Use builtin recycle +if (ECALUDP_USE_BUILTIN_RECYCLE) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/build-recycle.cmake") +endif() + +# Use builtin gtest +if (ECALUDP_USE_BUILTIN_GTEST) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/build-gtest.cmake") +endif() + +# For tests we need to make sure that all shared libraries and executables are +# put into the same directory. Otherwise the tests will fail on windows. +if(ECALUDP_BUILD_TESTS AND BUILD_SHARED_LIBS AND ECALUDP_USE_BUILTIN_GTEST) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +endif() + +# Add main ecaludp library +add_subdirectory(ecaludp) + +# Add the ecaludp dummy module +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ecaludp-module) + +if (ECALUDP_BUILD_SAMPLES) + add_subdirectory(samples/ecaludp_sample) +endif() + +if (ECALUDP_BUILD_TESTS) + enable_testing() + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/tests/ecaludp_test") + + # Check if ecaludp is a static lib. We can only add the private tests for + # static libs and object libs, as we need to have access to the private + # implementation details. + get_target_property(ecaludp_target_type ecaludp TYPE) + if ((ecaludp_target_type STREQUAL STATIC_LIBRARY) OR (ecaludp_target_type STREQUAL OBJECT_LIBRARY)) + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/tests/ecaludp_private_test") + endif() +endif() + +# Make this package available for packing with CPack +include("${CMAKE_CURRENT_LIST_DIR}/cpack_config.cmake") diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4947287 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..893e26b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +# ecaludp + +ecaludp is the underlying implementation for UDP traffic in eCAL. It transparently fragments and reassembles messages to provide support for big messages. + +## Dependencies + +The following dependencies are required to build ecaludp: + +| **Dependency** | **License** | **Integration** | +|----------------|-------------|-----------------| +| [asio](https://github.com/chriskohlhoff/asio) | [Boost Software License](https://github.com/chriskohlhoff/asio/blob/master/asio/LICENSE_1_0.txt) | [git submodule](https://github.com/eclipse-ecal/ecaludp/tree/master/thirdparty) | +| [recycle](https://github.com/steinwurf/recycle) | [BSD-3](https://github.com/steinwurf/recycle/blob/master/LICENSE.rst) | [git submodule](https://github.com/eclipse-ecal/ecaludp/tree/master/thirdparty) | + +## CMake Options + +You can set the following CMake Options to control how ecaludp is built: + +|**Option** | **Type** | **Default** | **Explanation** | +|---------------------------------|----------|-------------|-----------------------------------------------------------------------------------------------------------------| +| `ECALUDP_BUILD_SAMPLES` | `BOOL` | `ON` | Build the ecaludp sample project. | +| `ECALUDP_BUILD_TESTS` | `BOOL` | `OFF` | Build the the ecaludp tests. Requires gtest to be available. If ecaludp is built as static or object library, additional tests will be built that test the internal implementation that is not available as public API. | +| `ECALUDP_USE_BUILTIN_ASIO`| `BOOL`| `ON` | Use the builtin asio submodule. If set to `OFF`, asio must be available from somewhere else (e.g. system libs). | +| `ECALUDP_USE_BUILTIN_RECYCLE`| `BOOL`| `ON` | Use the builtin steinwurf::recycle submodule. If set to `OFF`, recycle must be available from somewhere else (e.g. system libs). | +| `ECALUDP_USE_BUILTIN_GTEST`| `BOOL`| `ON`
_(when building tests)_ | Use the builtin GoogleTest submodule. Only needed if `FINEFTP_SERVER_BUILD_TESTS` is `ON`. If set to `OFF`, GoogleTest must be available from somewhere else (e.g. system libs). | +| `ECALUDP_LIBRARY_TYPE` | `STRING` | | Controls the library type of ecaludp. Currently supported are `STATIC` / `SHARED` / `OBJECT`. If set, this will override the regular `BUILD_SHARED_LIBS` CMake option. If not set, that option will be used. | \ No newline at end of file diff --git a/cmake/ecaludp-module/Findecaludp.cmake b/cmake/ecaludp-module/Findecaludp.cmake new file mode 100644 index 0000000..3aff4af --- /dev/null +++ b/cmake/ecaludp-module/Findecaludp.cmake @@ -0,0 +1,2 @@ +# Stub find script for in-source build of samples +set(ecaludp_FOUND True) diff --git a/cmake/ecaludpConfig.cmake.in b/cmake/ecaludpConfig.cmake.in new file mode 100644 index 0000000..806b5d6 --- /dev/null +++ b/cmake/ecaludpConfig.cmake.in @@ -0,0 +1,6 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(asio) + +INCLUDE("${CMAKE_CURRENT_LIST_DIR}/ecaludpTargets.cmake") \ No newline at end of file diff --git a/cpack_config.cmake b/cpack_config.cmake new file mode 100755 index 0000000..2f3aae7 --- /dev/null +++ b/cpack_config.cmake @@ -0,0 +1,20 @@ +set(CPACK_PACKAGE_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A UDP-socket doing transparent fragmentation to support large packaes") +set(CPACK_PACKAGE_VENDOR "Eclipse eCAL") +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_PACKAGE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/_package") + +set(CPACK_PACKAGE_CONTACT "florian.reimold@continental-corporation.com") + +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Florian Reimold ") +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/eclipse-ecal/ecaludp") +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) +set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON) + +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_LIST_DIR}/LICENSE") +set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_LIST_DIR}/README.md") + +include(CPack) diff --git a/ecaludp/CMakeLists.txt b/ecaludp/CMakeLists.txt new file mode 100644 index 0000000..b0a216e --- /dev/null +++ b/ecaludp/CMakeLists.txt @@ -0,0 +1,195 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +cmake_minimum_required(VERSION 3.13) + +include("${CMAKE_CURRENT_LIST_DIR}/version.cmake") +project(ecaludp VERSION ${ECALUDP_VERSION_MAJOR}.${ECALUDP_VERSION_MINOR}.${ECALUDP_VERSION_PATCH}) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Disable default export of symbols +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) + +find_package(asio REQUIRED) +find_package(recycle REQUIRED) + +# Include GenerateExportHeader that will create export macros for us +include(GenerateExportHeader) + +# Public API include directory +set (includes + include/ecaludp/error.h + include/ecaludp/owning_buffer.h + include/ecaludp/raw_memory.h + include/ecaludp/socket.h +) + +# Private source files +set(sources + src/socket.cpp + src/protocol/datagram_builder_v5.cpp + src/protocol/datagram_builder_v5.h + src/protocol/datagram_description.h + src/protocol/header_common.h + src/protocol/header_v5.h + src/protocol/header_v6.h + src/protocol/portable_endian.h + src/protocol/reassembly_v5.cpp + src/protocol/reassembly_v5.h +) + +# Build as object library +add_library (${PROJECT_NAME} ${ECALUDP_LIBRARY_TYPE} + ${includes} + ${sources} +) + +# Generate version defines +configure_file("ecaludp_version.h.in" "${PROJECT_BINARY_DIR}/include/ecaludp/ecaludp_version.h" @ONLY) + +# Generate header with export macros +generate_export_header(${PROJECT_NAME} + EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/include/ecaludp/ecaludp_export.h + BASE_NAME ECALUDP +) + +add_library (ecaludp::ecaludp ALIAS ${PROJECT_NAME}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + asio::asio + PRIVATE + # Link header-only libs (recycle) as described in this workaround: + # https://gitlab.kitware.com/cmake/cmake/-/issues/15415#note_633938 + $ + $<$:ws2_32> + $<$:wsock32> +) + +target_compile_definitions(${PROJECT_NAME} + PRIVATE + ASIO_STANDALONE + _WIN32_WINNT=0x0601 +) + +# Check if ecaludp is a static lib. We can only add the private tests for +# static libs and object libs, as we need to have access to the private +# implementation details. +get_target_property(ecaludp_target_type ecaludp TYPE) +if (ecaludp_target_type STREQUAL OBJECT_LIBRARY) + target_compile_definitions(${PROJECT_NAME} + PUBLIC + ECALUDP_STATIC_DEFINE + ) +endif() + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) + +target_compile_options(${PROJECT_NAME} PRIVATE + $<$,$,$>: + -Wall -Wextra> + $<$: + /W4>) + + +# Add own public include directory +target_include_directories(${PROJECT_NAME} + PUBLIC + $ + $ # To find the export file generated by generate_export_header + $ + PRIVATE + "src/" +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + OUTPUT_NAME ${PROJECT_NAME} + FOLDER ecal/udp +) + +################################## + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${includes} + ${sources} +) + + +################################################################################ +### Installation rules +################################################################################ + +set(ECALUDP_INSTALL_CMAKE_DIR "lib/cmake/ecaludp") + +# Install Runtime & Libs +install( + TARGETS ${PROJECT_NAME} + EXPORT ecaludpTargets + + RUNTIME + DESTINATION "bin" + COMPONENT ecaludp_runtime + + LIBRARY + DESTINATION "lib" + COMPONENT ecaludp_runtime + + ARCHIVE + DESTINATION "lib" + COMPONENT ecaludp_dev +) + +# Install public header files (-> dev package) +install( + DIRECTORY "include/ecaludp" + DESTINATION "include" + COMPONENT ecaludp_dev + FILES_MATCHING PATTERN "*.h" +) + +# Install the auto-generated header with the export macros (-> dev package) +install( + DIRECTORY "${PROJECT_BINARY_DIR}/include/ecaludp" + DESTINATION "include" + COMPONENT ecaludp_dev + FILES_MATCHING PATTERN "*.h" +) + +install( + EXPORT ecaludpTargets + FILE ecaludpTargets.cmake + DESTINATION ${ECALUDP_INSTALL_CMAKE_DIR} + NAMESPACE ecaludp:: + COMPONENT ecaludp_dev +) + +# Create and install Config.cmake file (-> dev package) + +include(CMakePackageConfigHelpers) + +configure_package_config_file( + "../cmake/ecaludpConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cmake_/ecaludpConfig.cmake" + INSTALL_DESTINATION ${ECALUDP_INSTALL_CMAKE_DIR} + PATH_VARS ECALUDP_INSTALL_CMAKE_DIR +) +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/cmake_/ecaludpConfig.cmake" + DESTINATION ${ECALUDP_INSTALL_CMAKE_DIR} + COMPONENT ecaludp_dev +) \ No newline at end of file diff --git a/ecaludp/ecaludp_version.h.in b/ecaludp/ecaludp_version.h.in new file mode 100644 index 0000000..8172028 --- /dev/null +++ b/ecaludp/ecaludp_version.h.in @@ -0,0 +1,5 @@ +#pragma once + +#define ECALUDP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ +#define ECALUDP_VERSION_MINOR @PROJECT_VERSION_MINOR@ +#define ECALUDP_VERSION_PATCH @PROJECT_VERSION_PATCH@ diff --git a/ecaludp/include/ecaludp/error.h b/ecaludp/include/ecaludp/error.h new file mode 100644 index 0000000..e26d4fe --- /dev/null +++ b/ecaludp/include/ecaludp/error.h @@ -0,0 +1,114 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +#include + +namespace ecaludp +{ + class Error + { + ////////////////////////////////////////// + // Data model + ////////////////////////////////////////// + public: + enum ErrorCode + { + // Generic + OK, + GENERIC_ERROR, + + // Receiving + UNSUPPORTED_PROTOCOL_VERSION, + + DUPLICATE_DATAGRAM, + MALFORMED_DATAGRAM, + MALFORMED_REASSEMBLED_MESSAGE, + }; + + ////////////////////////////////////////// + // Constructor & Destructor + ////////////////////////////////////////// + public: + Error(ErrorCode error_code, const std::string& message) : error_code_(error_code), message_(message) {} + Error(ErrorCode error_code) : error_code_(error_code) {} + + // Copy constructor & assignment operator + Error(const Error& other) = default; + Error& operator=(const Error& other) = default; + + // Move constructor & assignment operator + Error(Error&& other) = default; + Error& operator=(Error&& other) = default; + + ~Error() = default; + + ////////////////////////////////////////// + // Public API + ////////////////////////////////////////// + public: + inline std::string GetDescription() const + { + switch (error_code_) + { + // Generic + case OK: return "OK"; break; + case GENERIC_ERROR: return "Error"; break; + + case UNSUPPORTED_PROTOCOL_VERSION: return "Unsupported protocol version"; break; + case DUPLICATE_DATAGRAM: return "Duplicate datagram"; break; + case MALFORMED_DATAGRAM: return "Malformed datagram"; break; + case MALFORMED_REASSEMBLED_MESSAGE: return "Malformed reassembled message"; break; + + default: return "Unknown error"; + } + } + + inline std::string ToString() const + { + return (message_.empty() ? GetDescription() : GetDescription() + " (" + message_ + ")"); + } + + const inline std::string& GetMessage() const + { + return message_; + } + + ////////////////////////////////////////// + // Operators + ////////////////////////////////////////// + inline operator bool() const { return error_code_ != ErrorCode::OK; } + inline bool operator== (const Error& other) const { return error_code_ == other.error_code_; } + inline bool operator== (const ErrorCode other) const { return error_code_ == other; } + inline bool operator!= (const Error& other) const { return error_code_ != other.error_code_; } + inline bool operator!= (const ErrorCode other) const { return error_code_ != other; } + + inline Error& operator=(ErrorCode error_code) + { + error_code_ = error_code; + return *this; + } + + ////////////////////////////////////////// + // Member Variables + ////////////////////////////////////////// + private: + ErrorCode error_code_; + std::string message_; + }; + +} // namespace ecaludp diff --git a/ecaludp/include/ecaludp/owning_buffer.h b/ecaludp/include/ecaludp/owning_buffer.h new file mode 100644 index 0000000..b97c50d --- /dev/null +++ b/ecaludp/include/ecaludp/owning_buffer.h @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include +#include + +namespace ecaludp +{ + class OwningBuffer + { + public: + // Default constructor + OwningBuffer() + : data_ (nullptr) + , size_ (0) + , owning_container_(nullptr) + {} + + // Constructor + OwningBuffer(const void* data, size_t size, const std::shared_ptr& owning_container) + : data_ (data) + , size_ (size) + , owning_container_(owning_container) + {} + + const void* data() const + { + return data_; + } + + size_t size() const + { + return size_; + } + + private: + const void* data_; + const size_t size_; + const std::shared_ptr owning_container_; + }; +} diff --git a/ecaludp/include/ecaludp/raw_memory.h b/ecaludp/include/ecaludp/raw_memory.h new file mode 100644 index 0000000..e38f316 --- /dev/null +++ b/ecaludp/include/ecaludp/raw_memory.h @@ -0,0 +1,153 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include +#include +#include +#include +#include + +namespace ecaludp +{ + class RawMemory + { + public: + // Default constructor + RawMemory() + : data_ (nullptr) + , capacity_(0) + , size_ (0) + {} + + // Constructor + RawMemory(size_t size, bool overprovisioning = true) + : data_ (malloc(overprovisioning? size * 2 : size)) + , capacity_(overprovisioning? size * 2 : size) + , size_ (size) + {} + + // Copy constructor + RawMemory(const RawMemory& other) + : data_ (malloc(other.capacity_)) + , capacity_(other.capacity_) + , size_ (other.size_) + { + if ((data_ != nullptr) && (other.data_ != nullptr)) + memcpy(data_, other.data_, size_); + } + + // Move constructor + RawMemory(RawMemory&& other) noexcept + : data_ (other.data_) + , capacity_(other.capacity_) + , size_ (other.size_) + { + other.data_ = nullptr; + other.capacity_ = 0; + other.size_ = 0; + } + + // Copy assignment operator + RawMemory& operator=(const RawMemory& other) + { + if (this != &other) + { + resize(other.capacity_, false); + resize(other.size_); + if ((data_ != nullptr) && (other.data_ != nullptr)) + memcpy(data_, other.data_, size_); + } + return *this; + } + + // Move assignment operator + RawMemory& operator=(RawMemory&& other) noexcept + { + if (this != &other) + { + free(data_); + + data_ = other.data_; + capacity_ = other.capacity_; + size_ = other.size_; + + other.data_ = nullptr; + other.capacity_ = 0; + other.size_ = 0; + } + return *this; + } + + // Destructor + ~RawMemory() + { + free(data_); + } + + void swap(RawMemory& other) noexcept + { + std::swap(data_, other.data_); + std::swap(capacity_, other.capacity_); + std::swap(size_, other.size_); + } + + uint8_t* data() const + { + return reinterpret_cast(data_); + } + + size_t size() const + { + return size_; + } + + void resize(size_t size, bool overprovisioning = true) + { + // If the new size is smaller than the current capacity, we can just set the new size + if (size <= capacity_) + { + size_ = size; + return; + } + + // If the new size is larger than the current capacity, we need to allocate new memory + + // Overprovisioning means that we allocate twice the size of the requested + // size. This is useful to not have to reallocate memory too often, if the + // user resizes it to a slightly bigger size. + const size_t new_capacity = (overprovisioning ? size * 2 : size); + + // Allocate new memory + void* new_data = realloc(data_, new_capacity); + if (new_data == nullptr) + { + throw std::bad_alloc(); // TODO: decide if throwing here is the right thing to do. Also decide if we should throw at the other places as well. + } + else + { + data_ = new_data; + capacity_ = new_capacity; + size_ = size; + } + } + + private: + void* data_; + size_t capacity_; + size_t size_; + }; +} diff --git a/ecaludp/include/ecaludp/socket.h b/ecaludp/include/ecaludp/socket.h new file mode 100644 index 0000000..dad23b9 --- /dev/null +++ b/ecaludp/include/ecaludp/socket.h @@ -0,0 +1,156 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace ecaludp +{ + namespace v5 + { + class Reassembly; + } + + class recycle_shared_pool; + + class Socket + { + ///////////////////////////////////////////////////////////////// + // Constructor + ///////////////////////////////////////////////////////////////// + public: + ECALUDP_EXPORT Socket(asio::io_service& io_service, std::array magic_header_bytes); + + // Destructor + ECALUDP_EXPORT ~Socket(); + + // Disable copy constructor and assignment operator + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + + // Disable move constructor and assignment operator + Socket(Socket&&) = delete; + Socket& operator=(Socket&&) = delete; + + ///////////////////////////////////////////////////////////////// + // API Passthrough + ///////////////////////////////////////////////////////////////// + + bool at_mark() const { return socket_.at_mark(); } + bool at_mark(asio::error_code& ec) const { return socket_.at_mark(ec); } + + std::size_t available() const { return socket_.available(); } + std::size_t available(asio::error_code& ec) const { return socket_.available(ec); } + + void bind(const asio::ip::udp::endpoint& endpoint) { socket_.bind(endpoint); } + asio::error_code bind(const asio::ip::udp::endpoint& endpoint, asio::error_code& ec) { return socket_.bind(endpoint, ec); } + + void cancel() { socket_.cancel(); } + asio::error_code cancel(asio::error_code& ec) { return socket_.cancel(ec); } + + void close() { socket_.close(); } + asio::error_code close(asio::error_code& ec) { return socket_.close(ec); } + + void connect(const asio::ip::udp::endpoint& peer_endpoint) { socket_.connect(peer_endpoint); } + asio::error_code connect(const asio::ip::udp::endpoint& peer_endpoint, asio::error_code& ec) { return socket_.connect(peer_endpoint, ec); } + + const asio::any_io_executor& get_executor() { return socket_.get_executor(); } + + template + void get_option(GettableSocketOption& option) { socket_.get_option(option); } + + template + asio::error_code get_option(GettableSocketOption& option, asio::error_code& ec) { return socket_.get_option(option, ec); } + + template + void io_control(IoControlCommand& command) { socket_.io_control(command); } + + template + asio::error_code io_control(IoControlCommand& command, asio::error_code& ec) { return socket_.io_control(command, ec); } + + bool is_open() const { return socket_.is_open(); } + + asio::ip::udp::endpoint local_endpoint() const { return socket_.local_endpoint(); } + asio::ip::udp::endpoint local_endpoint(asio::error_code& ec) const { return socket_.local_endpoint(ec); } + + void open(const asio::ip::udp& protocol) { socket_.open(protocol); } + asio::error_code open(const asio::ip::udp& protocol, asio::error_code& ec) { return socket_.open(protocol, ec); } + + asio::ip::udp::endpoint remote_endpoint() const { return socket_.remote_endpoint(); } + asio::ip::udp::endpoint remote_endpoint(asio::error_code& ec) const { return socket_.remote_endpoint(ec); } + + template + void set_option(const SettableSocketOption& option) { socket_.set_option(option); } + + template + asio::error_code set_option(const SettableSocketOption& option, asio::error_code& ec) { return socket_.set_option(option, ec); } + + void shutdown(asio::socket_base::shutdown_type what) { socket_.shutdown(what); } + asio::error_code shutdown(asio::socket_base::shutdown_type what, asio::error_code& ec) { return socket_.shutdown(what, ec); } + + ///////////////////////////////////////////////////////////////// + // Sending + ///////////////////////////////////////////////////////////////// + public: + ECALUDP_EXPORT void async_send_to(const std::vector& buffer_sequence + , const asio::ip::udp::endpoint& destination + , const std::function& completion_handler); + + ECALUDP_EXPORT void set_max_udp_datagram_size(std::size_t max_udp_datagram_size); + ECALUDP_EXPORT std::size_t get_max_udp_datagram_size() const; + + ECALUDP_EXPORT void set_max_reassembly_age(std::chrono::steady_clock::duration max_reassembly_age); + ECALUDP_EXPORT std::chrono::steady_clock::duration get_max_reassembly_age() const; + ///////////////////////////////////////////////////////////////// + // Receiving + ///////////////////////////////////////////////////////////////// + public: + ECALUDP_EXPORT void async_receive_from(asio::ip::udp::endpoint& sender_endpoint + , const std::function&, asio::error_code)>& completion_handler); + + private: + void receive_next_datagram_from(asio::ip::udp::endpoint& sender_endpoint + , const std::function&, asio::error_code)>& completion_handler); + + std::shared_ptr handle_datagram(const std::shared_ptr& buffer + , const std::shared_ptr& sender_endpoint + , ecaludp::Error& error); + + ///////////////////////////////////////////////////////////////// + // Member Variables + ///////////////////////////////////////////////////////////////// + private: + asio::ip::udp::socket socket_; + std::unique_ptr datagram_buffer_pool_; + std::unique_ptr reassembly_v5_; + + std::array magic_header_bytes_; + std::size_t max_udp_datagram_size_; + std::chrono::steady_clock::duration max_reassembly_age_; + }; +} diff --git a/ecaludp/src/protocol/datagram_builder_v5.cpp b/ecaludp/src/protocol/datagram_builder_v5.cpp new file mode 100644 index 0000000..a7f3d5a --- /dev/null +++ b/ecaludp/src/protocol/datagram_builder_v5.cpp @@ -0,0 +1,227 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "datagram_builder_v5.h" + +#include "header_v5.h" +#include "portable_endian.h" +#include "protocol/datagram_description.h" +#include +#include +#include +#include +#include +#include + +namespace ecaludp +{ + namespace v5 + { + + DatagramList create_datagram_list(const std::vector& buffer_sequence, size_t max_datagram_size, std::array magic_header_bytes) + { + // TODO: Complain when the max_udp_datagram_size is too small (the header doesn't even fit) + + constexpr size_t header_size = sizeof(ecaludp::v5::Header); + + size_t total_size = 0; + for (const auto& buffer : buffer_sequence) + { + total_size += buffer.size(); + } + + if ((total_size + header_size) <= max_datagram_size) + { + DatagramList datagram_list; + datagram_list.reserve(1); + datagram_list.emplace_back(create_non_fragmented_datagram(buffer_sequence, magic_header_bytes)); + + return datagram_list; + } + else + { + return create_fragmented_datagram_list(buffer_sequence, max_datagram_size, magic_header_bytes); + } + } + + DatagramDescription create_non_fragmented_datagram(const std::vector& buffer_sequence, std::array magic_header_bytes) + { + uint32_t total_size = 0; + for (const auto& buffer : buffer_sequence) + { + total_size += static_cast(buffer.size()); + } + + // Container for the header buffer and all asio buffers + DatagramDescription datagram_description; + + // Create one buffer for the header. That buffer will be the only additional + // needed buffer. + datagram_description.header_buffer_.resize(sizeof(ecaludp::v5::Header)); + + // Fill the header + auto* header_ptr = reinterpret_cast(datagram_description.header_buffer_.data()); + + header_ptr->magic[0] = magic_header_bytes[0]; + header_ptr->magic[1] = magic_header_bytes[1]; + header_ptr->magic[2] = magic_header_bytes[2]; + header_ptr->magic[3] = magic_header_bytes[3]; + + header_ptr->version = 5; + + header_ptr->type = static_cast( + htole32(static_cast(ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message))); + header_ptr->id = htole32(int32_t(-1)); // -1 => not fragmented + header_ptr->num = htole32(uint32_t(1)); // 1 => only 1 fragment + header_ptr->len = htole32(static_cast(total_size)); // denotes the length of the payload of this message only + + // Add an asio buffer for the header buffer + datagram_description.asio_buffer_list_.emplace_back(datagram_description.header_buffer_.data(), datagram_description.header_buffer_.size()); + + // Add an asio buffer for each payload buffer + for (const auto& buffer : buffer_sequence) + { + datagram_description.asio_buffer_list_.emplace_back(buffer.data(), buffer.size()); + } + + return datagram_description; + } + + DatagramList create_fragmented_datagram_list(const std::vector& buffer_sequence, size_t max_udp_datagram_size, std::array magic_header_bytes) + { + // Count the total size of all buffers + uint32_t total_size = 0; + for (const auto& buffer : buffer_sequence) + { + total_size += static_cast(buffer.size()); + } + + // Compute how many bytes we can send at once + const int payload_bytes_per_datagram = static_cast(max_udp_datagram_size) - sizeof(ecaludp::v5::Header); + + // Compute how many datagrams we need. We need 1 datagram more, as + // the fragmentation info must be sent in a separate datagram + const uint32_t needed_fragment_count = ((total_size + (payload_bytes_per_datagram - 1)) / payload_bytes_per_datagram); + const uint32_t needed_datagram_count = 1 + needed_fragment_count; + + // Pre-allocate the datagram list, so we never have to re-allocate + DatagramList datagram_list; + datagram_list.reserve(needed_datagram_count); + + // Create a random number for the package ID, that is used to match all fragments + uint32_t message_id = 0; + { + static std::mutex mutex; + const std::lock_guard lock(mutex); + static uint32_t x = static_cast(std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()).count() + ); + static uint32_t y = 362436069; + static uint32_t z = 521288629; + message_id = xorshf96(x, y, z); + } + + // Create the fragmentation info + { + datagram_list.emplace_back(); + datagram_list.back().header_buffer_.resize(sizeof(ecaludp::v5::Header)); + + auto* fragment_info_header_ptr = reinterpret_cast(datagram_list.back().header_buffer_.data()); + + fragment_info_header_ptr->magic[0] = magic_header_bytes[0]; + fragment_info_header_ptr->magic[1] = magic_header_bytes[1]; + fragment_info_header_ptr->magic[2] = magic_header_bytes[2]; + fragment_info_header_ptr->magic[3] = magic_header_bytes[3]; + + fragment_info_header_ptr->version = 5; + + fragment_info_header_ptr->type = static_cast( + htole32(static_cast(ecaludp::v5::message_type_uint32t::msg_type_fragmented_message_info))); + fragment_info_header_ptr->id = htole32(message_id); + fragment_info_header_ptr->num = htole32(needed_fragment_count); + fragment_info_header_ptr->len = htole32(total_size); // denotes the length of the entire payload + + // Add an asio buffer for the header buffer + datagram_list.back().asio_buffer_list_.emplace_back(datagram_list.back().header_buffer_.data(), datagram_list.back().header_buffer_.size()); + } + + // Iterate over all buffers and create fragments for them + size_t buffer_index = 0; + size_t offset_in_current_buffer = 0; + + while ((buffer_index < buffer_sequence.size() + || ((buffer_index == (buffer_sequence.size() - 1)) + && (offset_in_current_buffer < buffer_sequence[buffer_index].size())))) + { + // Create a new datagram, if the last one is full, or if this is the first real datagram (besides the fragmentation info) + // TODO: Only add a new datagram, if there are more bytes to be sent. The next buffer MAY be empty, so we don't need to add a new datagram. + if ((datagram_list.size() <= 1) + || (datagram_list.back().size() >= max_udp_datagram_size)) + { + datagram_list.emplace_back(); + datagram_list.back().header_buffer_.resize(sizeof(ecaludp::v5::Header)); + + auto* const header_ptr = reinterpret_cast(datagram_list.back().header_buffer_.data()); + + header_ptr->magic[0] = magic_header_bytes[0]; + header_ptr->magic[1] = magic_header_bytes[1]; + header_ptr->magic[2] = magic_header_bytes[2]; + header_ptr->magic[3] = magic_header_bytes[3]; + + header_ptr->version = 5; + + header_ptr->type = static_cast( + htole32(static_cast(ecaludp::v5::message_type_uint32t::msg_type_fragment))); + header_ptr->id = htole32(message_id); + header_ptr->num = htole32(static_cast(datagram_list.size() - 2)); // -1, because the first datagram is the fragmentation info + header_ptr->len = htole32(static_cast(0)); // denotes the length of the entire payload + + // Add an asio buffer for the header buffer + datagram_list.back().asio_buffer_list_.emplace_back(datagram_list.back().header_buffer_.data(), datagram_list.back().header_buffer_.size()); + } + + // TODO: Handle 0-byte-buffers. Those can be discarded directly + // TODO: Also test the 0-byte-buffers. + + // Compute how many bytes from the current buffer we can fit in the datagram + const size_t bytes_to_fit_in_current_datagram = std::min(max_udp_datagram_size - datagram_list.back().size(), buffer_sequence[buffer_index].size() - offset_in_current_buffer); + + // Add an asio buffer to the datagram that points to the data in the user buffer + datagram_list.back().asio_buffer_list_.emplace_back(reinterpret_cast(buffer_sequence[buffer_index].data()) + offset_in_current_buffer, bytes_to_fit_in_current_datagram); + + // Increase the size of the current datagram + auto* const header_ptr = reinterpret_cast(datagram_list.back().header_buffer_.data()); + header_ptr->len = htole32(le32toh(header_ptr->len) + static_cast(bytes_to_fit_in_current_datagram)); + + // Increase the offset in the user buffer + offset_in_current_buffer += bytes_to_fit_in_current_datagram; + + // Check if we reached the end of the current user buffer + if (offset_in_current_buffer >= buffer_sequence[buffer_index].size()) + { + // Increase the buffer index + buffer_index++; + + // Reset the offset in the current buffer + offset_in_current_buffer = 0; + } + } + + // return the datagram list + return datagram_list; + } + } +} diff --git a/ecaludp/src/protocol/datagram_builder_v5.h b/ecaludp/src/protocol/datagram_builder_v5.h new file mode 100644 index 0000000..b20f2d5 --- /dev/null +++ b/ecaludp/src/protocol/datagram_builder_v5.h @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include "datagram_description.h" +#include +#include +#include + +namespace ecaludp +{ + namespace v5 + { + DatagramList create_datagram_list(const std::vector& buffer_sequence, size_t max_datagram_size, std::array magic_header_bytes); + + DatagramDescription create_non_fragmented_datagram(const std::vector& buffer_sequence, std::array magic_header_bytes); + + DatagramList create_fragmented_datagram_list(const std::vector& buffer_sequence, size_t max_udp_datagram_size, std::array magic_header_bytes); + + inline uint32_t xorshf96(uint32_t& x, uint32_t& y, uint32_t& z) + { + uint32_t t = 0; + x ^= x << 16; + x ^= x >> 5; + x ^= x << 1; + + t = x; + x = y; + y = z; + z = t ^ x ^ y; + + return z; + } + } +} diff --git a/ecaludp/src/protocol/datagram_description.h b/ecaludp/src/protocol/datagram_description.h new file mode 100644 index 0000000..4c6706f --- /dev/null +++ b/ecaludp/src/protocol/datagram_description.h @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include +#include + +#include + +namespace ecaludp { +class DatagramDescription +{ + public: + // Default constructor + DatagramDescription() = default; + + // Delete Copy constructor and assignment operator + DatagramDescription(const DatagramDescription&) = delete; + DatagramDescription& operator=(const DatagramDescription&) = delete; + + // Default Move constructor and assignment operator + DatagramDescription(DatagramDescription&&) = default; + DatagramDescription& operator=(DatagramDescription&&) = default; + + ~DatagramDescription() = default; + + public: + std::vector header_buffer_ {}; + std::vector asio_buffer_list_{}; + + size_t size() const + { + size_t size = 0; + for (const auto& buffer : asio_buffer_list_) + { + size += buffer.size(); + } + return size; + } + }; + + using DatagramList = std::vector; + } // namespace ecaludp diff --git a/ecaludp/src/protocol/header_common.h b/ecaludp/src/protocol/header_common.h new file mode 100644 index 0000000..f9dc7b2 --- /dev/null +++ b/ecaludp/src/protocol/header_common.h @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include + +namespace ecaludp +{ +#pragma pack(push, 1) + struct HeaderCommon + { + char magic[4]; + uint8_t version; + }; +#pragma pack(pop) +} diff --git a/ecaludp/src/protocol/header_v5.h b/ecaludp/src/protocol/header_v5.h new file mode 100644 index 0000000..957cfa0 --- /dev/null +++ b/ecaludp/src/protocol/header_v5.h @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include + +namespace ecaludp +{ + namespace v5 + { + enum class message_type_uint32t : uint32_t + { + msg_type_unknown = 0, + msg_type_fragmented_message_info = 1, // former name: msg_type_header + msg_type_fragment = 2, // former name: msg_type_content + msg_type_non_fragmented_message = 3 // former name: msg_type_header_with_content + }; + + #pragma pack(push, 1) + struct Header + { + char magic[4]; + + uint8_t version; /// Header version. Must be 5 for this version 5 header + uint8_t reserved1; /// Must be sent as 0. The old implementation used this byte as 4-byte version (little endian), but never checked it. Thus, it may be used in the future. + uint8_t reserved2; /// Must be sent as 0. The old implementation used this byte as 4-byte version (little endian), but never checked it. Thus, it may be used in the future. + uint8_t reserved3; /// Must be sent as 0. The old implementation used this byte as 4-byte version (little endian), but never checked it. Thus, it may be used in the future. + + message_type_uint32t type; /// The message type. See message_type_uint32t for possible values + + int32_t id; /// Random ID to match fragmented parts of a message (Little-endian). Used differently depending on the message type: + /// - msg_type_fragmented_message_info: The Random ID that this fragmentation info will be applied to + /// - msg_type_fragment: The Random ID that this fragment belongs to. Used to match fragments to their fragmentation info + /// - msg_type_non_fragmented_message: Unused field. Must be sent as -1. Must not be evaluated. + + uint32_t num; /// Fragment number (Little-endian). Used differently depending on the message type: + /// - msg_type_fragmented_message_info: Amount of fragments that this message was split into. + /// - msg_type_fragment: The number of this fragment + /// - msg_type_non_fragmented_message: Unused field. Must be sent as 1. Must not be evaluated. + + uint32_t len; /// Payload length (Little-endian). Used differently depending on the message type. The payload must start directly after the header. + /// - msg_type_fragmented_message_info: Length of the original message before it got fragmented. + /// Messages of this type must not carry any payload themselves. + /// - msg_type_fragment: The payload lenght of this fragment + /// - msg_type_non_fragmented_message: The payload length of this message + }; + #pragma pack(pop) + } +} \ No newline at end of file diff --git a/ecaludp/src/protocol/header_v6.h b/ecaludp/src/protocol/header_v6.h new file mode 100644 index 0000000..804cd02 --- /dev/null +++ b/ecaludp/src/protocol/header_v6.h @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include + +// TODO: Add V6 +namespace ecaludp +{ + namespace v6 + { + #pragma pack(push, 1) + struct Header + { + unsigned char magic[4]; + + uint8_t version = 0; + uint8_t header_size = 0; + uint8_t flags = 0; + uint8_t reserved = 0; + + uint32_t package_id = 0; + }; + + struct FragmentHeader + { + uint32_t total_length = 0; + uint32_t fragment_offset = 0; + }; + #pragma pop + } +} \ No newline at end of file diff --git a/ecaludp/src/protocol/portable_endian.h b/ecaludp/src/protocol/portable_endian.h new file mode 100644 index 0000000..e6d238f --- /dev/null +++ b/ecaludp/src/protocol/portable_endian.h @@ -0,0 +1,134 @@ +// This file has been taken from: +// https://gist.github.com/panzi/6856583#file-portable_endian-h +// It includes manual changes for the QNX platform +// +// "License": Public Domain +// I, Mathias Panzenböck, place this file hereby into the public domain. Use it at your own risk for whatever you like. +// In case there are jurisdictions that don't support putting things in the public domain you can also consider it to +// be "dual licensed" under the BSD, MIT and Apache licenses, if you want to. This code is trivial anyway. Consider it +// an example on how to get the endian conversion functions on different platforms. + +#ifndef PORTABLE_ENDIAN_H_ +#define PORTABLE_ENDIAN_H_ + +#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__) + +# define __WINDOWS__ + +#endif + +#if defined(__linux__) || defined(__CYGWIN__) + +# include + +#elif defined(__APPLE__) + +# include + +# define htobe16(x) OSSwapHostToBigInt16(x) +# define htole16(x) OSSwapHostToLittleInt16(x) +# define be16toh(x) OSSwapBigToHostInt16(x) +# define le16toh(x) OSSwapLittleToHostInt16(x) + +# define htobe32(x) OSSwapHostToBigInt32(x) +# define htole32(x) OSSwapHostToLittleInt32(x) +# define be32toh(x) OSSwapBigToHostInt32(x) +# define le32toh(x) OSSwapLittleToHostInt32(x) + +# define htobe64(x) OSSwapHostToBigInt64(x) +# define htole64(x) OSSwapHostToLittleInt64(x) +# define be64toh(x) OSSwapBigToHostInt64(x) +# define le64toh(x) OSSwapLittleToHostInt64(x) + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__OpenBSD__) + +# include + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__) + +# include + +# define be16toh(x) betoh16(x) +# define le16toh(x) letoh16(x) + +# define be32toh(x) betoh32(x) +# define le32toh(x) letoh32(x) + +# define be64toh(x) betoh64(x) +# define le64toh(x) letoh64(x) + +#elif defined(__WINDOWS__) + +# include +# ifdef __GNUC__ +# include +# endif + +# if BYTE_ORDER == LITTLE_ENDIAN + +# define htobe16(x) htons(x) +# define htole16(x) (x) +# define be16toh(x) ntohs(x) +# define le16toh(x) (x) + +# define htobe32(x) htonl(x) +# define htole32(x) (x) +# define be32toh(x) ntohl(x) +# define le32toh(x) (x) + +# define htobe64(x) htonll(x) +# define htole64(x) (x) +# define be64toh(x) ntohll(x) +# define le64toh(x) (x) + +# elif BYTE_ORDER == BIG_ENDIAN + + /* that would be xbox 360 */ +# define htobe16(x) (x) +# define htole16(x) __builtin_bswap16(x) +# define be16toh(x) (x) +# define le16toh(x) __builtin_bswap16(x) + +# define htobe32(x) (x) +# define htole32(x) __builtin_bswap32(x) +# define be32toh(x) (x) +# define le32toh(x) __builtin_bswap32(x) + +# define htobe64(x) (x) +# define htole64(x) __builtin_bswap64(x) +# define be64toh(x) (x) +# define le64toh(x) __builtin_bswap64(x) + +# else + +# error byte order not supported + +# endif + +# define __BYTE_ORDER BYTE_ORDER +# define __BIG_ENDIAN BIG_ENDIAN +# define __LITTLE_ENDIAN LITTLE_ENDIAN +# define __PDP_ENDIAN PDP_ENDIAN + +#elif defined(__QNXNTO__) + +# include +# include + +#else + +# error platform not supported + +#endif + +#endif diff --git a/ecaludp/src/protocol/reassembly_v5.cpp b/ecaludp/src/protocol/reassembly_v5.cpp new file mode 100644 index 0000000..f7b8804 --- /dev/null +++ b/ecaludp/src/protocol/reassembly_v5.cpp @@ -0,0 +1,288 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "reassembly_v5.h" + +#include "ecaludp/owning_buffer.h" +#include "ecaludp/raw_memory.h" +#include "header_v5.h" +#include "portable_endian.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ecaludp +{ + namespace v5 + { + ////////////////////////////////////////////////////////////////////////////// + // Constructor, Destructor + ////////////////////////////////////////////////////////////////////////////// + Reassembly::Reassembly() = default; + + ////////////////////////////////////////////////////////////////////////////// + // Receiving, datagram handling & fragment reassembly + ////////////////////////////////////////////////////////////////////////////// + + std::shared_ptr Reassembly::handle_datagram(const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error) + { + if (buffer->size() < sizeof(ecaludp::v5::Header)) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM, "Datagram too small, cannot contain V5 header. Size is " + std::to_string(buffer->size()) + " bytes."); + return nullptr; + } + + const auto* header = reinterpret_cast(buffer->data()); + + // Each message type must be handled differently + if (static_cast(le32toh(static_cast(header->type))) + == ecaludp::v5::message_type_uint32t::msg_type_fragmented_message_info) + { + return handle_datagram_fragmented_message_info(buffer, sender_endpoint, error); + } + else if (static_cast(le32toh(static_cast(header->type))) + == ecaludp::v5::message_type_uint32t::msg_type_fragment) + { + return handle_datagram_fragment(buffer, sender_endpoint, error); + } + else if (static_cast(le32toh(static_cast(header->type))) + == ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message) + { + return handle_datagram_non_fragmented_message(buffer, error); + } + else + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM, "Invalid type"); + return nullptr; + } + } + + std::shared_ptr Reassembly::handle_datagram_fragmented_message_info(const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error) + { + auto* header = reinterpret_cast(buffer->data()); + + const int32_t package_id = le32toh(header->id); + fragmented_package_key package_key{*sender_endpoint, package_id}; + + // Check if we already have a package with this id. If not, create one + auto existing_package_it = fragmented_packages_.find(package_key); + if (existing_package_it == fragmented_packages_.end()) + { + existing_package_it = fragmented_packages_.emplace(package_key, fragmented_package{}).first; + + } + else if (existing_package_it->second.first.fragment_info_received_) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM + , "Received fragment info for package " + std::to_string(package_id) + " twice"); + return nullptr; + } + + // Store that we received the fragment info + existing_package_it->second.first.fragment_info_received_ = true; + + // Set the fragmentation info + existing_package_it->second.first.total_fragments_ = le32toh(header->num); + existing_package_it->second.first.total_size_bytes_ = le32toh(header->len); + + // Resize the list of fragments, so we never have to resize again + existing_package_it->second.second.resize(existing_package_it->second.first.total_fragments_); + + // Set the last access time + existing_package_it->second.first.last_access_ = std::chrono::steady_clock::now(); + + // Maybe the message is already complete. So let's check and reassemble the + // package if necessary + return handle_fragmented_package_if_complete(existing_package_it, error); + } + + std::shared_ptr Reassembly::handle_datagram_fragment(const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error) + { + auto* header = reinterpret_cast(buffer->data()); + + const int32_t package_id = le32toh(header->id); + fragmented_package_key package_key{*sender_endpoint, package_id}; + + // Check if we already have a package with this id. If not, create one + auto existing_package_it = fragmented_packages_.find(package_key); + if (existing_package_it == fragmented_packages_.end()) + { + existing_package_it = fragmented_packages_.emplace(package_key, fragmented_package{}).first; + } + + const uint32_t package_num = le32toh(header->num); + + // Resize the list of fragments, if necessary. We only do that, if we didn't + // receive the fragment info yet, so we don't know how many fragments there + // will be, yet + if (!existing_package_it->second.first.fragment_info_received_) + { + existing_package_it->second.second.resize(std::max(existing_package_it->second.second.size(), static_cast(package_num + 1))); + } + + // Check if this fragment number fits in the list of fragments. + if (package_num >= static_cast(existing_package_it->second.second.size())) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM + , "Fragment number " + std::to_string(package_num) + " is invalid. Should be smaller than " + std::to_string(existing_package_it->second.second.size())); + return nullptr; + } + + // Check if we already received this fragment + if (existing_package_it->second.second[package_num] != nullptr) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM + , "Fragment " + std::to_string(package_num) + " for package " + std::to_string(package_id)); + return nullptr; + } + + // Check if the size information from the fragment is valid + const uint32_t fragment_size = le32toh(header->len); + const unsigned int bytes_available = (static_cast(buffer->size()) - sizeof(ecaludp::v5::Header)); + if (fragment_size > bytes_available) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM + , "Faulty size of fragment. Should be " + std::to_string(fragment_size) + + ", but only " + std::to_string(bytes_available) + " bytes availabe."); + return nullptr; + } + + // prepare a buffer view to the payload data and store the fragment in the list + const void* payload_data_ptr = static_cast(buffer->data()) + sizeof(ecaludp::v5::Header); + auto fragment_buffer_view = std::make_shared(payload_data_ptr, static_cast(fragment_size), buffer); + existing_package_it->second.second[package_num] = fragment_buffer_view; + + // Increase the number of received fragments + existing_package_it->second.first.received_fragments_++; + + // Set the last access time + existing_package_it->second.first.last_access_ = std::chrono::steady_clock::now(); + + // Maybe the message is already complete. So let's check and reassemble the + // package if necessary + return handle_fragmented_package_if_complete(existing_package_it, error); + } + + std::shared_ptr Reassembly::handle_datagram_non_fragmented_message(const std::shared_ptr& buffer, ecaludp::Error& error) + { + const auto* header = reinterpret_cast(buffer->data()); + + // Check if the size information from the header is valid + const uint32_t payload_size = le32toh(header->len); + const unsigned int bytes_available = (static_cast(buffer->size()) - sizeof(ecaludp::v5::Header)); + + if (payload_size > bytes_available) + { + error = ecaludp::Error(ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM + , "Faulty size of datagram. Should be " + std::to_string(payload_size) + + ", but only " + std::to_string(bytes_available) + " bytes availabe."); + return nullptr; + } + + // Calculate the pointer to the payload data and create an OwningBuffer for that memory area + const void* payload_data_ptr = static_cast(buffer->data()) + sizeof(ecaludp::v5::Header); + auto payload_buffer = std::make_shared(payload_data_ptr, static_cast(payload_size), buffer); + + error = ecaludp::Error::ErrorCode::OK; + return payload_buffer; + } + + std::shared_ptr Reassembly::handle_fragmented_package_if_complete(const fragmented_package_map_t::const_iterator& it, ecaludp::Error& error) + { + // Check if we have a completed package + if (!(it->second.first.fragment_info_received_) + || !(it->second.first.received_fragments_ == it->second.first.total_fragments_)) + { + error = ecaludp::Error::ErrorCode::OK; + return nullptr; + } + + // Check if the package size is valid + { + size_t cummulated_package_sizes = 0; + for (const auto& fragment : it->second.second) + { + cummulated_package_sizes += fragment->size(); + } + + if (cummulated_package_sizes != it->second.first.total_size_bytes_) + { + // TODO: Add a performance option that doesn't build the long error string. + error = ecaludp::Error(Error::ErrorCode::MALFORMED_REASSEMBLED_MESSAGE + , "Size error. Should be " + std::to_string(it->second.first.total_size_bytes_) + + " bytes, but received " + std::to_string(cummulated_package_sizes) + "bytes."); + + // Remove the package from the map. We don't need it anymore, as it is corrupted + fragmented_packages_.erase(it); + + return nullptr; + } + } + + // We have a complete package, so we can reassemble it + auto reassebled_buffer = reassemble_package(it); + + // Remove the package from the map. We don't need it anymore, as it is complete + fragmented_packages_.erase(it); + + // Return the package to the user + error = ecaludp::Error::ErrorCode::OK; + return reassebled_buffer; + } + + std::shared_ptr Reassembly::reassemble_package(const fragmented_package_map_t::const_iterator& it) + { + // Create a mutable buffer that is big enough to hold the entire package + auto reassembled_buffer = largepackage_buffer_pool_.allocate(); + + reassembled_buffer->resize(it->second.first.total_size_bytes_); + + void* current_pos = reassembled_buffer->data(); + + for (const auto& fragment : it->second.second) + { + // Copy the fragment into the reassembled buffer + memcpy(current_pos, fragment->data(), fragment->size()); + current_pos = static_cast(current_pos) + fragment->size(); + } + + // In this case we don't have the header as residue in the raw memory, so we return the entire buffer. + return std::make_shared(reassembled_buffer->data(), reassembled_buffer->size(), reassembled_buffer); + } + + void Reassembly::remove_old_packages(std::chrono::steady_clock::time_point max_age) + { + // Remove all packages that are older than max_age + for (auto it = fragmented_packages_.begin(); it != fragmented_packages_.end();) + { + if (it->second.first.last_access_ < max_age) + { + fragmented_packages_.erase(it++); + } + else + { + ++it; + } + } + } + + } +} diff --git a/ecaludp/src/protocol/reassembly_v5.h b/ecaludp/src/protocol/reassembly_v5.h new file mode 100644 index 0000000..389dafa --- /dev/null +++ b/ecaludp/src/protocol/reassembly_v5.h @@ -0,0 +1,94 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace ecaludp +{ + namespace v5 + { + class Reassembly + { + ////////////////////////////////////////////////////////////////////////////// + // Private types + ////////////////////////////////////////////////////////////////////////////// + private: + using fragmented_package_key = std::pair; + struct fragmented_package_info + { + bool fragment_info_received_ {false}; + uint32_t total_fragments_ {0}; + uint32_t total_size_bytes_ {0}; + unsigned int received_fragments_ {0}; + std::chrono::steady_clock::time_point last_access_ {std::chrono::steady_clock::duration(0)}; + }; + using fragmented_package = std::pair>>; + using fragmented_package_map_t = std::map; + + ////////////////////////////////////////////////////////////////////////////// + // Constructor, Destructor + ////////////////////////////////////////////////////////////////////////////// + public: + Reassembly(); + + ////////////////////////////////////////////////////////////////////////////// + // Receiving, datagram handling & fragment reassembly + ////////////////////////////////////////////////////////////////////////////// + public: + std::shared_ptr handle_datagram (const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error); + + private: + std::shared_ptr handle_datagram_fragmented_message_info(const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error); + std::shared_ptr handle_datagram_fragment (const std::shared_ptr& buffer, const std::shared_ptr& sender_endpoint, ecaludp::Error& error); + std::shared_ptr handle_datagram_non_fragmented_message (const std::shared_ptr& buffer, ecaludp::Error& error); + + std::shared_ptr handle_fragmented_package_if_complete(const fragmented_package_map_t::const_iterator& it, ecaludp::Error& error); + std::shared_ptr reassemble_package (const fragmented_package_map_t::const_iterator& it); + + public: + void remove_old_packages(std::chrono::steady_clock::time_point max_age); + + ////////////////////////////////////////////////////////////////////////////// + // Member variables + ////////////////////////////////////////////////////////////////////////////// + private: + fragmented_package_map_t fragmented_packages_; + + // Buffer pool + struct buffer_pool_lock_policy_ + { + using mutex_type = std::mutex; + using lock_type = std::lock_guard; + }; + recycle::shared_pool largepackage_buffer_pool_; + }; + } +} diff --git a/ecaludp/src/socket.cpp b/ecaludp/src/socket.cpp new file mode 100644 index 0000000..26fae85 --- /dev/null +++ b/ecaludp/src/socket.cpp @@ -0,0 +1,237 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include +#include +#include +#include + +#include + +#include + +#include "ecaludp/error.h" +#include "ecaludp/raw_memory.h" +#include "protocol/datagram_builder_v5.h" +#include "protocol/datagram_description.h" +#include "protocol/header_common.h" + +#include "protocol/reassembly_v5.h" + +#include + +#include +#include +#include +#include +#include + +namespace ecaludp +{ + namespace + { + void async_send_datagram_list_to(asio::ip::udp::socket& socket + , const DatagramList& datagram_list + , DatagramList::const_iterator start_it + , const asio::ip::udp::endpoint& destination + , const std::function& completion_handler) + { + if (start_it == datagram_list.end()) + { + completion_handler(asio::error_code()); + return; + } + + socket.async_send_to(start_it->asio_buffer_list_ + , destination + , [&socket, &datagram_list, start_it, destination, completion_handler](asio::error_code ec, std::size_t /*bytes_transferred*/) + { + if (ec) + { + completion_handler(ec); + return; + } + + async_send_datagram_list_to(socket, datagram_list, start_it + 1, destination, completion_handler); + }); + } + } + + struct buffer_pool_lock_policy_ + { + using mutex_type = std::mutex; + using lock_type = std::lock_guard; + }; + + class recycle_shared_pool : public recycle::shared_pool{}; + + + Socket::Socket(asio::io_service& io_service, std::array magic_header_bytes) + : socket_ (io_service) + , datagram_buffer_pool_(new recycle_shared_pool()) // TODO: make_unique + , reassembly_v5_ (new ecaludp::v5::Reassembly()) + , magic_header_bytes_ (magic_header_bytes) + , max_udp_datagram_size_(1448) + , max_reassembly_age_ (std::chrono::seconds(5)) + {} + + Socket::~Socket() = default; + + void Socket::async_send_to(const std::vector& buffer_sequence + , const asio::ip::udp::endpoint& destination + , const std::function& completion_handler) + { + constexpr int protocol_version = 5; //TODO: make this configurable + + auto datagram_list = std::make_shared(); + + if (protocol_version == 5) + { + *datagram_list = ecaludp::v5::create_datagram_list(buffer_sequence, max_udp_datagram_size_, magic_header_bytes_); + } + else + { + throw std::runtime_error("Protocol version not supported"); + } + + async_send_datagram_list_to(socket_ + , *datagram_list + , datagram_list->begin() + , destination + , [completion_handler, datagram_list](asio::error_code ec) + { + completion_handler(ec); + }); + } + + void Socket::set_max_udp_datagram_size(std::size_t max_udp_datagram_size) + { + max_udp_datagram_size_ = max_udp_datagram_size; + } + + std::size_t Socket::get_max_udp_datagram_size() const + { + return max_udp_datagram_size_; + } + + void Socket::set_max_reassembly_age(std::chrono::steady_clock::duration max_reassembly_age) + { + max_reassembly_age_ = max_reassembly_age; + } + + std::chrono::steady_clock::duration Socket::get_max_reassembly_age() const + { + return max_reassembly_age_; + } + + void Socket::async_receive_from(asio::ip::udp::endpoint& sender_endpoint + , const std::function&, asio::error_code)>& completion_handler) + { + receive_next_datagram_from(sender_endpoint, completion_handler); + } + + + void Socket::receive_next_datagram_from(asio::ip::udp::endpoint& sender_endpoint + , const std::function&, asio::error_code)>& completion_handler) + { + auto datagram_buffer = datagram_buffer_pool_->allocate(); + datagram_buffer->resize(65535, false); // Max UDP datagram size. Overprovisioning is not required here, so we safe some time and memory. + + auto buffer = datagram_buffer_pool_->allocate(); + + buffer->resize(65535); // max datagram size + + auto sender_endpoint_of_this_datagram = std::make_shared(); + + socket_.async_receive_from(asio::buffer(buffer->data(), buffer->size()) + , *sender_endpoint_of_this_datagram + , [this, buffer, completion_handler, sender_endpoint_of_this_datagram, &sender_endpoint](const asio::error_code& ec, std::size_t bytes_received) + { + if (ec) + { + completion_handler(nullptr, ec); + return; + } + + // resize the buffer to the actually received size + buffer->resize(bytes_received); + + // Handle the datagram + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto completed_package = this->handle_datagram(buffer, sender_endpoint_of_this_datagram, error); + + if (completed_package != nullptr) + { + sender_endpoint = *sender_endpoint_of_this_datagram; + completion_handler(completed_package, ec); + } + else + { + // Receive the next datagram + receive_next_datagram_from(sender_endpoint, completion_handler); + } + }); + + } + + std::shared_ptr Socket::handle_datagram(const std::shared_ptr& buffer + , const std::shared_ptr& sender_endpoint + , ecaludp::Error& error) + { + // Clean the reassembly from fragments that are too old + reassembly_v5_->remove_old_packages(std::chrono::steady_clock::now() - max_reassembly_age_); + + // Start to parse the header + + if (buffer->size() < sizeof(ecaludp::HeaderCommon)) // Magic number + version + { + error = ecaludp::Error(ecaludp::Error::MALFORMED_DATAGRAM, "Datagram too small to contain common header (" + std::to_string(buffer->size()) + " bytes)"); + return nullptr; + } + + auto* header = reinterpret_cast(buffer->data()); + + // Check the magic number + if (strncmp(header->magic, magic_header_bytes_.data(), 4) != 0) + { + error = ecaludp::Error(ecaludp::Error::MALFORMED_DATAGRAM, "Wrong magic bytes"); + return nullptr; + } + + std::shared_ptr finished_package; + + // Check the version and invoke the correct handler + if (header->version == 5) + { + finished_package = reassembly_v5_->handle_datagram(buffer, sender_endpoint, error); + } + else if (header->version == 6) + { + error = ecaludp::Error(Error::UNSUPPORTED_PROTOCOL_VERSION, std::to_string(header->version)); + //handle_datagram_v6(buffer); + } + else + { + error = ecaludp::Error(Error::UNSUPPORTED_PROTOCOL_VERSION, std::to_string(header->version)); + } + + if (error) + { + return nullptr; + } + + return finished_package; + } +} diff --git a/ecaludp/version.cmake b/ecaludp/version.cmake new file mode 100644 index 0000000..1ffd0ad --- /dev/null +++ b/ecaludp/version.cmake @@ -0,0 +1,3 @@ +set(ECALUDP_VERSION_MAJOR 0) +set(ECALUDP_VERSION_MINOR 1) +set(ECALUDP_VERSION_PATCH 0) diff --git a/samples/ecaludp_sample/CMakeLists.txt b/samples/ecaludp_sample/CMakeLists.txt new file mode 100644 index 0000000..fc65a09 --- /dev/null +++ b/samples/ecaludp_sample/CMakeLists.txt @@ -0,0 +1,41 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +cmake_minimum_required(VERSION 3.13) + +project(ecaludp_sample) + +find_package(Threads REQUIRED) +find_package(ecaludp REQUIRED) + +set(sources + src/main.cpp +) + +add_executable(${PROJECT_NAME} ${sources}) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + ecaludp::ecaludp + Threads::Threads) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_14) + +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER ecal/udp) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${sources} +) diff --git a/samples/ecaludp_sample/src/main.cpp b/samples/ecaludp_sample/src/main.cpp new file mode 100644 index 0000000..9d97a49 --- /dev/null +++ b/samples/ecaludp_sample/src/main.cpp @@ -0,0 +1,132 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include +#include + +#include + +std::shared_ptr io_context_; + +std::shared_ptr socket_; +std::shared_ptr send_timer_; + +void send_package() +{ + auto douglas_adams_buffer_1 = std::make_shared("In the beginning the Universe was created."); + auto douglas_adams_buffer_2 = std::make_shared(" "); + auto douglas_adams_buffer_3 = std::make_shared("This had made many people very angry and has been widely regarded as a bad move."); + + socket_->async_send_to({ asio::buffer(*douglas_adams_buffer_1), asio::buffer(*douglas_adams_buffer_2), asio::buffer(*douglas_adams_buffer_3) } + , asio::ip::udp::endpoint(asio::ip::address_v4::loopback() + , 14000) + , [douglas_adams_buffer_1, douglas_adams_buffer_2, douglas_adams_buffer_3](asio::error_code ec) + { + if (ec) + { + std::cout << "Error sending: " << ec.message() << std::endl; + return; + } + + send_timer_->expires_from_now(std::chrono::milliseconds(500)); + send_timer_->async_wait([](asio::error_code ec) + { + if (ec) + { + std::cout << "Error waiting: " << ec.message() << std::endl; + return; + } + + send_package(); + }); + }); +} + +void receive_package() +{ + auto sender_endpoint = std::make_shared(); + + socket_->async_receive_from(*sender_endpoint + , [sender_endpoint](const std::shared_ptr& buffer, asio::error_code ec) + { + if (ec) + { + std::cout << "Error receiving: " << ec.message() << std::endl; + return; + } + + std::string received_string(static_cast(buffer->data()), buffer->size()); + std::cout << "Received " << buffer->size() << " bytes from " << sender_endpoint->address().to_string() << ":" << sender_endpoint->port() << ": " << received_string << std::endl; + + + receive_package(); + }); +} + +int main(int argc, char** argv) +{ + std::cout << "Starting...\n"; + + io_context_ = std::make_shared(); + + socket_ = std::make_shared(*io_context_, std::array{'E', 'C', 'A', 'L'}); + + { + asio::error_code ec; + socket_->open(asio::ip::udp::v4(), ec); + + if (ec) + { + std::cout << "Error opening socket: " << ec.message() << std::endl; + return -1; + } + } + + { + asio::error_code ec; + socket_->bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), 14000), ec); + + if (ec) + { + std::cout << "Error binding socket: " << ec.message() << std::endl; + return -1; + } + } + + //{ + // asio::error_code ec; + // socket_->set_option(asio::socket_base::reuse_address(true), ec); + + // if (ec) + // { + // std::cout << "Error setting reuse-socket option: " << ec.message() << std::endl; + // return -1; + // } + //} + + send_timer_ = std::make_shared(*io_context_); + + asio::io_context::work work(*io_context_); + + receive_package(); + send_package(); + + io_context_->run(); + + return 0; +} diff --git a/samples/integration_test/CMakeLists.txt b/samples/integration_test/CMakeLists.txt new file mode 100644 index 0000000..59fab1b --- /dev/null +++ b/samples/integration_test/CMakeLists.txt @@ -0,0 +1,39 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +cmake_minimum_required(VERSION 3.13) + +project(integration_test) + +find_package(Threads REQUIRED) +find_package(ecaludp REQUIRED) + +set(sources + src/main.cpp +) + +add_executable(${PROJECT_NAME} ${sources}) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + Threads::Threads + ecaludp::ecaludp) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_14) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${sources} +) diff --git a/samples/integration_test/src/main.cpp b/samples/integration_test/src/main.cpp new file mode 100644 index 0000000..6a8b0fa --- /dev/null +++ b/samples/integration_test/src/main.cpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include +#include + +#include + +int main() +{ + asio::io_context io_context; + + // Create a socket + ecaludp::Socket socket(io_context, {'E', 'C', 'A', 'L'}); + + // Open the socket + { + asio::error_code ec; + socket.open(asio::ip::udp::v4(), ec); + } + + // Bind the socket + { + asio::error_code ec; + socket.bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), 14000), ec); + } + + auto work = std::make_unique(io_context); + std::thread io_thread([&io_context]() { io_context.run(); }); + + std::shared_ptr sender_endpoint = std::make_shared(); + std::shared_ptr message_to_send = std::make_shared("Hello World!"); + + // Wait for the next message + socket.async_receive_from(*sender_endpoint + , [sender_endpoint, message_to_send](const std::shared_ptr& buffer, asio::error_code ec) + { + }); + + // Send a message + socket.async_send_to({ asio::buffer(*message_to_send) } + , asio::ip::udp::endpoint(asio::ip::address_v4::loopback() + , 14000) + , [message_to_send](asio::error_code ec) + { + }); + + work.reset(); + io_thread.join(); +} diff --git a/tests/ecaludp_private_test/CMakeLists.txt b/tests/ecaludp_private_test/CMakeLists.txt new file mode 100644 index 0000000..d3453eb --- /dev/null +++ b/tests/ecaludp_private_test/CMakeLists.txt @@ -0,0 +1,47 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +project(ecaludp_private_test) + +find_package(Threads REQUIRED) +find_package(GTest REQUIRED) +find_package(ecaludp REQUIRED) + +set(sources + src/fragmentation_v5_test.cpp +) + +add_executable(${PROJECT_NAME} ${sources}) + +# Add private includes of the ecaludp target +target_include_directories(${PROJECT_NAME} + PRIVATE + $ +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + ecaludp + GTest::gtest_main) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_14) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${sources} +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) \ No newline at end of file diff --git a/tests/ecaludp_private_test/src/fragmentation_v5_test.cpp b/tests/ecaludp_private_test/src/fragmentation_v5_test.cpp new file mode 100644 index 0000000..05f286f --- /dev/null +++ b/tests/ecaludp_private_test/src/fragmentation_v5_test.cpp @@ -0,0 +1,774 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include +#include + +#include + +#include +#include +#include + +#include + +// Define your test fixture +class FragmentationV5Test : public ::testing::Test { +protected: + // Set up the test fixture + void SetUp() override + { + // Code to set up the test fixture + } + + // Tear down the test fixture + void TearDown() override + { + // Code to tear down the test fixture + } +}; + +std::shared_ptr to_binary_buffer(const ecaludp::DatagramDescription& datagram_description) +{ + std::shared_ptr buffer = std::make_shared(); + buffer->resize(datagram_description.size()); + + size_t current_pos = 0; + + for (const auto& asio_buffer : datagram_description.asio_buffer_list_) + { + std::memcpy(buffer->data() + current_pos, asio_buffer.data(), asio_buffer.size()); + current_pos += asio_buffer.size(); + } + + return buffer; +} + +// Check "Fragmentation" and "Defragmentation" of a single normal message that is smaller than the MTU, i.e. no fragmentation is needed +TEST_F(FragmentationV5Test, NonFragmentedMessage) +{ + // Create a Hello World string + std::string hello_world = "Hello World!"; + + // Create an asio buffer from the string + asio::const_buffer hello_world_buffer = asio::buffer(hello_world); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 1000 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({hello_world_buffer}, 1000, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 1 entry + ASSERT_EQ(datagram_list.size(), 1); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list.front().size(), hello_world.size() + sizeof(ecaludp::v5::Header)); + + // Copy the datagram list to a binary buffer + auto binary_buffer = to_binary_buffer(datagram_list.front()); + + // Check the header + auto* header = reinterpret_cast(binary_buffer->data()); + ASSERT_EQ(header->version, 5); + ASSERT_EQ(le32toh(static_cast(header->type)), 3u /* = ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message */); + ASSERT_EQ(le32toh(header->id), -1); + ASSERT_EQ(le32toh(header->num), 1); + ASSERT_EQ(le32toh(header->len), hello_world.size()); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassebly the datagram + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), hello_world.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), hello_world.data(), hello_world.size()), 0); +} + +// Check the fragmentation and defragmentation of a single message that is larger than the MTU +TEST_F(FragmentationV5Test, FragmentedMessage) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 100); // This is the entire full datagram + ASSERT_EQ(datagram_list[2].size(), sizeof(ecaludp::v5::Header) + message_size - (100 - sizeof(ecaludp::v5::Header))); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // Check the header of the fragment info + auto* header_1 = reinterpret_cast(binary_buffer_1->data()); + auto common_id = le32toh(header_1->id); + ASSERT_EQ(header_1->version, 5); + ASSERT_EQ(le32toh(static_cast(header_1->type)), 1u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment_info */); + ASSERT_EQ(le32toh(header_1->num), 2); + ASSERT_EQ(le32toh(header_1->id), common_id); + ASSERT_EQ(le32toh(header_1->len), message_size); + + // Check the header of the first fragment + auto* header_2 = reinterpret_cast(binary_buffer_2->data()); + ASSERT_EQ(header_2->version, 5); + ASSERT_EQ(le32toh(static_cast(header_2->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_2->id), common_id); + ASSERT_EQ(le32toh(header_2->num), 0); + ASSERT_EQ(le32toh(header_2->len), 100 - sizeof(ecaludp::v5::Header)); + + // Check the header of the last fragment + auto* header_3 = reinterpret_cast(binary_buffer_3->data()); + ASSERT_EQ(header_3->version, 5); + ASSERT_EQ(le32toh(static_cast(header_3->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_3->id), common_id); + ASSERT_EQ(le32toh(header_3->num), 1); + ASSERT_EQ(le32toh(header_3->len), message_size - (100 - sizeof(ecaludp::v5::Header))); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), message_size); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), message_to_send.data(), message_size), 0); + } +} + +// Check the defragmentation of a long message that is larger than the MTU and arrives out of order +TEST_F(FragmentationV5Test, OutOfOrderFragments) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 100); // This is the entire full datagram + ASSERT_EQ(datagram_list[2].size(), sizeof(ecaludp::v5::Header) + message_size - (100 - sizeof(ecaludp::v5::Header))); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), message_size); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), message_to_send.data(), message_size), 0); + } +} + +// Check the handling of a 1-fragment-message (i.e. a message that is small enough to fit into a single datagram, but is still fragmented) +TEST_F(FragmentationV5Test, SingleFragmentFragmentation) +{ + // Create a Hello World string + std::string hello_world = "Hello World!"; + + // Create an asio buffer from the string + asio::const_buffer hello_world_buffer = asio::buffer(hello_world); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_fragmented_datagram_list({hello_world_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 2 entries: 1 fragment info and 1 fragment + ASSERT_EQ(datagram_list.size(), 2); + + // Check the size of the datagrams + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), sizeof(ecaludp::v5::Header) + hello_world.size()); // This is the fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + + // Check the header of the fragment info + auto* header_1 = reinterpret_cast(binary_buffer_1->data()); + auto common_id = le32toh(header_1->id); + ASSERT_EQ(header_1->version, 5); + ASSERT_EQ(le32toh(static_cast(header_1->type)), 1u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment_info */); + ASSERT_EQ(le32toh(header_1->num), 1); + ASSERT_EQ(le32toh(header_1->id), common_id); + ASSERT_EQ(le32toh(header_1->len), hello_world.size()); + + // Check the header of the first fragment + auto* header_2 = reinterpret_cast(binary_buffer_2->data()); + ASSERT_EQ(header_2->version, 5); + ASSERT_EQ(le32toh(static_cast(header_2->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_2->id), common_id); + ASSERT_EQ(le32toh(header_2->num), 0); + ASSERT_EQ(le32toh(header_2->len), hello_world.size()); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), hello_world.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), hello_world.data(), hello_world.size()), 0); + } +} + +// Check "Fragmentation" and "Defragmentation" of a 0-byte message +TEST_F(FragmentationV5Test, ZeroByteMessage) +{ + // Create a 0-byte string + std::string zero_byte_string; + + // Create an asio buffer from the string + asio::const_buffer zero_byte_buffer = asio::buffer(zero_byte_string); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 1000 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({zero_byte_buffer}, 1000, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 1 entry + ASSERT_EQ(datagram_list.size(), 1); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list.front().size(), sizeof(ecaludp::v5::Header)); + + // Copy the datagram list to a binary buffer + auto binary_buffer = to_binary_buffer(datagram_list.front()); + + // Check the header + auto* header = reinterpret_cast(binary_buffer->data()); + ASSERT_EQ(header->version, 5); + ASSERT_EQ(le32toh(static_cast(header->type)), 3u /* = ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message */); + ASSERT_EQ(le32toh(header->id), -1); + ASSERT_EQ(le32toh(header->num), 1); + ASSERT_EQ(le32toh(header->len), 0); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassebly the datagram + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), 0); +} + +// Check Fragmentation and defragmentation of a muli-buffer-message +TEST_F(FragmentationV5Test, MultiBufferFragmentation) +{ + auto message_to_send_1 = std::make_shared("In the beginning the Universe was created."); + auto message_to_send_2 = std::make_shared(" "); + auto message_to_send_3 = std::make_shared("This had made many people very angry and has been widely regarded as a bad move."); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer_1 = asio::buffer(*message_to_send_1); + asio::const_buffer message_to_send_buffer_2 = asio::buffer(*message_to_send_2); + asio::const_buffer message_to_send_buffer_3 = asio::buffer(*message_to_send_3); + + // create the entire message for later + std::string entire_message = *message_to_send_1 + *message_to_send_2 + *message_to_send_3; + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer_1, message_to_send_buffer_2, message_to_send_buffer_3}, 70, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 4 entries: 1 fragment info and 3 fragments + ASSERT_EQ(datagram_list.size(), 4); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 70); // This is the first fragment + ASSERT_EQ(datagram_list[2].size(), 70); // This is the second fragment + + int payload_per_datagram = 70 - sizeof(ecaludp::v5::Header); + ASSERT_EQ(datagram_list[3].size(), sizeof(ecaludp::v5::Header) + entire_message.size() - (2 * payload_per_datagram)); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + auto binary_buffer_4 = to_binary_buffer(datagram_list[3]); + + // Create the reassembly + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the fourth datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_4, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), entire_message.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), entire_message.data(), entire_message.size()), 0); + } +} + +TEST_F(FragmentationV5Test, Cleanup) +{ + // Create 2 messages that are the same size + auto message_1 = std::make_shared("In the beginning the Universe was created."); + auto message_2 = std::make_shared("Hello World!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + // Create an asio buffer from the strings + asio::const_buffer message_1_buffer = asio::buffer(*message_1); + asio::const_buffer message_2_buffer = asio::buffer(*message_2); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 40 bytes (including header) + auto datagram_list_1 = ecaludp::v5::create_datagram_list({message_1_buffer}, 60, {'E', 'C', 'A', 'L'}); + auto datagram_list_2 = ecaludp::v5::create_datagram_list({message_2_buffer}, 60, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list_1.size(), 3); + ASSERT_EQ(datagram_list_2.size(), 3); + + // Parse the datagram lists to binary buffers + auto binary_buffer_1_1 = to_binary_buffer(datagram_list_1[0]); + auto binary_buffer_1_2 = to_binary_buffer(datagram_list_1[1]); + auto binary_buffer_1_3 = to_binary_buffer(datagram_list_1[2]); + + auto binary_buffer_2_1 = to_binary_buffer(datagram_list_2[0]); + auto binary_buffer_2_2 = to_binary_buffer(datagram_list_2[1]); + auto binary_buffer_2_3 = to_binary_buffer(datagram_list_2[2]); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram of the first message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram of the first message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_1 = reassembly.handle_datagram(binary_buffer_1_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_1, nullptr); + } + + // Safe the current time + auto current_time = std::chrono::steady_clock::now(); + + // sleep 1 ms + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + // Reassemble the first datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_2, nullptr); + } + + // Cleanup old packages (this should hit the first message only) + reassembly.remove_old_packages(current_time); + + // Reassemble the third datagram of the first message. This should fail, as the first message should have been removed + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_1 = reassembly.handle_datagram(binary_buffer_1_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as the first fragments should have been removed + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_1, nullptr); + } + + // Reassemble the third datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2_3, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message_2, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message_2->size(), message_2->size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message_2->data(), message_2->data(), message_2->size()), 0); + } +} + +TEST_F(FragmentationV5Test, FaultyFragmentedMessages) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // Convert to binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // Create the reassembly + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Add some way too small fake datagram to the reassembly. This fails, as the datagram cannot even fit a header + { + auto fake_datagram = std::make_shared(8); + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(fake_datagram, sender_endpoint, error); + + // The reassembly must have failed, as the datagram is too small + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram again (-> duplicate datagram) + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have failed, as the message is a duplicate + ASSERT_EQ(error, ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy the second datagram to a new buffer and change the size information to something large, so the datagram doesn't even contain enough data + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->len = htole32(1000); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy the second datagram to a new buffer and modify the type to something invalid + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->type = static_cast(1000); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Actually add fragment 2 + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_2, nullptr); + } + + // Add fragment 2 again, this time it is a duplicate datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is a duplicate + ASSERT_EQ(error, ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM); + ASSERT_EQ(message_2, nullptr); + } + + // Copy fragment 3, but change the num to fragment 4 that DOES NOT EXIST (-> i.e. num = 2). + { + auto faulty_binary_buffer_3 = std::make_shared(*binary_buffer_3); + auto* header = reinterpret_cast(faulty_binary_buffer_3->data()); + header->num = htole32(2); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_3, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty (we know that it does not fit in the list) + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy fragment 2, but change the num to fragment 3 (-> i.e. num = 1). This will cause the entire message to corrupt. + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->num = htole32(1); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_REASSEMBLED_MESSAGE); + ASSERT_EQ(message, nullptr); + } + + // Add the actual fragment 3 now. Unfortunatelly, the last faulty fragment 2 caused the reassembly to drop the message, so this will not return the reassebled message, either + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly succeeds, but the message is nullptr, as the message was dropped as being corrupt, before. + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } +} + +// TODO: Test adding faulty datagrams and duplicated datagrams to the reassembly +// TODO: Test adding messages from more than 1 sender to the reassembly + +// Entry point for running the tests +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/ecaludp_test/CMakeLists.txt b/tests/ecaludp_test/CMakeLists.txt new file mode 100644 index 0000000..729f3a0 --- /dev/null +++ b/tests/ecaludp_test/CMakeLists.txt @@ -0,0 +1,42 @@ +################################################################################ +# Copyright (c) 2024 Continental Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +project(ecal_udp_test) + +find_package(Threads REQUIRED) +find_package(GTest REQUIRED) +find_package(ecaludp REQUIRED) + +set(sources + src/atomic_signalable.h + src/ecaludp_socket_test.cpp +) + +add_executable(${PROJECT_NAME} ${sources}) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + ecaludp + GTest::gtest_main) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_14) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${sources} +) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) \ No newline at end of file diff --git a/tests/ecaludp_test/src/atomic_signalable.h b/tests/ecaludp_test/src/atomic_signalable.h new file mode 100644 index 0000000..237a59d --- /dev/null +++ b/tests/ecaludp_test/src/atomic_signalable.h @@ -0,0 +1,205 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include + +template +class atomic_signalable +{ +public: + atomic_signalable(T initial_value) : value(initial_value) {} + + atomic_signalable& operator=(const T new_value) + { + std::lock_guard lock(mutex); + value = new_value; + cv.notify_all(); + return *this; + } + + T operator++() + { + std::lock_guard lock(mutex); + T newValue = ++value; + cv.notify_all(); + return newValue; + } + + T operator++(T) + { + std::lock_guard lock(mutex); + T oldValue = value++; + cv.notify_all(); + return oldValue; + } + + T operator--() + { + std::lock_guard lock(mutex); + T newValue = --value; + cv.notify_all(); + return newValue; + } + + T operator--(T) + { + std::lock_guard lock(mutex); + T oldValue = value--; + cv.notify_all(); + return oldValue; + } + + T operator+=(const T& other) + { + std::lock_guard lock(mutex); + value += other; + cv.notify_all(); + return value; + } + + T operator-=(const T& other) + { + std::lock_guard lock(mutex); + value -= other; + cv.notify_all(); + return value; + } + + T operator*=(const T& other) + { + std::lock_guard lock(mutex); + value *= other; + cv.notify_all(); + return value; + } + + T operator/=(const T& other) + { + std::lock_guard lock(mutex); + value /= other; + cv.notify_all(); + return value; + } + + T operator%=(const T& other) + { + std::lock_guard lock(mutex); + value %= other; + cv.notify_all(); + return value; + } + + template + bool wait_for(Predicate predicate, std::chrono::milliseconds timeout) + { + std::unique_lock lock(mutex); + return cv.wait_for(lock, timeout, [&]() { return predicate(value); }); + } + + T get() const + { + std::lock_guard lock(mutex); + return value; + } + + bool operator==(T other) const + { + std::lock_guard lock(mutex); + return value == other; + } + + bool operator==(const atomic_signalable& other) const + { + std::lock_guard lock_this(mutex); + std::lock_guard lock_other(other.mutex); + return value == other.value; + } + + bool operator!=(T other) const + { + std::lock_guard lock(mutex); + return value != other; + } + + bool operator<(T other) const + { + std::lock_guard lock(mutex); + return value < other; + } + + bool operator<=(T other) const + { + std::lock_guard lock(mutex); + return value <= other; + } + + bool operator>(T other) const + { + std::lock_guard lock(mutex); + return value > other; + } + + bool operator>=(T other) const + { + std::lock_guard lock(mutex); + return value >= other; + } + +private: + T value; + std::condition_variable cv; + mutable std::mutex mutex; +}; + + +template +bool operator==(const T& other, const atomic_signalable& atomic) +{ + return atomic == other; +} + +template +bool operator!=(const T& other, const atomic_signalable& atomic) +{ + return atomic != other; +} + +template +bool operator<(const T& other, const atomic_signalable& atomic) +{ + return atomic > other; +} + +template +bool operator<=(const T& other, const atomic_signalable& atomic) +{ + return atomic >= other; +} + +template +bool operator>(const T& other, const atomic_signalable& atomic) +{ + return atomic < other; +} + +template +bool operator>=(const T& other, const atomic_signalable& atomic) +{ + return atomic <= other; +} diff --git a/tests/ecaludp_test/src/ecaludp_socket_test.cpp b/tests/ecaludp_test/src/ecaludp_socket_test.cpp new file mode 100644 index 0000000..4f156c2 --- /dev/null +++ b/tests/ecaludp_test/src/ecaludp_socket_test.cpp @@ -0,0 +1,158 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include + +#include + +#include "atomic_signalable.h" + +TEST(EcalUdpSocket, HelloWorldMessage) +{ + atomic_signalable received_messages(0); + + asio::io_context io_context; + + // Create a socket + ecaludp::Socket socket(io_context, {'E', 'C', 'A', 'L'}); + + // Open the socket + { + asio::error_code ec; + socket.open(asio::ip::udp::v4(), ec); + ASSERT_EQ(ec, asio::error_code()); + } + + // Bind the socket + { + asio::error_code ec; + socket.bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), 14000), ec); + ASSERT_EQ(ec, asio::error_code()); + } + + auto work = std::make_unique(io_context); + std::thread io_thread([&io_context]() { io_context.run(); }); + + std::shared_ptr sender_endpoint = std::make_shared(); + std::shared_ptr message_to_send = std::make_shared("Hello World!"); + + // Wait for the next message + socket.async_receive_from(*sender_endpoint + , [sender_endpoint, &received_messages, message_to_send](const std::shared_ptr& buffer, asio::error_code ec) + { + // No error + if (ec) + { + FAIL(); + } + + // compare the messages + std::string received_string(static_cast(buffer->data()), buffer->size()); + ASSERT_EQ(received_string, *message_to_send); + + // increment + received_messages++; + }); + + // Send a message + socket.async_send_to({ asio::buffer(*message_to_send) } + , asio::ip::udp::endpoint(asio::ip::address_v4::loopback() + , 14000) + , [message_to_send](asio::error_code ec) + { + // No error + ASSERT_EQ(ec, asio::error_code()); + }); + + // Wait for the message to be received + received_messages.wait_for([](int received_messages) { return received_messages == 1; }, std::chrono::milliseconds(100)); + + ASSERT_EQ(received_messages, 1); + + work.reset(); + io_thread.join(); +} + +TEST(EcalUdpSocket, BigMessage) +{ + atomic_signalable received_messages(0); + + asio::io_context io_context; + + // Create a socket + ecaludp::Socket socket(io_context, {'E', 'C', 'A', 'L'}); + + // Open the socket + { + asio::error_code ec; + socket.open(asio::ip::udp::v4(), ec); + ASSERT_EQ(ec, asio::error_code()); + } + + // Bind the socket + { + asio::error_code ec; + socket.bind(asio::ip::udp::endpoint(asio::ip::address_v4::loopback(), 14000), ec); + ASSERT_EQ(ec, asio::error_code()); + } + + auto work = std::make_unique(io_context); + std::thread io_thread([&io_context]() { io_context.run(); }); + + std::shared_ptr sender_endpoint = std::make_shared(); + std::shared_ptr message_to_send = std::make_shared(1024 * 1024, 'a'); + + // Fill the message with random characters + std::generate(message_to_send->begin(), message_to_send->end(), []() { return static_cast(std::rand()); }); + + // Wait for the next message + socket.async_receive_from(*sender_endpoint + , [sender_endpoint, &received_messages, message_to_send](const std::shared_ptr& buffer, asio::error_code ec) + { + // No error + if (ec) + { + FAIL(); + } + + // compare the messages + std::string received_string(static_cast(buffer->data()), buffer->size()); + ASSERT_EQ(received_string, *message_to_send); + + // increment + received_messages++; + }); + + // Send a message + socket.async_send_to({ asio::buffer(*message_to_send) } + , asio::ip::udp::endpoint(asio::ip::address_v4::loopback() + , 14000) + , [message_to_send](asio::error_code ec) + { + // No error + ASSERT_EQ(ec, asio::error_code()); + }); + + // Wait for the message to be received + received_messages.wait_for([](int received_messages) { return received_messages == 1; }, std::chrono::milliseconds(1000)); + + ASSERT_EQ(received_messages, 1); + + work.reset(); + io_thread.join(); +} \ No newline at end of file diff --git a/tests/ecaludp_test/src/fragmentation_v5_test.cpp b/tests/ecaludp_test/src/fragmentation_v5_test.cpp new file mode 100644 index 0000000..05f286f --- /dev/null +++ b/tests/ecaludp_test/src/fragmentation_v5_test.cpp @@ -0,0 +1,774 @@ +/******************************************************************************** + * Copyright (c) 2024 Continental Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include + +#include +#include + +#include + +#include +#include +#include + +#include + +// Define your test fixture +class FragmentationV5Test : public ::testing::Test { +protected: + // Set up the test fixture + void SetUp() override + { + // Code to set up the test fixture + } + + // Tear down the test fixture + void TearDown() override + { + // Code to tear down the test fixture + } +}; + +std::shared_ptr to_binary_buffer(const ecaludp::DatagramDescription& datagram_description) +{ + std::shared_ptr buffer = std::make_shared(); + buffer->resize(datagram_description.size()); + + size_t current_pos = 0; + + for (const auto& asio_buffer : datagram_description.asio_buffer_list_) + { + std::memcpy(buffer->data() + current_pos, asio_buffer.data(), asio_buffer.size()); + current_pos += asio_buffer.size(); + } + + return buffer; +} + +// Check "Fragmentation" and "Defragmentation" of a single normal message that is smaller than the MTU, i.e. no fragmentation is needed +TEST_F(FragmentationV5Test, NonFragmentedMessage) +{ + // Create a Hello World string + std::string hello_world = "Hello World!"; + + // Create an asio buffer from the string + asio::const_buffer hello_world_buffer = asio::buffer(hello_world); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 1000 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({hello_world_buffer}, 1000, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 1 entry + ASSERT_EQ(datagram_list.size(), 1); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list.front().size(), hello_world.size() + sizeof(ecaludp::v5::Header)); + + // Copy the datagram list to a binary buffer + auto binary_buffer = to_binary_buffer(datagram_list.front()); + + // Check the header + auto* header = reinterpret_cast(binary_buffer->data()); + ASSERT_EQ(header->version, 5); + ASSERT_EQ(le32toh(static_cast(header->type)), 3u /* = ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message */); + ASSERT_EQ(le32toh(header->id), -1); + ASSERT_EQ(le32toh(header->num), 1); + ASSERT_EQ(le32toh(header->len), hello_world.size()); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassebly the datagram + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), hello_world.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), hello_world.data(), hello_world.size()), 0); +} + +// Check the fragmentation and defragmentation of a single message that is larger than the MTU +TEST_F(FragmentationV5Test, FragmentedMessage) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 100); // This is the entire full datagram + ASSERT_EQ(datagram_list[2].size(), sizeof(ecaludp::v5::Header) + message_size - (100 - sizeof(ecaludp::v5::Header))); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // Check the header of the fragment info + auto* header_1 = reinterpret_cast(binary_buffer_1->data()); + auto common_id = le32toh(header_1->id); + ASSERT_EQ(header_1->version, 5); + ASSERT_EQ(le32toh(static_cast(header_1->type)), 1u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment_info */); + ASSERT_EQ(le32toh(header_1->num), 2); + ASSERT_EQ(le32toh(header_1->id), common_id); + ASSERT_EQ(le32toh(header_1->len), message_size); + + // Check the header of the first fragment + auto* header_2 = reinterpret_cast(binary_buffer_2->data()); + ASSERT_EQ(header_2->version, 5); + ASSERT_EQ(le32toh(static_cast(header_2->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_2->id), common_id); + ASSERT_EQ(le32toh(header_2->num), 0); + ASSERT_EQ(le32toh(header_2->len), 100 - sizeof(ecaludp::v5::Header)); + + // Check the header of the last fragment + auto* header_3 = reinterpret_cast(binary_buffer_3->data()); + ASSERT_EQ(header_3->version, 5); + ASSERT_EQ(le32toh(static_cast(header_3->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_3->id), common_id); + ASSERT_EQ(le32toh(header_3->num), 1); + ASSERT_EQ(le32toh(header_3->len), message_size - (100 - sizeof(ecaludp::v5::Header))); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), message_size); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), message_to_send.data(), message_size), 0); + } +} + +// Check the defragmentation of a long message that is larger than the MTU and arrives out of order +TEST_F(FragmentationV5Test, OutOfOrderFragments) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 100); // This is the entire full datagram + ASSERT_EQ(datagram_list[2].size(), sizeof(ecaludp::v5::Header) + message_size - (100 - sizeof(ecaludp::v5::Header))); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), message_size); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), message_to_send.data(), message_size), 0); + } +} + +// Check the handling of a 1-fragment-message (i.e. a message that is small enough to fit into a single datagram, but is still fragmented) +TEST_F(FragmentationV5Test, SingleFragmentFragmentation) +{ + // Create a Hello World string + std::string hello_world = "Hello World!"; + + // Create an asio buffer from the string + asio::const_buffer hello_world_buffer = asio::buffer(hello_world); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_fragmented_datagram_list({hello_world_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 2 entries: 1 fragment info and 1 fragment + ASSERT_EQ(datagram_list.size(), 2); + + // Check the size of the datagrams + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), sizeof(ecaludp::v5::Header) + hello_world.size()); // This is the fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + + // Check the header of the fragment info + auto* header_1 = reinterpret_cast(binary_buffer_1->data()); + auto common_id = le32toh(header_1->id); + ASSERT_EQ(header_1->version, 5); + ASSERT_EQ(le32toh(static_cast(header_1->type)), 1u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment_info */); + ASSERT_EQ(le32toh(header_1->num), 1); + ASSERT_EQ(le32toh(header_1->id), common_id); + ASSERT_EQ(le32toh(header_1->len), hello_world.size()); + + // Check the header of the first fragment + auto* header_2 = reinterpret_cast(binary_buffer_2->data()); + ASSERT_EQ(header_2->version, 5); + ASSERT_EQ(le32toh(static_cast(header_2->type)), 2u /* = ecaludp::v5::message_type_uint32t::msg_type_fragment */); + ASSERT_EQ(le32toh(header_2->id), common_id); + ASSERT_EQ(le32toh(header_2->num), 0); + ASSERT_EQ(le32toh(header_2->len), hello_world.size()); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), hello_world.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), hello_world.data(), hello_world.size()), 0); + } +} + +// Check "Fragmentation" and "Defragmentation" of a 0-byte message +TEST_F(FragmentationV5Test, ZeroByteMessage) +{ + // Create a 0-byte string + std::string zero_byte_string; + + // Create an asio buffer from the string + asio::const_buffer zero_byte_buffer = asio::buffer(zero_byte_string); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 1000 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({zero_byte_buffer}, 1000, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 1 entry + ASSERT_EQ(datagram_list.size(), 1); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list.front().size(), sizeof(ecaludp::v5::Header)); + + // Copy the datagram list to a binary buffer + auto binary_buffer = to_binary_buffer(datagram_list.front()); + + // Check the header + auto* header = reinterpret_cast(binary_buffer->data()); + ASSERT_EQ(header->version, 5); + ASSERT_EQ(le32toh(static_cast(header->type)), 3u /* = ecaludp::v5::message_type_uint32t::msg_type_non_fragmented_message */); + ASSERT_EQ(le32toh(header->id), -1); + ASSERT_EQ(le32toh(header->num), 1); + ASSERT_EQ(le32toh(header->len), 0); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassebly the datagram + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), 0); +} + +// Check Fragmentation and defragmentation of a muli-buffer-message +TEST_F(FragmentationV5Test, MultiBufferFragmentation) +{ + auto message_to_send_1 = std::make_shared("In the beginning the Universe was created."); + auto message_to_send_2 = std::make_shared(" "); + auto message_to_send_3 = std::make_shared("This had made many people very angry and has been widely regarded as a bad move."); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer_1 = asio::buffer(*message_to_send_1); + asio::const_buffer message_to_send_buffer_2 = asio::buffer(*message_to_send_2); + asio::const_buffer message_to_send_buffer_3 = asio::buffer(*message_to_send_3); + + // create the entire message for later + std::string entire_message = *message_to_send_1 + *message_to_send_2 + *message_to_send_3; + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer_1, message_to_send_buffer_2, message_to_send_buffer_3}, 70, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 4 entries: 1 fragment info and 3 fragments + ASSERT_EQ(datagram_list.size(), 4); + + // The size of the datagram list is the size of the buffer plus the size of the header + ASSERT_EQ(datagram_list[0].size(), sizeof(ecaludp::v5::Header)); // This is the fragment info + ASSERT_EQ(datagram_list[1].size(), 70); // This is the first fragment + ASSERT_EQ(datagram_list[2].size(), 70); // This is the second fragment + + int payload_per_datagram = 70 - sizeof(ecaludp::v5::Header); + ASSERT_EQ(datagram_list[3].size(), sizeof(ecaludp::v5::Header) + entire_message.size() - (2 * payload_per_datagram)); // This is the last fragment + + // Copy the datagram list to a couple of binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + auto binary_buffer_4 = to_binary_buffer(datagram_list[3]); + + // Create the reassembly + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the third datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the fourth datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_4, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message->size(), entire_message.size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message->data(), entire_message.data(), entire_message.size()), 0); + } +} + +TEST_F(FragmentationV5Test, Cleanup) +{ + // Create 2 messages that are the same size + auto message_1 = std::make_shared("In the beginning the Universe was created."); + auto message_2 = std::make_shared("Hello World!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + // Create an asio buffer from the strings + asio::const_buffer message_1_buffer = asio::buffer(*message_1); + asio::const_buffer message_2_buffer = asio::buffer(*message_2); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 40 bytes (including header) + auto datagram_list_1 = ecaludp::v5::create_datagram_list({message_1_buffer}, 60, {'E', 'C', 'A', 'L'}); + auto datagram_list_2 = ecaludp::v5::create_datagram_list({message_2_buffer}, 60, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list_1.size(), 3); + ASSERT_EQ(datagram_list_2.size(), 3); + + // Parse the datagram lists to binary buffers + auto binary_buffer_1_1 = to_binary_buffer(datagram_list_1[0]); + auto binary_buffer_1_2 = to_binary_buffer(datagram_list_1[1]); + auto binary_buffer_1_3 = to_binary_buffer(datagram_list_1[2]); + + auto binary_buffer_2_1 = to_binary_buffer(datagram_list_2[0]); + auto binary_buffer_2_2 = to_binary_buffer(datagram_list_2[1]); + auto binary_buffer_2_3 = to_binary_buffer(datagram_list_2[2]); + + // Create a reassembly object + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Reassemble the first datagram of the first message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram of the first message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_1 = reassembly.handle_datagram(binary_buffer_1_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_1, nullptr); + } + + // Safe the current time + auto current_time = std::chrono::steady_clock::now(); + + // sleep 1 ms + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + + // Reassemble the first datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_2_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the second datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_2, nullptr); + } + + // Cleanup old packages (this should hit the first message only) + reassembly.remove_old_packages(current_time); + + // Reassemble the third datagram of the first message. This should fail, as the first message should have been removed + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_1 = reassembly.handle_datagram(binary_buffer_1_3, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as the first fragments should have been removed + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_1, nullptr); + } + + // Reassemble the third datagram of the second message + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2_3, sender_endpoint, error); + + // The reassembly must have succeeded + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + + // The message must not be nullptr + ASSERT_NE(message_2, nullptr); + + // The message must have the same size as the original buffer + ASSERT_EQ(message_2->size(), message_2->size()); + + // The message must contain the same data as the original buffer + ASSERT_EQ(std::memcmp(message_2->data(), message_2->data(), message_2->size()), 0); + } +} + +TEST_F(FragmentationV5Test, FaultyFragmentedMessages) +{ + // Create a longer string + auto message_to_send = std::string("In the beginning the Universe was created. This had made many people very angry and has been widely regarded as a bad move."); + const size_t message_size = message_to_send.size(); + + // Create an asio buffer from the string + asio::const_buffer message_to_send_buffer = asio::buffer(message_to_send); + + // Let the datagram builder create fragments for the buffer with a max datagram size of 100 bytes (including header) + auto datagram_list = ecaludp::v5::create_datagram_list({message_to_send_buffer}, 100, {'E', 'C', 'A', 'L'}); + + // The datagram list must have exactly 3 entries: 1 fragment info and 2 fragments + ASSERT_EQ(datagram_list.size(), 3); + + // Convert to binary buffers + auto binary_buffer_1 = to_binary_buffer(datagram_list[0]); + auto binary_buffer_2 = to_binary_buffer(datagram_list[1]); + auto binary_buffer_3 = to_binary_buffer(datagram_list[2]); + + // Create the reassembly + ecaludp::v5::Reassembly reassembly; + + // Create a fake sender endpoint as shared_ptr + auto sender_endpoint = std::make_shared(); + sender_endpoint->address(asio::ip::address::from_string("127.0.0.1")); + sender_endpoint->port(1234); + + // Add some way too small fake datagram to the reassembly. This fails, as the datagram cannot even fit a header + { + auto fake_datagram = std::make_shared(8); + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(fake_datagram, sender_endpoint, error); + + // The reassembly must have failed, as the datagram is too small + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } + + // Reassemble the first datagram again (-> duplicate datagram) + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_1, sender_endpoint, error); + + // The reassembly must have failed, as the message is a duplicate + ASSERT_EQ(error, ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy the second datagram to a new buffer and change the size information to something large, so the datagram doesn't even contain enough data + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->len = htole32(1000); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy the second datagram to a new buffer and modify the type to something invalid + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->type = static_cast(1000); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Actually add fragment 2 + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have succeeded, but the message must be nullptr, as it is not yet complete + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message_2, nullptr); + } + + // Add fragment 2 again, this time it is a duplicate datagram + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message_2 = reassembly.handle_datagram(binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is a duplicate + ASSERT_EQ(error, ecaludp::Error::ErrorCode::DUPLICATE_DATAGRAM); + ASSERT_EQ(message_2, nullptr); + } + + // Copy fragment 3, but change the num to fragment 4 that DOES NOT EXIST (-> i.e. num = 2). + { + auto faulty_binary_buffer_3 = std::make_shared(*binary_buffer_3); + auto* header = reinterpret_cast(faulty_binary_buffer_3->data()); + header->num = htole32(2); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_3, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty (we know that it does not fit in the list) + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_DATAGRAM); + ASSERT_EQ(message, nullptr); + } + + // Copy fragment 2, but change the num to fragment 3 (-> i.e. num = 1). This will cause the entire message to corrupt. + { + auto faulty_binary_buffer_2 = std::make_shared(*binary_buffer_2); + auto* header = reinterpret_cast(faulty_binary_buffer_2->data()); + header->num = htole32(1); + + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(faulty_binary_buffer_2, sender_endpoint, error); + + // The reassembly must have failed, as the message is faulty + ASSERT_EQ(error, ecaludp::Error::ErrorCode::MALFORMED_REASSEMBLED_MESSAGE); + ASSERT_EQ(message, nullptr); + } + + // Add the actual fragment 3 now. Unfortunatelly, the last faulty fragment 2 caused the reassembly to drop the message, so this will not return the reassebled message, either + { + ecaludp::Error error = ecaludp::Error::ErrorCode::GENERIC_ERROR; + auto message = reassembly.handle_datagram(binary_buffer_3, sender_endpoint, error); + + // The reassembly succeeds, but the message is nullptr, as the message was dropped as being corrupt, before. + ASSERT_EQ(error, ecaludp::Error::ErrorCode::OK); + ASSERT_EQ(message, nullptr); + } +} + +// TODO: Test adding faulty datagrams and duplicated datagrams to the reassembly +// TODO: Test adding messages from more than 1 sender to the reassembly + +// Entry point for running the tests +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/thirdparty/asio b/thirdparty/asio new file mode 160000 index 0000000..7609450 --- /dev/null +++ b/thirdparty/asio @@ -0,0 +1 @@ +Subproject commit 7609450f71434bdc9fbd9491a9505b423c2a8496 diff --git a/thirdparty/asio-module/Findasio.cmake b/thirdparty/asio-module/Findasio.cmake new file mode 100644 index 0000000..e2e12de --- /dev/null +++ b/thirdparty/asio-module/Findasio.cmake @@ -0,0 +1,30 @@ +find_path(asio_INCLUDE_DIR + NAMES asio.hpp + HINTS + "${CMAKE_CURRENT_LIST_DIR}/../asio/asio/include" + NO_DEFAULT_PATH + NO_CMAKE_FIND_ROOT_PATH +) + +if(asio_INCLUDE_DIR-NOTFOUND) + message(FATAL_ERROR "Could not find asio library") + set(asio_FOUND FALSE) +else() + set(asio_FOUND TRUE) + set(ASIO_INCLUDE_DIR ${asio_INCLUDE_DIR}) +endif() + +if(asio_FOUND) + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(asio + REQUIRED_VARS asio_INCLUDE_DIR) + + if(NOT TARGET asio::asio) + set(asio_INCLUDE_DIRS ${asio_INCLUDE_DIR}) + add_library(asio::asio INTERFACE IMPORTED) + set_target_properties(asio::asio PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${asio_INCLUDE_DIR} + INTERFACE_COMPILE_DEFINITIONS ASIO_STANDALONE) + mark_as_advanced(asio_INCLUDE_DIR) + endif() +endif() diff --git a/thirdparty/build-asio.cmake b/thirdparty/build-asio.cmake new file mode 100644 index 0000000..823e44b --- /dev/null +++ b/thirdparty/build-asio.cmake @@ -0,0 +1,2 @@ +# Prepend asio-module/Fineasio.cmake to the module path +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/asio-module") diff --git a/thirdparty/build-gtest.cmake b/thirdparty/build-gtest.cmake new file mode 100644 index 0000000..80a7e64 --- /dev/null +++ b/thirdparty/build-gtest.cmake @@ -0,0 +1,19 @@ +# Googletest automatically forces MT instead of MD if we do not set this option. +if(MSVC) + set(gtest_force_shared_crt ON CACHE BOOL "My option" FORCE) + set(BUILD_GMOCK OFF CACHE BOOL "My option" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "My option" FORCE) +endif() + +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/googletest" EXCLUDE_FROM_ALL) + +if(NOT TARGET GTest::gtest) + add_library(GTest::gtest ALIAS gtest) +endif() + +if(NOT TARGET GTest::gtest_main) + add_library(GTest::gtest_main ALIAS gtest_main) +endif() + +# Prepend googletest-module/FindGTest.cmake to Module Path +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/googletest-module") diff --git a/thirdparty/build-recycle.cmake b/thirdparty/build-recycle.cmake new file mode 100644 index 0000000..db9ea8c --- /dev/null +++ b/thirdparty/build-recycle.cmake @@ -0,0 +1,5 @@ +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/recycle" EXCLUDE_FROM_ALL) +add_library(steinwurf::recycle ALIAS recycle) + +# Prepend asio-module/Findrecycle.cmake to the module path +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/recycle-module") diff --git a/thirdparty/googletest b/thirdparty/googletest new file mode 160000 index 0000000..f8d7d77 --- /dev/null +++ b/thirdparty/googletest @@ -0,0 +1 @@ +Subproject commit f8d7d77c06936315286eb55f8de22cd23c188571 diff --git a/thirdparty/googletest-module/FindGTest.cmake b/thirdparty/googletest-module/FindGTest.cmake new file mode 100644 index 0000000..f62c081 --- /dev/null +++ b/thirdparty/googletest-module/FindGTest.cmake @@ -0,0 +1 @@ +set(GTest_FOUND TRUE CACHE BOOL "Found Google Test" FORCE) \ No newline at end of file diff --git a/thirdparty/recycle b/thirdparty/recycle new file mode 160000 index 0000000..a20d211 --- /dev/null +++ b/thirdparty/recycle @@ -0,0 +1 @@ +Subproject commit a20d211776e30edd64a49d21157a522424352980 diff --git a/thirdparty/recycle-module/Findrecycle.cmake b/thirdparty/recycle-module/Findrecycle.cmake new file mode 100644 index 0000000..bd3e37b --- /dev/null +++ b/thirdparty/recycle-module/Findrecycle.cmake @@ -0,0 +1 @@ +set(recycle_FOUND TRUE CACHE BOOL "Found steinwurf::recycle" FORCE) \ No newline at end of file